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).

<!DOCTYPE html>
<!--[if lt IE 7]>      <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]>         <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]>         <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <title><%= title %></title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width">
 
        <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
 
        <link rel="stylesheet" href="css/normalize.css">
        <link rel="stylesheet" href="css/main.css">
        <link rel="stylesheet" href="css/app.css">
    </head>
    <body>
        <!-- Add your site or application content here -->
        <section id="main">
 
          <section id="content">
            <%= yield %>
          </section>
 
          <h2>Headers</h2>
          < id="headers"><%= headers.inspect %>>
          <h2>Status</h2>
          < id="status">
            <%= Time.now.to_s %>
            Randomized: <%= rand(1000) %>
          >
        </section>
 
        <!-- JS -->
        <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>
    </body>
</html>
<%# index.erb %>
<nav>
  <a href="/"> Welcome</a>
  <a href="/goodbye">Goodbye</a>
</nav>
 
<h1>Application title</h1>
 
<p>Welcome to the application, have a nice day.</p>
<%# goodbye.erb %>
<nav>
  <a href="/"> Welcome</a>
  <a href="/goodbye">Goodbye</a>
</nav>
 
<h1>Sorry to see you go</h1>
 
<p>Goodbye</p>

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:

  • you can skip layout rendering and its associated logic, and
  • the response HTML should be smaller which means less network usage and download time

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:

<section id="content">
  <div id="magic">
    <a href="#">Naked link</a>
    <a href="#" class="js-pjax">Class link</a>
    <a href="#" id="id">Id link</a>
    <p>Some content here with an <a href="#">internal Naked link</a></p>
  </div>
</section>
  • To pjax everything: $("#content").pjax('a');
  • To pjax just the class link: $("#content").pjax('.js-pjax');
  • To pjax just the id link: $("#content").pjax('#id');
  • To pjax all of the link except for the internal naked link: $("#content").pjax('#magic > a');
  • To pjax just the two naked links: $("#content").pjax('a:contains(Naked)');
  • etc, etc, you get the point

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.

<nav>
  <a href="/"> Welcome</a>
  <a href="/goodbye">Goodbye</a>
</nav>
 
<section id="content">
  <div id="magic">
    <a href="#">Naked link</a>
    <a href="#" class="js-pjax">Class link</a>
    <a href="#" id="id">Id link</a>
    <p>Some content here with an <a href="#">internal Naked link</a></p>
  </div>
</section>

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:

  • Use the default pjax markup for links inside a container: $('#content').pjax('a'); and then
  • Use the data-pjax markup for links outside of a container like a global navigation: $(document).pjax('a[data-pjax]') and set the data-pjax attribute to “#content”

Screencast

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

(View it in full screen on YouTube)

3 comments

  1. Markus says:

    It’s usually common that the navigation is not part of the article. So, clicking the navigation should load a new article AND update the navigation. So what’s the solution here now? Make two pjax requests, one for navigation, one for article? Make one pjax request which contains two HTML parts one for navigation, one for article? Make one pjax request only for article and simulate changing the navigation (highlighting, new submenus, etc.) in JavaScript (which essentially would duplicate logic which is already present server side somewhere)?

    And navigation and article is still just a simplification. Usually you have header which isn’t changing, probably part of the header the navigation which may change, you may have a separate sub-navigation related to article for group of articles, you you have a sidebar somewhere, then other stuff like social bar, and then your footer.

    And then you also have web tracking stuff. If you just use pjax and not think about web-tracking stuff, you’re getting wrong metrics. One has to really put much thought into it I gues.

    • edavis10 says:

      Great points. It all depends on how your application layout is.

      For example on Chirk HR, the main navigation is static and doesn’t change from request to request. There is a breadcrumb navigation but it’s inside of the main body of the page so updating it with pjax would work.

      On the other hand, if every section of your site changes from request to request then you would need to pick an option like you said (e.g. full request, multiple pjax requests, JavaScript updates).

      It all comes down to knowing your application and what would work best. The nice thing about pjax is that you can add it without much server side work and then remove it if your needs changes.

      Thanks for the comment.

Comments are closed.