Augment a traditional Rails application with a completely AJAX frontend, while transparently handling issues important to both the enterprise and end users, such as testing, SEO and browser history.
Ruby JavaScript
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.
app
config/initializers
lib
public
rails
spec
tasks
.gitignore
Gemfile
Gemfile.lock
MIT-LICENSE
README.rdoc
Rakefile
VERSION
ajax.gemspec
init.rb

README.rdoc

Ajax

Ajax augments a traditional Rails application with a completely AJAX frontend, while transparently handling issues important to both the enterprise and end users. Issues like SEO/Crawlability, browser history, deep-linking and testing.

The Ajax philosophy is that you shouldn't have to develop for AJAX. Your code shouldn't change; your tests shouldn't change; and the way Google sees your site shouldn't change.

The beauty of Ajax is that your Rails application only ever sees traditional requests, so it does not have to be “Ajax aware”. The Ajax framework does not interfere with your existing AJAX requests. AJAX requests pass through the framework unmodified. If the request headers contain the special Ajax-Info header then we invoke some additional Ajax framework handling.

As of May 2010 Ajax is being used live in production on kazaa.com. Try it out and create some wicked playlists while you're at it!

Changelog

  • v1.2.10: Fix layout detection under Rails 3.1

  • v1.2.9: Fix integration specs under Rails 3.1

  • v1.2.8: Support Rails 3.1

  • v1.2.7: Activate tabs after inserting the response into the page

  • v1.2.5: Loading image improvements. Add onNextPageLoad and onEveryPageLoad callbacks

  • v1.2.4: Fix special redirect for Google-crawlable URLs

  • v1.2.2: Fix redirect issue when redirecting back to the referer; Strip cache-busting parameter from the Referer URL that is sent in the Ajax-Info

  • v1.2.1: Make Google-crawlable URLs configurable and default it off for backwards compatibility; Snapshot requests are always supported regardless of the value of </tt>crawlable</tt>.

  • v1.2.0: Support Google-crawlable URLs by default

  • v1.1.8: Fix redirect issue caused by setting bad HTTP headers in URL rewrite

  • v1.1.7: Fix layout handling for Rails 3

  • v1.1.6: Fix redirect_to to handle resourceful redirects. [Rails 3]

  • v1.1.5: Fix inclusion of controller and layout in Ajax-Info response header. Improve RSpec 1.* integration

  • v1.1.4: Fix RSpec 2 integration

  • v1.1.3: Guard against possible nil values for the redirect_to url and the referrers

  • v1.1.2: Fix Rails 3 render hook so it doesn't break rendering with no layout e.g. render :json => ...

  • v1.1.1: Backwards compatibility fix for Rails < 3

  • v1.1.0: Rails 3 supported!

Install for Rails

After getting the Gem installed, take a look at Getting Started for more information about setting up your application for Ajax.

Rails 3

Add the gem to your Gemfile:

gem 'ajax'

Then run bundle.

Rails 2 Gem

  1. Follow the Rails 3 install if you are using a Gemfile.

    If you are not using a Gemfile add the gem to your config/environment.rb configuration block with:

    config.gem 'ajax'

    Then run rake gems:install.

  2. Include the gem's Rake tasks in your Rakefile:

    begin
      require 'ajax/tasks'
    rescue Exception => e
      puts "Warning, couldn't load gem tasks: #{e.message}! Skipping..."
    end
  3. Add a route for the framework path to your config/routes.rb:

    ActionController::Routing::Routes.draw do |map|
      Ajax::Routes.draw(map)
    end

Rails 2 Plugin

Run script/plugin install http://github.com/kjvarga/ajax.git from your application's root directory.

