Loading...

Pushstate and Gotchas!

What would a solution be without a few gotchas and a lot of learning along the way! Hopefully what we've learnt will help you in your SammyJS and Umbraco travels.

Pushstate

"WHAT ABOUT PUSHSTATE!?!?" I hear a few of you cry followed by: "Get with the times, we're supposed to be HTML5'ing"

Well fear not! It shall be addressed! Although this site is called #! we haven't considered staying current and getting with the times!

 

As of SammyJS 0.7 the library now natively supports pushstate with a very graceful fallback to #!. Why didn't we use it? we did! but in our travels we found a few "issues". The example we will be talking about is one that was constructed by SammyJS writer Aaron Quint. See it in action.

For those of you who want to have a play, download the source of the example we're speaking about.

 

Here are the parts of the demo we'll be talking about:

HTML

<!DOCTYPE html>
<html>
<head>
  <title>pushstate</title>

  <link href="css/screen.css" media="screen, projection" rel="stylesheet" type="text/css" />
</head>
<body>
  <div id="container">
    <div id="info" class="warning"></div>
    <h3>/ pushstate</h3>
    <p>
    <a href="/push/one">/one</a>
    |
    <a href="/push/two">/two</a>
    |
    <a href="/push/three">/three</a>
    </p>
    <h3># hashchange</h3>
    <p>
    <a href="#/push/one">#one</a>
    |
    <a href="#/push/two">#two</a>
    |
    <a href="#/push/three">#three</a>
    </p>
    <h3>unspecified (determined by browser support)</h3>
    <p>
    <a class="unspecific" href="push/one">one</a>
    |
    <a class="unspecific" href="push/two">two</a>
    |
    <a class="unspecific" href="push/three">three</a>
    </p>
    <strong>You're at</strong>
    <h3 id="at" class="success">nothing</h3>
    <ul id="log">
      <li><em>Page Load</em></li>
    </ul>
  </div>
  <script src="js/default.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

Main Sammy App

(function($) {
  Sammy.log('Loading ...');
  var app = $.sammy('#container', function() {
    this.use('Couch');

    var clicked = false;
    this.before(function() {
      var log = this.path;
      if (clicked) {
        log += "<em> clicked " + clicked.attr('href') + '</em>';
        clicked = false;
      }
      $('#log').append('<li>' + log + '</li>');
    });

    this.get(/\/$/, function() {
      $('#at').text('IN THE BEGINNING!');
    });

    this.get('/push/:push_id', function(ctx) {
      $('#at').text(this.params.push_id);
    });

    this.bind('run', function(e) {
      var ctx = this;
      $('a').click(function() {
        clicked = $(this);
        if (clicked.hasClass('unspecific')) {
          e.preventDefault();
          ctx.redirect(clicked.attr('href'));
          return false;
        }
      });

      var info = "Your browser <strong>" + navigator.userAgent + "<strong> ";
      var support = false;
      if (this.app._location_proxy.has_history) {
        info += " supports HTML5 History";
        $('#info').addClass('success').removeClass('warning');
      } else {
        info += " does not support HTML5 History";
      }
      $('#info').html(info);

      $(window).bind('popstate hashchange', function(e) {
        $('#log').append('<li><em>window event ' + e.type + '</em></li>');
      });
    });

  });

  $(function() {
    app.run();
  });

})(jQuery);

As you can see there is nothing TOO complex in there. SammyJS will deal with #! links, but will also support a scenario where the browser capabilities determine how the links are setup.

In the markup, the links that are manipulated based on browser capabilities are the links with the css class unspecific. You will notice that there is no leading / at the beginning of those links.

This would be fine for a JS enabled solution because Sammy will take care of it. When the app loads, if the browser supports pushstate, Sammy prepends a leading /. If the browser does not support pushstate then Sammy will prepend /#!/ and the routes will JUST WORK! To me that is impressive and elegant and I really applaud Aaron for being so considered in planning the functionality this way as we all know cross-browser issues haunt us all.

Pushstate GOTCHA (Kind of!?)

The primary focus of our solution was to ensure that Google would be able to effectively navigate and index our site. The current pushstate functionality will ONLY be implemented if we leave the leading / or leading #! off our links. This means that our markup for our navigation menu would look something like so:

