Skip to content

Instantly share code, notes, and snippets.

@icidasset
Last active August 29, 2015 14:04
Show Gist options
  • Save icidasset/0838b5ecdd484cf7b487 to your computer and use it in GitHub Desktop.
Save icidasset/0838b5ecdd484cf7b487 to your computer and use it in GitHub Desktop.
Loading Pages Via AJAX Into An Overlay

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 Default Content (Grid in this case)

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

  PAGES_WITH_ONLY_DEFAULT_CONTENT = [
    "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 *PAGES_WITH_ONLY_DEFAULT_CONTENT 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

Add this plugin to your project.
Include the javascript and css.
And then make an instance of the overlay.

(function() {

"use strict";


NAMESPACE.overlay = new Overlay();


}());

Step 3 — 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
// replace 'NAMESPACE.overlay' with your overlay instance

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

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

Step 4 — 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 < ...
  def action
    @getting_stuff = FromTheDatabase.all
    return_json_when_xhr
  end

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.

Router.js
// router.js

(function() {

"use strict";


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

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


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


//
//  Getters
//
Router.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;
};


Router.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
//
Router.prototype.set_document_title = function(title) {
  document.title = title;
};


Router.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;
    }
  }
};


Router.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_router = function() {
  var instance = new Router();
  window.NAMESPACE.router = 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;

  NAMESPACE.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.router.can_stay_on_the_same_page()) {
    NAMESPACE.overlay.show();

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

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

  }
};


OT.prototype.overlay_hide_handler = function(e) {
  NAMESPACE.router.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