Getting Started

  1. First run rake -T ajax to see the Rake tasks provided and to verify that the gem and its Rake tasks are being included properly.

  2. Run rake ajax:install to install required asset files into various application directories. The output from running the command will list the files that are created.

  3. Run rake routes to verify that the /ajax/framework path is being routed correctly. It should route to AjaxController#framework.

    The route will automatically be added to your application if you are running Rails 3 or Ajax is installed as a plugin. If you are running Rails 2 you must add it to your routes file yourself.

    If you have catch-all routes they may take precedence, so if that is the case you will have to add it to the top of your config/routes.rb to ensure it comes first.

    Rails 2:

    ActionController::Routing::Routes.draw do |map|
      Ajax::Routes.draw(map)
    end

    Rails 3:

    YourAppName::Application.routes.draw do
      Ajax::Routes.draw(self)
    end
  4. Include the JavaScript files in your application layout file:

    # app/views/layouts/application.html.erb
    <%= javascript_include_tag 'jquery', 'jquery.json-2.2.min', 'jquery.address-1.4', 'ajax', 'application' %>

    While you are in this file, add a container element that will be the default container to receive content loaded via the Ajax framework. Usually this is the main body of the page below the header. For example:

    # app/views/layouts/application.html.erb
    <div id="main">
      <%= yield %>
    </div>

    NOTE: jQuery is NOT provided for you by the install. You will need to download it to your public/javascripts directory yourself.

  5. Instantiate an instance of the Ajax JavaScript class in your application.js. This object will handle clicks on links, communication with the server and provide methods that you can use for custom behaviour.

    An example of creating the Ajax instance:

    // public/javascripts/application.js
    if (typeof(Ajax) != 'undefined') {
      window.ajax = new Ajax({
        default_container: '#main',  // jQuery selector of your container element
        enabled: true,               // Enable/disable the plugin
        lazy_load_assets: false      // YMMV
      });
    }

    Make sure you set your default_container correctly. The selector you use must match the container element you added in app/views/layouts/application.html.erb in the previous step.

  6. Ajax should now be installed and configured and ready to handle requests. Start your Rails server, open your favorite browser and load the home page. Using the browser's developer tools take a look at the console. You should not see any JavaScript errors.

    If everything is working correctly when you load the root url / the URL should change to /#! and you should see some output in the console like:

    [ajax] loadPage /
    GET http://localhost:3000/?_=1304973841796
    [ajax] in response handler! status: 200
    [ajax] extracted body [6..1828] chars
    [ajax] using container #main
    [ajax] got ajax-info Object {}

    In the server logs your should see two requests. One is a normal GET request for / which should be processed by AjaxController#framework and another which is an AJAX GET request also for / which is handled by whatever you have set your application's root path handler to.

    Congratulations on getting everything setup and working! Now you can start customizing your setup further and adding new functionality.

  7. At this point it is a good idea to include the Ajax integration specs in your application to ensure that Ajax is properly integrated going forward. If you are running RSpec 1 or 2 this is as simple as running:

    rake ajax:install:specs

    This adds spec/integration/ajax_spec.rb to your specs. Run your specs and verify that they are all passing.

