Learning pjax - Tutorial and Screencast

This post is part of my weekly tech learning series, where I take one hour each week to try out a piece of technology that I'd like to learn.

This week I'm back to JavaScript, trying out the pjax library from Chris Wanstrath. pjax 1.0.0 was just released a few days ago and I've been itching to try it out so I took some time today to get a feel for it.

App

Since pjax is more of an infrastructure library, I decided not to try and build an actual application with it. It's an enhancement to how webpages behave so modifying an existing application would be a better fit.

Server

For use as an example, I created a simple Sinatra app that would respond to two actions (index or goodbye). Each action uses a different template and content so I could tell the difference between the two. Super simple but enough to test out pjax.

# Non-pjax version of server
require 'rubygems'
require 'sinatra'

helpers do
  def title
    @title ||= "No title"
  end
end

get '/' do
  @title = 'Welcome'
  erb :index
end

get '/goodbye' do
  @title = 'Goodbye'
  erb :goodbye
end

Layout and views

The layout and the views are pretty simple. One thing to take note of because I'll explain this later, the navigation is embedded in each view (nav element).

<%= title %>
<%= yield %>

<div id="headers"><%= headers.inspect %></div>

<div id="status">
  <%= Time.now.to_s %>
  Randomized: <%= rand(1000) %>
</div>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
<script type="text/javascript" src="/js/app.js"></script>
<%# index.erb %>

<a href="/">Welcome</a>
<a href="/goodbye">Goodbye</a>

Welcome to the application, have a nice day.
<%# goodbye.erb %>

<a href="/">Welcome</a>
<a href="/goodbye">Goodbye</a>

Sorry to see you go

Goodbye

Nothing too unusual. erb templates with a basic HTML5 layout. In the layout I have some debugging information to see the HTTP headers, current time, and a random number generated server side (an idea I got from Ryan Bates).

pjax time

Installing pjax is simple, just include its script file in the layout and make sure jQuery is loaded.

This is where I got confused and stalled a bit. pjax support three different ways to call it.

  1. Obtrusive by using a data-pjax attribute on links with a jQuery selector for the part of the page you want to replace (the container). e.g. <a href='/explore' data-pjax='#main'>Explore</a> will replace the main element.
  2. Semi-obtrusive by adding markup to links and then selecting them with jQuery. e.g. <a href='/explore' class='js-pjax'>Explore</a> and then selecting all of the .js-pjax elements.
  3. Unobtrusive by selecting the links directly.

There were two parts that confused me.

First, the examples of each of these covered the different ways to use pjax but they also added on additional things. So it wasn't a direct comparison of A vs B vs C, more like A vs B+X vs C+Y where the X and Y features are optional. To help out the next person who comes along, here is an apples-to-apples comparison:

One. Functionally obtrusive, loading the href with ajax into data-pjax:

<a href='/explore' data-pjax='#main'>Explore</a>

$(document).pjax('a\[data-pjax\]')

Two. Slightly obtrusive, passing in a container.

<a href='/explore' class='js-pjax'>Explore</a>

$('#main').pjax('.js-pjax')

Three. Unobtrusive.

<div id='main'>
  <div class='tabs'>
    <a href='/explore'>Explore</a>
    <a href='/help'>Help</a>
  </div>
</div>

$('#main').pjax('a')

This is much clearer in my opinion and doesn't confuse the comparisons with error handling or loading indicators.

