294: Playing With PJAX
(view original Railscast)
Pjax is a jQuery plugin, written by Chris Wanstrath, that allows you to easily update a section of a page with an AJAX request instead of having to do a full HTTP request. The demo page1 shows how this works. By default, clicking on any of the links on this page will cause the page to fully reload and we can tell that this has happened as the time on the page changes. When we enable pjax by clicking the checkbox clicking the links no longer reload a full page and the time no longer updates, although the main section of the page still changes.
Pjax uses pushState so the user won’t notice that AJAX requests are being made in the background. Each time pjax updates the page the URL in the address bar updates, the page’s title changes and the previous page is added to the browser’s history so that the back button works correctly. Pjax degrades gracefully too: if the user’s browser doesn’t support pushState or they’ve disabled JavaScript it will fall back to making a traditional HTTP request.
Integrating Pjax into a Rails Application
There are several ways we can integrate pjax into a Rails application and we’ll try two of them in this episode. The application we’ll be working with is shown below. It shows a list of products and clicking on a product reveals a sidebar showing some information about that product.
Currently when we click on the link the entire page reloads. To show how much of the page has changed the layout file and the page’s template both generate random numbers when they’re loaded. When both numbers change this shows that the entire page has been reloaded and this is what we currently see. We’ll use pjax now so that only the areas of the page that need to change are updated.
There’s a gem called pjax_rails by David Heinemeier Hansson that makes it easy to add pjax to a Rails 3.1 application and though it doesn’t quite fit the needs of our application we’ll try it anyway to show how it works. To install it we just add the pjax_rails gem to our/Gemfile
and run bundle
.
/Gemfile
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'pjax_rails'
Once Bundler has finished we need to modify our application.js
manifest file so that it loads pjax’s JavaScript.
/assets/javascripts/application.js
// This is a manifest file that'll be compiled into including all the files listed below. // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically // be included in the compiled file accessible from http://example.com/assets/application.js // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // the compiled file. // //= require jquery //= require jquery_ujs //= require pjax //= require_tree .
Finally we need to wrap the yield
call in the layout file in a div with a data-pjax-container
attribute.
/app/views/layouts/application.html.erb
<!DOCTYPE html> <html> <head> <title><%= content_for?(:title) ? content_for(:title) : "Store" %></title> <%= stylesheet_link_tag "application" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tag %> </head> <body> <div id="container"> <div class="layout_time">Layout random: <strong><%= rand(900)+100 %></strong></div> <% flash.each do |name, msg| %> <%= content_tag :div, msg, :id => "flash_#{name}" %> <% end %> <div data-pjax-container> <%= yield %> </div> </div> </body> </html>
When we restart the server and reload our page now every link on the page will be pjax-enabled. When we click a link now the page doesn’t reload fully and we can see this as only the template’s random number changes. This is almost what we want but if we look closely we’ll see that the page’s title doesn’t change when we click on one of the links.
We can fix this by adding a title
tag in the template. This is a little hacky and makes the page’s HTML invalid, but it does work.
/app/views/index.html.erb
<% content_for :title, @product ? @product.name : "Products" %> <title><%= yield (:title) %></title> <!-- Rest of page omitted -->
When we reload the page now the title changes when we click one of the links and we also have a fully-working back button and history. From the user’s perspective everything works as it did before but it feels a little faster. This is good but the pjax_rails
gem is a little aggressive. Every link on the page is pjax-enabled by default but this isn’t really what we want in our application. Likewise the whole template is always updated when we click a link and there’s no way of restricting which parts of the page are updated. There is a way of disabling pjax for certain links but having to keep track of these can be awkward. If you have a simple application whose layout file always stays the same, though, then pjax-rails may well suit you, though.
It should be possible to use Rack middleware to make a more flexible solution and this is how Gert Goet’s rack-pjax gem works so we’ll replace pjax_rails with rack-pjax in our application. First we’ll replace the pjax_rails gem with rack-pjax in the Gemfile
and run bundle
again.
/Gemfile
# gem 'pjax_rails' gem 'rack-pjax
Next we need to add the included Rack middleware to the application’s config file.
/config/application.rb
module Store class Application < Rails::Application config.middleware.use Rack::Pjax # Other config commands omitted end end
Rack-pjax doesn’t come with a jQuery plugin embedded so we’ll need to install this separately. We’ll create a vendor/assets/javascripts
directory in our application and download the jquery.pjax.js
file to it using curl
.
terminal
$ mkdir -p vendor/assets/javascripts $ curl https://raw.github.com/defunkt/jquery-pjax/master/jquery.pjax.js > vendor/assets/javascripts/jquery.pjax.js
We’ll need to include this file in our application’s JavaScript manifest file so we’ll replace pjax_rails’s JavaScript file with this one.
/assets/javascripts/application.js
//= require jquery //= require jquery_ujs //= require jquery.pjax //= require_tree .
Unlike pjax_rails, rack-pjax doesn’t pjax-enable all links by default so we need to specify the ones to enable. These are all in pages in the ProductsController
so we’ll do this in the products CoffeeScript file.
/app/assets/javascripts/products.js.coffee
jQuery -> $('.product a').pjax('[data-pjax-container]')
In this code we first make sure that the DOM has loaded then fetch the links that we want to enable. In our case this is any link inside a parent element with a product
class. We then call pjax
on them and specify what we want to update when the link is clicked.
With rack-pjax we no longer need to override the title
element and so we can remove this from the index
template. When we reload the page now it works as it did before; the template is updated when we click a link but not the layout and only the template’s random number is updated. The title is updated now, too, even though it’s not specified in the template because this is detected from the layout.
What’s really useful is that we can now move our data-pjax-container
element anywhere we want. It no longer has to be placed around the yield
call in the layout file. We’ll move it into the index
template and wrap it around the product details section, i.e. the section that displays the details for a product.
/app/views/products/index.html.erb
<% content_for :title, @product ? @product.name : "Products" %> <div class="template_time">Template random: <strong><%= rand(900)+100 %></strong></div> <h1>Products</h1> <% for product in @products %> <div class="product"> <h2> <%= link_to product.name, :product_id => product %> <span class="price"><%= number_to_currency(product.price) %></span> </h2> </div> <% end %> <p><%= link_to "New Product", new_product_path %></p> <div data-pjax-container> <% if @product %> <div id="product_details"> <h3><%= @product.name %></h3> <dl> <dt>Price:</dt> <dd><%= number_to_currency(@product.price) %></dd> <dt>Released:</dt> <dd><%= @product.released_at.strftime("%B %e, %Y") %></dd> <dt>Category:</dt> <dd><%= @product.category %></dd> </dl> <p class="actions"> <%= link_to "Edit", edit_product_path(@product) %> | <%= link_to "Destroy", @product, method: :delete, confirm: "Are you sure?" %> </p> </div> <% end %> </div>
Now when we click a link only the side panel changes and neither of the random numbers changes.
If you’re curious about how this gem works then you should take a look at its source code. It’s really simple, being just one Ruby file of about fifty lines of code. Bear in mind though that the entire layout and template are still rendered by the server before rack-pjax decides which parts to send back to the client. If rendering the template is a big performance problem then you might be better off looking at an alternative solution, but for most application this should work perfectly.
That’s it for our episode on pjax. It’s a simple solution and well worth taking a look at if you think it might fit your application’s needs.