Next Steps

  1. Ajax looks for a layout to use for an Ajax-handled request in app/views/layouts/ajax/. It looks for a layout with the same name as the Rails layout that is configured for that action. So copy your existing layouts into layouts/ajax/ and get them ready by removing all the excess HTML, like the HEAD section and the BODY element. You want to leave just the HTML content that will be inserted into your container element.

    Here is an example of converting our layouts/application.html.haml to layouts/ajax/application.html.haml.

  2. Add a data-deep-link attribute to links that you want to load using the Ajax framework. A jQuery live event automatically intercepts all clicks on links with this attribute and loads their content using the framework.

    By default all links that use the Rails link helpers will include this attribute, so you won't need to do anything.

    When a link with this attribute is clicked, content is requested using AJAX and goes through the Ajax framework. Rails receives a request for content at the data-deep-link location. The content is rendered using the default layout for the action but from the app/views/layouts/ajax directory. The rendered content is received client-side and special Ajax headers are processed. The content is then inserted into the specified container, or the default_container defined in your Ajax JS class above.

  3. To submit forms using the Ajax framework, or to manually request content using various request methods, you can call window.ajax.loadPage, passing in options like +url, method and data.

    For example to handle form submissions with Ajax, you could use code like the following:

    $('.radio-search form').live('submit', function(e) {
      var form = $(e.target);
      window.ajax.loadPage({
        url: form.attr('action'),
        method: 'POST',
        data: form.serialize()
      });
    });
  4. Specify a container to receive Ajax content on a per-action, per-controller, per-layout or dynamic basis using the ajax_header :container, '<jquery css selector>' helper method.

    For example:

    # app/views/layouts/ajax/two_column.html.haml
    ajax_header :container, '#twocolumns'

    Ajax provides helper methods for you to use. In your controllers you can call ajax_header and ajax_layout to add configuration for the whole controller or on a per-action basis:

    For example:

    # app/controllers/application_controller.rb
    ApplciationController < ActionController::Base
      ajax_header :tab, "#app-tab"
    
      def maintenance
        ajax_layout :maintenance
        render
      end
    end

    In your views you only have access to ajax_header.

    Lots of other useful methods are provided by the <tt>Ajax<tt> module. Take a look at the documentation for more information.

  5. Specify tabs that should be activated on a per-action, per-controller or dynamic basis using the ajax_header :tab, '<jquery css selector>' helper method. The element(s) that match the selector will have their activate event triggered, so you will need to setup an event handler.

    For example:

    # app/controllers/pages_controller.rb
    ajax_header :tab, '#header .nav li:contains(Tour)', :only => :tour
    
    # public/javascripts/application.js
    $('#header .nav li').live('activate', function() {
      $(this).siblings().removeClass('active').end().addClass('active');
    });
  6. Specify paths that should bypass the Ajax framework. I.e. accessing these paths using a full URL (not a hashed URL), will render the page like in a traditional Rails app. If that path was not excepted the Ajax framework would have forced a redirect to the hashed version of the URL before rendering the page contents using Ajax. See Excepted Links.

    For example:

    # config/initializers/ajax.rb
    Ajax.exclude_paths %w[ /login /logout /signup ]

Introduction

Features and Support

  • SEO/Crawlability/Google Analytics support

  • Browser History

  • Bookmarkability / Deep-linking

  • Redirects

  • Cookies

  • Lazy-loaded Assets

  • Activating Tabs

  • Request Rewriting & Redirecting

  • Jammit compatible with these new helpers. Also supported are stylesheets with embedded images.

Ajax augments a traditional Rails application with a completely AJAX frontend.

What do I mean by “completely AJAX”? Everyone uses AJAX. What we mean when we say “completely AJAX” is that the main page is only loaded once. Every link now loads content via AJAX.

But if we do that, the URL will never change and we will have no history, because that is how browsers determine history. It turns out the only way to change the URL without causing the browser to issue a new request, is to modify the named anchor - or “hashed” - part of the URL.

So now your traditional links auto-magically load content via AJAX into a page container and update the browser URL with the new URL. You have all the benefits of AJAX as well as history and link bookmarkability.

Where before you would have seen something like http://example.com/the-beatles/history, now you would see http://example.com/#!/the-beatles/history. Notice the #/?

How does it work?

Ajax comprises Rack middleware, Rails integrations and some JavaScript libraries to handle everything from redirecting and rewriting incoming requests, to managing the response headers and content, to handling the browser URL, JavaScript callbacks and client-side events.

Browsers do not send the hashed part of the the URL with page requests, so to an Ajax-ed application, all requests look like they are for root.

In order to load the correct page we must first render a framework page with accompanying JavaScript. The JS examines the URL and then issues another request to the server for the hashed part (which may still be / if the user requested the home page).

An Example User Interaction

  1. User pastes example.com/#!/beyonce/albums into a browser.

  2. Server receives request for example.com/ and renders the framework page.

  3. AJAX request for example.com/beyonce/albums is initiated by client-side JavaScript, and received by the server.

  4. Server renders example.com/beyonce/albums.

  5. Response headers are processed and the response inserted into the page container.

Request Handling

Ajax uses a custom HTTP header Ajax-Info to pass JSON back and forth between the client and server. The client sends information about the state of the container, and the server sends new information back.

By default the current layout is sent in the Ajax-Info header. This can be useful for determining which assets to include, or layout to render in your response.

Ajax-Info headers with special meaning:

title

Sets the page title.

tab

jQuery selector, triggers the activate event on matched element(s).

container

jQuery selector, the container to receive the content (default: ajax.default_container).

assets

Hash of JavaScript and CSS assets to load { :javascripts => [], :stylesheets => [] }

callbacks

List of string callbacks to execute after assets have finished loading.

