= Ajax A Ruby on Rails plugin to 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. 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". Ajax is being used live in production on altnet.com[http://altnet.com], if you would like to try it out (as of May 1st 2010 or thereabouts). == Install === Rails 3 1. Add the gem to your Gemspec gem 'ajax' 2. bundle install 3. rake ajax:install === Rails 2.x.x As a Gem 1. Add the gem as a dependency in your config/environment.rb config.gem 'ajax' 2. rake gems:install 3. Add to your Rakefile begin require 'ajax/tasks' rescue Exception => e puts "Warning, couldn't load gem tasks: #{e.message}! Skipping..." end 4. rake ajax:install to install required files. This command is safe to run multiple times. It will skip existing files. As a Plugin ./script/plugin install http://github.com/kjvarga/ajax.git === Sample Output $ 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.2rc.js created: public/javascripts/jquery.address-1.2rc.min.js created: public/javascripts/jquery.json-2.2.min.js created: public/images/ajax-loading.gif $ rake ajax:install skipped: app/controllers/ajax_controller.rb exists! skipped: app/views/ajax/framework.html.erb exists! ... === Post Install 1. Create layouts in app/views/layouts/ajax/ that mimic your existing layouts. See Request Handling -> Layouts. 2. Instantiate an instance of the Ajax class in application.js. For example: window.ajax = new Ajax({ default_container: '#main', enabled: true, lazy_load_assets: false }); == Introduction Ajax handles these common problems: * SEO/Crawlability/Google Analytics support * Browser History * Bookmarkability / Deep-linking * Redirects * Cookies * Lazy-loaded Assets * Activating Tabs * Request Rewriting & Redirecting Ajax converts a traditional Rails application to use 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 http://example.com/#/beyonce/albums into a browser. 2. Server receives request for http://example.com/ and renders the framework page. 4. AJAX request for http://example.com/beyonce/albums is initiated by client-side JavaScript, and received by the server. 5. Server renders http://example.com/beyonce/albums. 6. 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: 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 robot sees the traditional Rails application. 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. == 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: * {jQuery Address 1.2RC}[http://www.asual.com/jquery/address/] * jQuery JSON 2.2 === Ruby and Rails: * Rails 2.3.4 running Ruby 1.8.7 and Ruby 1.9 * Rails 2.3.5 running Ruby 1.8.7 and Ruby 1.9 === Browsers: (See {jQuery address supported browsers}[http://www.asual.com/jquery/address/docs/].) * 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}[http://rdoc.info/projects/kjvarga/ajax] == 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. == 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? My solution is to first look in app/views/layouts/ajax/ for a layout with the same name as the default layout for the current action. If a layout is found, we use it, otherwise the default layout is used. In your Ajax layouts you can define callbacks, tabs to activate or the container to receive content. For example, 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 Gists * {ajax/application.html.haml}[http://gist.github.com/373133#file_application.html.haml] * {ajax/two_column.html.haml}[http://gist.github.com/373133#file_two_column.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 from the link's HREF. The Ajax JavaScript class listens for clicks on any link with a data-deep-link attribute and loads the link's content using AJAX. 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}[http://gist.github.com/373133#file_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. window.ajax.prependOnLoad 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 1. Safari {scoping issues}[http://forum.jquery.com/topic/dealing-with-globaleval-and-safari-suggestion-for-a-better-approach] 3. {Inconsistent support for script.onload}[http://unixpapa.com/js/dyna.html] 3. Badly written JavaScript libraries To ease some of the pain, observe some of the following advice: 1. Never use {document.write}[http://javascript.crockford.com/script.html] 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}[http://api.jquery.com/live/] 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 * We use RSpec * See Ajax::Spec::Helpers and Ajax::Spec::Extension {in the rdocs}[http://rdoc.info/projects/kjvarga/ajax] * Copy ajax/spec/integration/ajax_spec.rb into your project to ensure that the Ajax integration always works. == Contributions Contributions are welcome. Please fork the project and send me a pull request with your changes and Spec tests. == Useful Resources * {AJAX site crawling specification}[http://code.google.com/web/ajaxcrawling/docs/getting-started.html]. * AjaxPatters[http://ajaxpatterns.org/] useful discussion of AJAX-related problems and their solutions. * {jQuery Address}[http://www.asual.com/jquery/address/] JavaScript library for managing the URL and deep-linking. Copyright (c) 2010 Karl Varga, released under the MIT license