The second confusing part is that by reading the documentation it sounds like all three of those ways are different but actually they are all the same way. Each one is selecting a container (#main or document) and then calling pjax on each element. The only differences are

  1. that if your container is anything except for the full document, then that element will be used as the target for the replacement. If the container is the document, then you must supply a replacement target in the data-pjax attribute.
  2. you can override a link's target by using the data-pjax attribute.

So really, all you need is: $(your-replacement-target-selector).pjax(your-link-selector).

pjax with Sinatra

The nice thing about pjax is you can reuse most of the existing code on the server. pjax requests pass through the stack like any other request, except the browser replaces the content on the page instead of drawing a new page. This means you don't need to duplicate server-side template on the client side or do any JSON encoding/decoding, which is nice.

The one change pjax recommends is to not render the layout on a pjax request. For non-Rails developers, this means you don't want to render any of the response except for the container element (e.g. #main or #content in typically apps). This is how pjax can enhance performance:

The documentation for pjax shows what to change in Rails for pjax. pjax sets a "X-PJAX" HTTP header so it's easy to check for that and toggle your layout on or off.

In my case I'm using Sinatra so the code is different than Rails, but still simple. First I created a helper method to check if this is a pjax request, is_pjax?. Then in each of my routes I changed the rendering to skip the layout for pjax requests. On a larger app I'd probably extract the layout logic to a helper method but for two actions I'm fine with some duplication.

helpers do
  # ...

  def is_pjax?
    #  headers['X-PJAX']
    env['HTTP_X_PJAX']
  end

end

get '/' do
  @title = 'Welcome'
  erb :index, :layout => !is_pjax?
end

get '/goodbye' do
  @title = 'Goodbye'
  erb :goodbye, :layout => !is_pjax?
end

From what I saw, you can use either Sintra's headers method (commented out) or Rack's env method. The beauty of Rack systems...

So now the server is configured for pjax, there are views that are working with regular request/response cycles, and jQuery and pjax are included on the page. Time to configure pjax itself.

Configuring pjax

The unobtrusive option seems the best for me so I went ahead and used the following JavaScript to configure pjax. #content is the container and the pjax enabled links are the nav links. pjax is automatically picking up the href and using that for where to get the data.

$('#content').pjax('nav a')

Pretty simple right?

More complex pjax example

Since pjax is just using css selectors you can get pretty technical with it too. Given the HTML below:

    <a href="#">Naked link</a>
    <a href="#" class="js-pjax">Class link</a>
    <a href="#" id="id">Id link</a>

    <div id="magic">
      Some content here with an <a href="#">internal Naked link</a>
    </div>

From what I understand, you can also call pjax() on multiple elements so don't feel like you need to build a complex selector to get everything at once.

Boxes, little boxes (what stalled me for a bit)

There was one gotcha that tripped me up for a bit until I understood how pjax worked. When using an element for the container (#content), only the elements inside of the container are used in the pjax selector. Lets modify the example from earlier, say you have a global navigation you want to use pjax with too.

<a href="/">Welcome</a>
<a href="/goodbye">Goodbye</a>

<div id="content">
  <a href="#">Naked link</a>
  <a href="#" class="js-pjax">Class link</a>
  <a href="#" id="id">Id link</a>

  <div id="magic">
    Some content here with an <a href="#">internal Naked link</a>
  </div>
</div>

In this case $("#content").pjax('a'); will only get you the links in the content, even though the selector is 'a', because pjax is scoping its find to the container.

This is why I had to put the navigation inside of each view. If it was outside of the replacement target then pjax wouldn't find it.

Summary

pjax is a really good library. It feels a lot like convention over configuration. You link pages with links, so pjax will just use those hrefs you already have to ask for the ajax version. The majority of sites use a main element for the content, so you configure that container once and pjax replaces your content in there unless you override it.

Perhaps the part I like the most about pjax is that if it fails for whatever reason (e.g. server error, JavaScript error, network error) it will fallback to the browser's default behavior which is to follow the link. This kind of error handling means that there is very little risk to using pjax in production, if fails and you are no worse off than you are now.

I'm definitely going to add pjax to my toolbox. What I might do is a blend of the default and data-pjax markups:

Screencast

Now watch the screencast below to see a walk through of the code and see how pjax works in the browser.

Topics: Ajax Javascript Pjax Ruby Sinatra

Would you like a daily tip about Shopify?

Each tip includes a way to improve your store: customer analysis, analytics, customer acquisition, CRO... plus plenty of puns and amazing alliterations.