Robots and External APIS

We detect robots by their User-Agent strings. If a robot is detected the Ajax handling is bypassed and the robotsees the traditional Rails application. The robot will see traditional links and requests for those pages will load traditionally and bypass all Ajax handling.

Check out the robot User-Agent detection. If we cannot identify a robot, the robot will receive a redirect on each request for a traditional URL. Requests for hashed URLs will only render the framework because there will be no JavaScript to trigger loading the inner contents. It's important that the User-Agent list be kept up-to-date, or some other method of detecting robots is found. IP address perhaps?

By default any AJAX or non-GET requests pass through unmodified.

If you need to expose external APIs you can do so using a regular expression that is matched against incoming URLs. See Documentation->Configuration->Excepted Links.

Compatibility

You must be running jQuery >= 1.4.2 to use this plugin. Sorry, Prototype users.

The following JavaScript libraries are required and included in the plugin:

Ruby and Rails:

  • Rails 2.3.4, 2.3.5, 3.0.5, 3.0.7

  • Ruby 1.8.7, 1.9.1, 1.9.2

Browsers:

(See jQuery address supported browsers.)

  • Internet Explorer 6.0+

  • Mozilla Firefox 1.0+

  • Safari 1.3+

  • Opera 9.5+

  • Chrome 1.0+

  • Camino 1.0+

Documentation

Please browse the API documentation at rDoc.info

Rake Tasks

Here are the rake tasks provided and sample output from running them.

To see the tasks provided:

$ rake -T ajax
rake ajax:install            # Install required Ajax files.
rake ajax:install:specs      # Copy Ajax integration spec into spec/integration/ajax_spec.rb.
rake ajax:update:javascript  # Overwrite public/javascripts/ajax.js with the latest version.

The install task:

$ rake ajax:install
created: app/controllers/ajax_controller.rb
created: app/views/ajax/framework.html.erb
created: config/initializers/ajax.rb
created: public/javascripts/ajax.js
created: public/javascripts/jquery.address-1.3.js
created: public/javascripts/jquery.address-1.3.min.js
created: public/javascripts/jquery.json-2.2.min.js
created: public/images/ajax-loading.gif

Copy an integration RSpec into your spec/ directory:

$ rake ajax:install:specs
already exists: spec/integration/ajax_spec.rb

Update the ajax.js javascript file:

$ rake ajax:update:javascript
created: public/javascripts/ajax.js

Configuration

It is important to be able to disable the plugin when you don't want it interfering, like when you are testing. You will also want to ensure that your site's JavaScript still works when the plugin is disabled.

If Ajax is disabled, your site will act like a traditional Rails application. Because each request will be a traditional request, callbacks specified in the Ajax-Info header will not be parsed by the browser, and so will not execute.

Callbacks added directly to the window.ajax instance will still be executed, and they will execute immediately.

To disable the plugin in your environment file:

# config/environments/test.rb
Ajax.enabled = false

If you need to, you can check the state of the plugin with:

Ajax.is_enabled?

Other onfiguration goes in config/initializers/ajax.rb such as indicating which links to except from the request processing. See Excepted Links.

Our config/initializers/ajax.rb file:

# config/initializers/ajax.rb
Ajax.enabled = true
Ajax.lazy_load_assets = false

# Excepted paths: allow these paths to pass through unmodified.
Ajax.exclude_paths %w[ /login /logout /signup /altnet-pro /my-account/edit /user-session/new /facebook_signup /facebook_login /facebook_link_account /health_check /reset-password/new]
Ajax.exclude_paths [%r[\/my-account\/.*]]
Ajax.exclude_paths [%r[\/admin.*]]
Ajax.exclude_paths [%r[\/newrelic.*]]

Ajax Layouts

Typically AJAX content does not render a layout because we just want to update a fragment of a page. Automatically turning off layouts when rendering AJAX is one option, but what about when we do want to use a layout?

Ajax looks for an alternative layout to use with AJAX requests in app/views/layouts/ajax/. If a layout is found, we use it, otherwise the default layout is used. Copy existing layouts into this directory and get them ready for AJAX by removing any HTML HEAD elements, everything but the inner BODY content.

