Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save yvesvanbroekhoven/056e78477781ecd843ae to your computer and use it in GitHub Desktop.
Save yvesvanbroekhoven/056e78477781ecd843ae to your computer and use it in GitHub Desktop.

Loading Pages Via AJAX Into An Overlay

In order to do this we need a few things. These will be explained in detail later on.

  • The default content (the content which is there when closing the overlay)
  • The overlay content
  • A return path, i.e. the desired path when closing the overlay

TODO:

  • History.pushState should be used instead of replaceState
  • Support navigating through history and show/hide overlay

The Content

Say that you have the following routes, having a grid of images on every page and on some pages there's an overlay where content will be loaded into.

"/work"                     # work — grid with all images
"/work/category"            # work_category — grid with images from this category
"/work/1-title"             # work_detail — work detail overlay + grid with all images
"/about"                    # about — about overlay + grid with all images

Visiting the URLs via the browser

When I visit /work, I should see all the items in the grid and no overlay.
When I visit /work/category, I should see all the items from that category and no overlay.
When I visit /work/1-title, I should see all items below the overlay and the work item content in the overlay.
When I visit /about, I should see all items below the overlay and the about content in the overlay.

Fetching the URLs via AJAX

When I fetch /work, I should do nothing special, because there is no overlay content here.
When I fetch /work/category, I should do nothing special, because there is no overlay content here.
When I fetch /work/1-title, I should present only the overlay content.
When I fetch /about, I should present only the overlay content.

What search engines should see

For /work, I should get all the items in the grid.
For /work/category, I should get all the items from that category in the grid.
For /work/1-title, I should get the work item content and nothing else.
For /about, I should get the about content and nothing else.

The Return Path and Title

# Helpers

module ApplicationHelper

  def return_path
    case controller.action_name
    when "work_detail" then "/work"
    when "about" then "/"
    else request.path
    end
  end
  
  def return_title
    ...
  end

end
<body data-return-path="<%= return_path %>" data-return-title="<% return_title %>">
  ...
</body>

In words

When you visit a page that routes to the work_detail action, an overlay will be shown. When we close this overlay, we will go to the /work path.

The Setup

Step 1 — The Grid

Say that the grid is a partial that you can render into your html.
Then we can do the following:

<!-- Grid Partial -->

<grid>
  <% images.each do |img| %>
    <img src="<%= img.src %>" />
  <% end %>
</grid>
<!-- Page HTML, layout, whatever, ... -->

<% if should_show_default_content %>
  <%= render partial: "grid" %>

<% elsif should_hide_default_content %>
  <script class="hidden-default-content" type="text/html">
    <%= render partial: "grid" %>
  </script>

<% else %>
  <%# Don't render anything %>

<% end %>
  1. should_show_default_content applies to the /work and /work/category routes.
  2. should_hide_default_content applies to the /work/1-title and /about routes, but not via AJAX/XHR.
  3. else applies to the /work/1-title and /about routes, only via AJAX/XHR.

The Helpers

Ruby (backend)
# Helpers

module ApplicationHelper

  DEFAULT_PAGES = ["work", "work_category"]

  def should_show_default_content
    # true if if the current page is a default page (i.e. not a overlay page)
    case controller.action_name
    when *DEFAULT_PAGES then true
    else false
    end
  end

  def should_hide_default_content
    # true if it is not an AJAX request
    !request.xhr?
  end

end
Javascript (frontend)
// show_hidden_default_content.js

$(".hidden-default-content").each(function() {
  $(this).replaceWith(this.innerHTML);
});

Step 2 — The "Overlay" Content

<div class="should-belong-in-overlay" style="display: none;">
  E.G. ABOUT
</div>

This way search engines can see the html for the actual content on that route, but you won't see an initial flash of the content in the browser. The next step is to move this html into the overlay, by using javascript.

// should_belong_in_overlay.js

var $sbio = $(".should-belong-in-overlay").detach();
var html = $sbio.html();

if (html) {
  overlay.append_content(html);
  overlay.show()
}

Step 3 — The Routing

Backend

Intercept the request if it's an xhr request and then return json instead of html. The following piece of code will return the title and the html of the template that was supposed to be rendered (without the layout).

module ApplicationHelper

  def title
    "Title and stuff"
  end
  
end
class PagesController < ...
  before_filter :return_json_when_xhr
  ...