<div id="logo">
    <a href=""><img src="/media/logo.png" width="70" height="70" alt="logo" /></a>
</div>
<nav id="main-menu">
<ul>
    <li class="the-umbraco-approach first">
        <a href="the-umbraco-approach/" class="the-umbraco-approach">The Umbraco Approach</a>
    </li>
    <li class="sammyjs">
        <a href="sammyjs/" class="sammyjs">SammyJS</a>
    </li>
    <li class="pushstate-and-gotchas">
        <a href="pushstate-and-gotchas/" class="pushstate-and-gotchas">Pushstate and Gotchas</a>
    </li>
    <li class="owls">
        <a href="owls/" class="owls">Owls</a>
    <ul>
        <li class="barn-owl first">
      <a href="owls/barn-owl/" class="barn-owl">Barn Owl</a>
      </li>
      <li class="great-horned-owl last">
         <a href="owls/great-horned-owl/" class="great-horned-owl">Great Horned Owl</a>
            </li>
        </ul>
        </li>
        <li class="robots last">
            <a href="robots/" class="robots">Robots</a>
        </li>
    </ul>
</nav>

What this would mean is that the menu would navigate incorrectly as we work through the site and raises concerns like what do we do with links like our homepage link to <a href="">?

Super Awesome Fallback Solution AKA Get Around the Gotcha

Fixing this problem is actually pretty simple (once you think of it post morning coffee). What we needed to do was prepend #! when required! ENTER MODERNIZR! sure you could have just used some standard checks but we were using modernizr anyway so why not!

THE FIX

Remember way back when on the SammyJS page?

var hashPrefix = (Modernizr.history) ? '' : '/#!';

So what we're doing before we tell Sammy to initialise is prepend #! to our main links on the site if history is supported.

Oh Android! - The little robot too far ahead of its' time

Unfortunately SammyJS's native solution AND our solution suffers from Android's super excited browser!

Google's stock browser jumps the gun a little when it comes to letting us know it supports pushstate

Unfortunately for us IT LIES! While it might say that it supports pushstate it does not. This means that the SammyJS plugin doesn't fallback and our history detect returns an incorrect value.

The fix

We haven't fixed site site due to time constraints but our proposed solution would be:

  1. Use user agent sniffing to detect the android browser/version and alter our hashPrefix javascript
  2. Force SammyJS to disable pushstate (thanks Aarron for letting us know!) by setting disable_push_state = true; This will force sammy to only use hashchange logic

Oh IE7 - Dealing with the HREF Attribute for Dynamic Content

What would a demo be complete without an IE hack? Funnily enough we came across this one when implementing SammyJS and this method for another demo.

This bug came up when trying to use jQuery's attr('href') on an element that had been loaded in via $.get or $.load or the response of $.ajax.

Say for example we used $.get to load in a file called test.html:

<!DOCTYPE html>
<html>
<head>
  <title>pushstate</title>

  <link href="css/screen.css" media="screen, projection" rel="stylesheet" type="text/css" />
</head>
<body>
  <div id="container">
	<a href="/test-link.html" id="mr-link">Test Link</a>
  </div> 
</body>
</html>

If we try and do something like:

$.get(p, function (response, status, xhr) {
        if (status == "error") {
            var msg = "Sorry but there was an error: ";
            contentBody.html(msg + xhr.status + " " + xhr.statusText);
        } else {
            var pageHtml = $(response);
			var linkUrl = pageHtml.find('a#mr-link').attr('href');
		}
});

The value of linkUrl will actually be http://www.domainname.com/test-link.html in IE7. So when we go to prepend #! or / (if we're dealing with pushstate) the domain is incorrectly included in the value resulting in http://www.domainname.com/#!/http://www.domainname.com/test-link.html

To get around this we can use some regex to pull out the url part after the domain in the address and then prepend our chosen characters:

var urlParts = /^(https?:\/\/[^\/]+)?(\/[^\?]+)(\?.*)?$/.exec($(this).attr("href"));
var hashBangLink = "#!" + urlParts[2];
$(this).attr("href", hashBangLink);

This demo is powered by

blog comments powered by Disqus