Your main layout should contain a container element that will receive page content. Typically this would be the container below the page header. If you don't have a static header, you can make the whole BODY element the container.

In your Ajax layouts you can define callbacks, tabs to activate or the container to receive content. Using the ajax_header method:

# layouts/ajax/two_column.html.haml
ajax_header :container, '#column2'

Our layouts:

layouts/
  _assets.html.haml
  ajax/
    application.html.haml
    single_column.html.haml
    two_column.html.haml
  application.html.haml
  single_column.html.haml
  two_column.html.haml

Converting our layouts/application.html.haml to layouts/ajax/application.html.haml.

Link Handling

All links which are rendered using the link_to (or any other url) helper method automatically include a data-deep-link attribute containing the path portion of the link's HREF URL.

The Ajax JavaScript class listens for clicks on any link with a data-deep-link attribute and loads the link's content using the Ajax framework.

Should you need to, you can set this attribute on a link by passing in HTML options to link_to:

link_to odd_url, {}, { :data-deep-link => '/even/odder/url' }

To manually mark a link as traditional, pass :traditional => true or :data-deep-link => nil.

Excepted Links

Excepted links bypass Ajax request and link handling. I call these traditional links.

Links can be excepted by passing in strings or regular expressions to Ajax.exclude_paths(). Only pass the path and not the full URL. The path will be modified to match against other paths as well as against full URLs:

# config/initializers/ajax.rb
Ajax.exclude_paths %w[ /login /logout /signup /altnet-pro /my-account/edit /user-session/new ]
Ajax.exclude_paths [%r[\/my-account\/.*]]

Typically, we except pages that require HTTPS, like signup forms, because including secure forms on an insecure page often triggers a browser warning.

Excepted links when rendered do not contain the data-deep-link attribute if they are rendered with the link_to (or any other url) helper method.

Rails Helpers

Use the ajax_header helper in your controllers or views to add data to the Ajax-Info header. Values are converted to JSON before being sent over the wire. Internally this function uses Ajax.set_header.

You can use Ajax.get_header to inspect Ajax-Info header values. See the In Views example.

In Controllers

In controllers, ajax_header uses an after_filter to add content to the response. It therefore accepts passing a block instead of a static value, as well as :only and :except modifiers, e.g:

# app/controllers/application_controller.rb
ajax_header :title { dynamic_page_attribute(:page_title) || "Music @ Altnet" }
ajax_header :assets do
  { :stylesheets => [current_controller_stylesheet] }
end

# app/controllers/browse_controller.rb
ajax_header :tab, '#header .nav li:contains(Music)', :only => [:music, :artists, :albums, :tracks, :new_releases]
ajax_header :tab, '#header .nav li:contains(Playlists)',  :only => :playlists
ajax_header :tab, '#header .nav li:contains(DJs)', :only => :djs

# app/controllers/activity_controller.rb
ajax_header :tab, '#header .nav li:contains(Realtime)'
ajax_header :assets, { :javascripts => javascript_files_for_expansion(:juggernaut_jquery) }

Array and Hash values are merged so you can call ajax_header multiple times. For example, the asset Hash and Array values will be merged.

In Views

The syntax is similar to the controller version, except that we do not use an after_filter, so you cannot pass a block or :only and :except modifiers.

See ajax/two_column.html.haml for an example.

Lazy-loading Assets

KJV 2010-04-22: Browser support for callbacks (specifically the problem of calling them only after all assets have loaded) is patchy/inconsistent at this time so lazy-loading is not recommended. It has been disabled by default. Once all browsers can be supported this may change.

The recommended way of dynamically enabling/disabling lazy loading:

# environment/initializer
Ajax.lazy_load_assets = false # or true

# application layout (HAML example)
:javascript
  var AJAX_LAZY_LOAD_ASSETS = #{Ajax.lazy_load_assets?};

if !Ajax.lazy_load_assets
  include_all_assets
end

# application.js
window.ajax = new Ajax({
  lazy_load_assets: window.AJAX_LAZY_LOAD_ASSETS !== undefined ? window.AJAX_LAZY_LOAD_ASSETS : false
});

