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.
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.
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 (
<%# 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).
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.
- 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
- 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
- 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')
<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
- 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.
- you can override a link's target by using the data-pjax attribute.
So really, all you need is:
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.
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>
- To pjax everything:
- To pjax just the class link:
- To pjax just the id link:
- To pjax all of the link except for the internal naked link:
$("#content").pjax('#magic > a');
- To pjax just the two naked links:
- 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.
<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.
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.
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:
- 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"
Now watch the screencast below to see a walk through of the code and see how pjax works in the browser.