private

  def return_json_when_xhr
    if request.xhr?
      render json: {
        title: view_context.title,
        html: render_to_string(template: "pages/#{self.action_name}", layout: false)
      }

      # nothing should happen after this
      return false
    end
  end
  
end

Frontend

Here we will use the History API, without a fallback. That is, on old browsers the page will reload, but it will still work. You can copy the following pretty much completely. You only have to replace the NAMESPACE parts, etc.

Navigation.js
// navigation.js

(function() {

"use strict";


function NS() {
  this.state = {};

  this.retrieve_return_pathname();
  this.retrieve_return_document_title();
}


//
//  Checks
//
NS.prototype.can_stay_on_the_same_page = function() {
  return Modernizr.history;
};


//
//  Getters
//
NS.prototype.retrieve_return_pathname = function() {
  var return_pathname =
    document.body.getAttribute("data-return-path") ||
    window.location.pathname;

  // chomp it
  return_pathname = return_pathname.length > 1 ?
    return_pathname.replace(/\/$/, "") :
    return_pathname;

  // state
  this.state.return_pathname = return_pathname;
};


NS.prototype.retrieve_return_document_title = function() {
  var return_document_title =
    document.body.getAttribute("data-return-document-title") ||
    document.title;
  
  // state
  this.state.return_document_title = return_document_title;
};


//
//  Setters
//
NS.prototype.set_document_title = function(title) {
  document.title = title;
};


NS.prototype.go_to_page = function(pathname, title, skip_replace) {
  var associated_path, associated_path_split,
      $header, $li, $previous_li, $active_li;

  // chomp
  pathname = pathname.length > 1 ?
    pathname.replace(/\/$/, "") :
    pathname;

  // document title
  this.set_document_title(title, pathname);

  // replace url if needed
  if (!skip_replace) {
    if (this.can_stay_on_the_same_page()) {
      history.replaceState({}, title, pathname);
    } else {
      window.location.href = pathname;
    }
  }
};


NS.prototype.go_to_return_page = function(skip_replace) {
  this.go_to_page(this.state.return_pathname, this.state.return_document_title, skip_replace);
};


//
//  Make an instance
//
window.NAMESPACE.initialize_navigation = function() {
  var instance = new NS();
  window.NAMESPACE.navigation = instance;
};


}());
Overlay triggers.js
// overlay_triggers.js

(function() {

"use strict";


function OT() {
  this.bind_events();
}


//
//  Content
//
OT.prototype.add_content_via_url = function(url) {
  var dfd = $.Deferred();
  var ot = this;

  $
  .when(this.get_content(url))
  .then(function(obj) {
    ot.add_content(obj);
    dfd.resolve(obj);
  }, function() {
    dfd.reject();
  });

  return dfd.promise();
};


OT.prototype.get_content = function(url) {
  var dfd = $.Deferred();

  $.ajax(url, {
    contentType: "json",

    success: function(response) {
      dfd.resolve({
        content_html: response.html,
        document_title: response.title
      });
    },

    error: function() {
      dfd.reject();
    }
  });

  return dfd.promise();
};


OT.prototype.add_content = function(obj) {
  var $elem = $(obj.content_html).css("display", "none"),
      self = this;

  overlay.append_content($elem);

  $elem.css("display", "block");
  $elem = null;
};


//
//  Events
//
OT.prototype.bind_events = function() {
  $(document.body).on(
    "click.overlay_trigger",
    ".overlay-trigger",
    $.proxy(this.overlay_trigger_click_handler, this)
  );
  
  $(window).on(
    "overlay.hide.default",
    $.proxy(this.overlay_hide_handler, this)
  );
};


OT.prototype.overlay_trigger_click_handler = function(e) {
  var href = e.currentTarget.getAttribute("href");
  var title;

  // prevent default
  e.preventDefault();

  // show overlay and load content
  if (NAMESPACE.navigation.can_stay_on_the_same_page()) {
    overlay.show();

    $.when(this.add_content_via_url(href))
     .then(function(obj) {
       NAMESPACE.navigation.go_to_page(href, obj.document_title);
    });

  } else {
    NAMESPACE.navigation.go_to_page(href, null);

  }
};


OT.prototype.overlay_hide_handler = function(e) {
  NAMESPACE.navigation.go_to_return_page();
};


//
//  Make an instance
//
window.NAMESPACE.initialize_overlay_triggers = function() {
  var instance = new OT();
  window.NAMESPACE.overlay_triggers = instance;
};


}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment