Making Framework Agnostic Isomorphic Web Applications with Query Strings and HTML5 pushState

December 11th, 2016 by zoltan · No Comments

With frameworks like React in vogue today, there is a lot of HTML being rendered exclusively on the client instead of the server, where it has traditionally occured. While this can result in some really snappy and slick interactions, there are a few issues with doing this:

  1. They do not work when the client has JavaScript turned off.
  2. In low bandwidth environments, performance will be sluggish at load time if the application is JavaScript heavy, since (usually) the majority of the JavaScript needs to load before the page state can be applied to the application. This is a huge consideration with mobile wireless connections, where you cannot guarantee a great signal all the time.
  3. Sharing that state of a “single page application” via social media/email/instant messaging platforms can be problematic unless the URL can change when the state changes.

To handle the first two issues, the first-render of the application should be done by the server instead of the client. This will allow users to see the page data immediately while the page loads all the assets, increasing the performance of the page at load time. Also, as the state of the page changes, the URL should also change. If the user wants to share that change, then it’s easy as sharing that URL. And since the first-render of that page will be done by the server, you can program the server side code to have the application give appropriate Facebook Open Graph and Twitter Card meta tag information on first load that reflects that change to social media sites to make sharing nicer.

When HTML5 single page applications first became popular a while back, we hit a snag — we couldn’t change the URL of the application without causing a browser refresh. In order to work around this, many JavaScript routing frameworks (like Backbone) would change the URL hash of the page in order to maintain state. This worked, but at a cost of sharing the state — social media sites would not be able to know the state of the application since the URL hash is never sent to the server via HTTP. Furthermore, Social Media platforms like Facebook cannot use Fragment Identifiers to distinguish different states of an application, which causes issues if you want Facebook users to be able to “Like” specific states of an application (e.g. the photo-gallery section of the application vs. other parts of the page).

Now, however, we can use HTML5 Session Management to change the browser’s URL within a single page web application without the browser going to the server to load the page for that URL. Furthermore, when the user clicks the back button, this Session Management API can be used to ensure that state of the page for that URL can also be rendered without a full server page load. I took this a step further and wrote progressive-pushstate, a JavaScript library which parses the data from a query string and sets this as the application’s history.state (the JavaScript object that HTML5 Session Management uses to keep state). Links and forms on the page that change the document’s query string can be rendered by JavaScript the same way they would be rendered if that URL and query string were requested by the web server. This also allows us to change the query string if form elements are changed without hitting the submit button.

Go to the progressive-pushstate GitHub page

If you are confused about the power of this, let’s look at some simple examples.

Example 1: A Simple Mobile/Desktop Navigation System

Let’s say you have to build a website that uses a <select> box “hamburger” menu in the mobile breakpoint, but a traditional “list of links” menu for the larger breakpoints:

This table contains screenshots of a web page that uses a select-box hamburger menu in the mobile breakpoint, and a traditional nav bar for larger breakpoints
Mobile BreakpointDesktop Breakpoint
screenshot of desktop version of video game demo screenshot of desktop version of video game demo

Look at live version of the the hybrid nav solution using progressive-pushstate

Let’s look briefly at the markup that makes this happen:

<nav class="fixedsticky">
    
  <!-- The CSS makes this visible only when the page is wider than 768px -->
  <ul>
    <li><a class="pp-link" href="?f=home">Home</a></li>
    <li><a class="pp-link" href="?f=donkey-kong">Donkey Kong</a></li>
    <li><a class="pp-link" href="?f=pac-man">Pac-Man</a></li>
    <li><a class="pp-link" href="?f=robotron">Robotron</a></li>
    <li><a class="pp-link" href="?f=tempest">Tempest</a></li>
  </ul>

  <!-- The CSS makes this visible only when the page is narrower than 768px -->
  <form class="pp-form" data-pp-events="change">
    <select name="f">
      <option value="home" selected>Home</option>
      <option value="donkey-kong">Donkey Kong</option>
      <option value="pac-man">Pac-Man</option>
      <option value="robotron">Robotron</option>
      <option value="tempest">Tempest</option>
    </select>

  </form>
</nav>

<script src="/path/to/progressive-pushstate.js"></script>  
<!-- not needed for progressive-pushstate, but used in page code -->
<script src="/path/to/jquery-3.1.1.min.js"></script>
<script src="/path/to/example02.js"></script>

If you take a look at the navigation markup, you will see that the links will submit a query string that would be identical if the form were to submit. Since the links have a class of pp-link, progressive-pushstate will know to override the browser’s default behaviour and handle the links. Since the form has a class of pp-form and has data-pp-events set to "change", progressive-pushstate knows that it must fire when any of its fields change.

So, how does progressive-pushstate handle the link behaviour? In example02.js, you will see the following code:

var example1 = new function () {
  
  // Use me to avoid using .bind() all over the place.
  var me = this;
  
  me.init = function () {
    pp.init(me.popstateEvent);
  };
  
  me.popstateEvent = function(e) {
    currentState = e.state;
    
    /*
     * At this point, `currentState.f` will be set to whatever value `f` is in
     * the query string.
     */
    
    .
    .
    .
    // rest of the logic is here.
  }
}

example1.init();

The init() method calls pp.init(me.popstateEvent), which initializes progressive-pushstate so that pp-link links and pp-form forms will be handled by the script. When these links and forms are used, progressive-pushstate will convert the resultant query string to a JavaScript object and set it as the pushState for that URL. It will then call example01.js’s popstateEvent(), which will change the contents of the main part of the page by grabbing an HTML fragment that corresponds to that page (for the sake of brevity, this logic is not shown in this article, but if you want, please take a look at the full source of example01.js to understand how it works). Note that the `popstateEvent()` method uses e.state to find out the state of the page … this is the pushState object we mentioned earlier.

There is one thing I did gloss over here — if JavaScript is turned off (or if the browser doesn’t support progressive-pushstate) the mobile navigation will fail to do anything when the select box value changes. Since we want things to be bullet proof, we add this to the the mobile menu:

<nav class="fixedsticky">
  
    <!-- The CSS makes this visible only when the page is wider than 768px -->
    <ul>
    <li><a class="pp-link" href="?f=home">Home</a></li>
    <li><a class="pp-link" href="?f=donkey-kong">Donkey Kong</a></li>
    <li><a class="pp-link" href="?f=pac-man">Pac-Man</a></li>
    <li><a class="pp-link" href="?f=robotron">Robotron</a></li>
    <li><a class="pp-link" href="?f=tempest">Tempest</a></li>
  </ul>
  
  <!-- The CSS makes this visible only when the page is narrower than 768px -->
  <form class="pp-form" data-pp-events="change">
    <select name="f">
      <option value="home" selected>Home</option>
      <option value="donkey-kong">Donkey Kong</option>
      <option value="pac-man">Pac-Man</option>
      <option value="robotron">Robotron</option>
      <option value="tempest">Tempest</option>
    </select>
    
<!-- * This button only appears when the library is not supported by the * browser, or if JavaScript is turned off --> <input class="pp-no-support-button" type="submit" aria-label="Go to page selected" value="Go" />
</form> </nav>

Note the submit button with the className pp-no-support-button. We want users to press this button if the browser doesn’t support progressive-pushstate. This will allow the page to submit the form data the old fashioned way so that the page can generate the right HTML on the server. If you look at the code for example01.php, you will see PHP code that does this (this PHP code also does the first render of the page).

We use the following CSS to show this button only when the browser can’t support progressive-pushstate (or when JavaScript is turned off):

.pp-no-support-button {
	display: inline-block;
}

.pp-support .pp-no-support-button {
	display: none;
}

The classes pp-support is set on the <html> tag by progressive-pushstate when the page loads if the browser is supported by the library — if not, the library sets the pp-no-support class instead.

These classes allow developers to give an alternative UI if the library is no supported. These fallback features that give an expected (but not as sexy) user experience to older and JavaScript-disabled browsers are a great example of progressive enhancement, which is why I called this library progressive-pushstate.js

If you use the navigation functionality in the above example to go to several pages, try hitting the back button. You will notice it remembers history perfectly. This is because pressing the browser history buttons (i.e. back and forward) will also invoke the popstateEvent as well. History management happens for free!

Let’s take a look at another example.

Example 2: A “Search As You Type” Page

Many Search Engines (including Google) will show results while the user types in a form. Let’s look at an example that uses progressive-pushstate:

Screen capture of search example

This basic search form will go to the server while the user is typing in a search term.

Look at live version of the search example.

You will note that the form will do a search while the user types a search string into it. This is because we instructed progressive-pushstate to do a search oninput:

<form class="pp-form" data-pp-events="input submit">
  <label for="country">
    Country: 
    <input
      autofocus
      type="text"
      id="country"
      name="country"
      placeholder="Please enter in a country."
      autocomplete="off" 
      value=""/>
    </label>
</form>

The library automatically throttles these submissions in order to prevent really fast typers that type 200 characters a minute from killing the web server with 200 requests a minute. Note also that history is again baked in, so if users make mistakes or want to see previous searches, they can just press the back button. Finally, the fallback is simple for JavaScript disabled browsers or browsers that don’t support this library — users can just hit enter to do a search. This will cause a page refresh, but that’s okay, since the page still works as intended.

Example 3: A Filtered Table

Since I am currently working on an accessibility project with my current client, I wanted to build a page with a table that listed all the Web Content Accessibility Guidelines with a way to filter them by principle (Perceivable, Operable, Understandable and Robust) as well as level (A, AA and AAA).

A screen shot of the table filtering example.

A screen shot of the table filtering example.

Look at live version of filtered table example.

Again, with a modern browser with Javascript turned on, you will see a nice animation when new rows are added to the table. When you refresh the page, the table with the proper filters will be generated on the server side.

Note the submit button at the end of the form. Unlike the previous examples, the content only changes when we submit. This is to ensure that this page conforms to the WCAG 2.0 (specifically, 3.2.2 – On Input (Level A). We force progressive-pushstate to only allow state changes when the form submits. This is done by setting the form’s data-pp-events to "submit"

<form class="pp-form" autocomplete="off" data-pp-events="submit">
  .
  .
  .
</form>

If you are using progressive-pushstate to create an accessible website that complies with this international standard, you must do this if you are providing a submit button to initiate a change of context. Note that change of content is not always a change of context.

Pitfalls and Solutions to Isomorphic Applications

When coding isomorphic applications, developers should ensure that the client and the server both display the same data for each URL. For example, the AJAX requests used in examples #1 and #2 return HTML. This is the same HTML that would be inserted into the page by the server for that page’s state. We do this by using PHP’s include to call the same PHP code that the AJAX request calls.

The method was used in these examples for simplicity sake — however, most single page web applications that do AJAX requests return JSON that is parsed by the front-end JavaScript and translated to HTML. If you want the same JSON payload to be parsed and translated to HTML on the server, you’d have to port that same code into whatever server side language you are using or run the same JavaScript you run on the client on the server as well. Running JavaScript on the server is the preferable way to go since you only have to code your presentation logic once.

Running server-side JavaScript is possible using Node, but what if you have an existing site that runs a different server-side technology? Don’t fret! Many of them can still run JavaScript. For example:

In short, there doesn’t seem to be a shortage of solutions of running JavaScript on a server if you can’t use Node.

Do I Have To Use progressive-pushstate To Make My Applications Isomorphic?

Not at all. Here are some other solutions excellent solutions (just to name a few):

You will notice that all of these solutions (and a lot of others) rely on you using a specific framework. This is why I created progressive-pushstate — I wanted something more generic, since it is not always possible to use these frameworks in every project (e.g. when you are interfacing with an existing package, when you are modifying an existing codebase, etc). Also, I have found JavaScript packages are like opinions — everyone has one and even the most popular ones tend to change or disappear over time.

Other Features of progressive-pushstate

There are more features to progressive-pushstate than shown in these examples. A full breakdown is available on the progressive-pushstate github page.

Planned Upcoming Features of progressive-pushstate

  • One thing I would think would be great is if instead of using standard query strings (e.g. http://domain.com/page?a=x&b=y&c=z) developers could use directories slugs instead, like Expression Engine does (e.g. http://domain.com/page/a/x/b/y/c/z). I did not do this in the first release because the work I do usually involves forms, and using query strings is the default way forms send information to the browser, so it just made sense to do it that way.
  • Another feature I would love to add is to ensure we can progressive-pushstate’s data into a separate property in the pushState. That is to say, instead of having http://domain.com/page?a=x&b=y&c=z resulting in a pushState of this:
    {
      a: 'x',
      b: 'y',
      c: 'z'
    }
    

    it can result of a pushState of this:

    {
      pp: {
        a: 'x',
        b: 'y',
        c: 'z'
      }
    }
    

    The advantage for doing this, in my opinion, is to allow progressive-pushstate to play nice with other JavaScript libraries that work with the HTML5 Session API.

I definitely want to add both of these features in a not-so-distant future release when I get some extra time. If anyone else would like to work on these or any other improvement, please feel free to send me a pull request. Alternatively, if you don’t have the time to contribute would like to request a different features for this library, please feel to leave a comment below or on the issue page for progressive-pushstate.

Browser Support for progressive-pushstate

It works in all modern browsers (including many of the older browsers in current use, like IE10+ and Android Browser 4.3+) and if you ensure the server side can do the render all the states without JavaScript, the fallback for unsupported browsers (like Opera Mini or IE <= 9) is to let the server render the page. Go to the progressive-pushstate GitHub page

Tags: Events · Events · forms · HTML5 · pushState · , , , , , , , , ,

0 responses so far ↓
Give Feedback

Don't be shy! Give feedback and join the discussion.

Please Note: If you are asking for help using the information on this page or if you are reporting a bug with the code featured here, please include a URL that shows the problem you are experiencing along with the browser/version number/operating system combination where the issue manifests itself. Without this information, I may not be able to respond.

An orange star denotes a required field.