Use ajax_header :assets { :stylesheets => [], :javascripts => [] } to define assets that a page depends on. These assets will be loaded before the response content is inserted into the DOM.

  1. Assets that have already been loaded are not loaded again

  2. Assets that are loaded, remain loaded (watch out for CSS conflicts and JS memory leaks)

  3. If lazy-loading assets is disabled, assets in the Ajax-Info header are ignored, but callbacks are still executed.

Often you will need to perform some DOM manipulations on the newly inserted content, or instantiate JavaScript objects that are defined in a lazy-loaded JS file. To execute some JavaScript after all assets have been loaded and the new content has been inserted, use JavaScript Callbacks.

JavaScript Callbacks

JavaScript callbacks can be added to the response and will be executed after any assets in Ajax-Info['assets'] have been loaded. (If lazy loading assets is disabled, they are executed immediately.)

You can bind callbacks directly to the window.ajax object in your view, for example, in HAML we could have:

:javascript
  window.ajax.onLoad(function() {
    window.juggernaut = new window.Juggernaut(#{juggernaut_options.to_json});
    window.liveFeed.init();
  });

  window.ajax.prependOnLoad(function() {
    $(document).trigger('player.init');
  });

In the onLoad callback I'm scoping everything to window to avoid scoping issues in different browsers.

<b>window.ajax.prependOnLoad<b> adds the callback to the front of the queue.

Alternatively callbacks can be passed as a list of Strings in the Ajax-Info header using the ajax_header helper:

ajax_header, :callbacks, 'window.player.init();'

These callbacks are executed in the global scope. This method of adding callbacks is not recommended for two reasons:

  1. Safari has trouble with some String callbacks.

  2. If Ajax is disabled, these callbacks will not be executed, because the Ajax-Info header will not be set.

    However, callbacks added directly to the window.ajax instance will still be executed, and they will execute immediately, so your code continues to work as expected.

JavaScript Gotchas

Most of the problems you will likely encounter from a change to Ajax will be JavaScript related. These problems become more noticeable for the following reasons:

  1. JavaScript that has been loaded, remains loaded for a very long time. This can lead to:

    1. Memory leaks

    2. Callbacks executing ad infinitum, likely on content that has since been replaced.

  2. Inconsistent browser handling of JavaScript returned via AJAX:

    1. JavaScript in AJAX response is executed in local scope

    2. Safari scoping issues

    3. Inconsistent support for script.onload

  3. Badly written JavaScript libraries

To ease some of the pain, observe some of the following advice:

  1. Never use document.write

  2. Use window to avoid scoping issues.

  3. Modify your third-party JavaScript libraries to also assign classes etc to window.

  4. Use jQuery live events

  5. Dynamically turn off repeating callbacks e.g.

    function my_repetitive_callback() {
      if ($(selector).size() == 0) {
        // Turn off the interval
        if (object.interval_id !== undefined) {
          clearInterval(object.interval_id);
          object.interval_id = undefined;
        }
      } else {
        $(selector).do().some().jquery().kung().foo();
      }
    }
    
    // Start the interval.  Do this whenever a page is rendered
    // that has content we want to work with.  This will start
    // the interval running.  When we change the page, the
    // content will disappear and the interval will turn itself off.
    object.interval_id = setInterval(my_repetitive_callback, 5000);

Testing

Ajax comes with RSpec integration tests for RSpec 1 and 2 and ActiveSupport::TestCase. You don't have to add anything to your spec/spec_helper.rb file. Just run the rake task to copy the integration spec into spec/integration/ajax_spec.rb:

rake ajax:install:specs

If you want to do add your own specs, you can make use of helper methods defined in the Ajax::RSpec::Extension module. To make these methods available to all your specs just add a require to your spec/spec_helper.rb:

# spec/spec_helper.rb
require 'ajax/rspec'

If ajax/rspec has been required before the testing framework has been defined (Spec, RSpec or ActiveSupport::TestCase) the extension methods will not have been integrated. In this case you can force re-integration by calling:

Ajax::RSpec.setup

See Ajax::RSpec::Helpers and Ajax::RSpec::Extension in the rdocs

Contributions

Contributions are welcome. Please fork the project and send me a pull request with your changes and Spec tests.

Useful Resources

Copyright © 2010 Karl Varga, released under the MIT license