Tag Archives: historyjs

Part 5: SPAs, Accessibility and the Server side

Part 1: Dipping your feet into KnockoutJS Components
Part 2: Knockout Components – Separating Templates from View Model
Part 3: KO Components – Routes and Hashes
Part 4: SPA with KO Components-PushState and History JS

Recap

In the previous 4 parts we have seen how to use Knockout Components to make a sample Single Page Application, with different type of client routing libraries.

Towards the end of the third article I had highlighted how SPAs hinder Accessibility and Search-ability of a site’s content primarily because:

a. Search engines don’t ‘click’ on URLs to load content, they simply follow URLs they find and request the server for content. Whatever the server returns is indexed as content of that URL.

b. Accessibility tools work on similar logic to Search engines and cannot ‘click’ on URLs to load content.

Towards both these ends we took a small first step by moving from a hash based client side router to a pushState based one that falls back gracefully for non-push state browsers. This gave us URLs that looked like /settings instead of #settings. The search engine or accessibility tool now knows it has to navigate to a new URL to get the content.

At the end of the last article I has shown how the server would return a 404 or some similar server-side error because when we click on http://buildingspausingko.azurewebsites.net/settings there is no page at that URL. Remember our entire site is being served from index.html. How to we fix this?

Enter server side routing fallbacks

Before we start let’s get the Part 4 code from Github.

Using _Layout and adding an Index.cshtml for the home page

Before anything, let’s start using the server in the first place.

Our Application was an Empty ASP.NET application to start with. So to get started we’ll first add ASP.NET MVC to it. We can do this by installing the package Microsoft.AspNet.Mvc

PM> install-package Microsoft.AspNet.Mvc

Now let’s add Controller/HomeController and a Views/Home/Index.cshtml files.

We update the Layout such that it contains the top navigation bar and then calls the RenderBody function to render the rest of the page.

<!DOCTYPE html>
<html xmlns=”
http://www.w3.org/1999/xhtml”>
<head>
<title>Dipping your feet into KnockoutJS Components</title>
<link href=”/Content/bootstrap.css” rel=”stylesheet” />
<link href=”/Content/bootstrap-theme.css” rel=”stylesheet” />
</head>
<body>
<div class=”navbar navbar-inverse navbar-fixed-top”>
<div class=”container”>
<div class=”navbar-header”>
<button type=”button” class=”navbar-toggle” data-toggle=”collapse” data-target=”.navbar-collapse”>
<span class=”icon-bar”></span>
<span class=”icon-bar”></span>
<span class=”icon-bar”></span>
</button>
<a class=”navbar-brand” href=”/”>KO Components</a>
<ul class=”nav navbar-nav”>
<li>
<a href=”/”>Home</a>
</li>
<li>
<a href=”settings”>Settings</a>
</li>
</ul>
</div>
</div>
</div>
<div id=”page1″ class=”container” style=”padding-top:50px”>
@RenderBody()
</div>
<footer class=”navbar navbar-fixed-bottom”>
<div class=”container-fluid”>
<p> &copy; 2014 – Still Learning</p>
</div>
</footer>
<script src=”App/boot/require.config.js”></script>
<script data-main=”App/boot/startup” src=”Scripts/require/require.js”></script>
</body>
</html>

We put in a simple header in the index.cshtml file showing that it is coming from the server. However not that the div containing the Index content is also the container for the dynamic component which will be loaded by Require JS.

@{
Layout = “~/Views/Shared/_Layout.cshtml”;
}
<div id=”page1″ class=”container” style=”padding-top:50px;” data-bind=”component: { name: route().page, params: route }”>
<div class=”container” style=”background-color: red”>
<h1>Index from Server</h1>
</div>
</div>

Finally, let’s rename the Index.html to Index2.html so that it’s not picked up as the default by IIS.

If you run the application now you’ll see the ‘Index from Server’ text flashes briefly before being replaced by the greetings.

This simply implies that there was a page that the server rendered and returned, once the page was loaded the client side routing kicked in and replaced the container.

This was the default page.

In the browser change URL by adding settings you’ll get an error as follows:

image

This is because there is an invalid redirection in my web.config. Open the web.config and delete the highlighted section

image

Now if you type in the URL /settings on the browser you’ll get a proper 404

image

You must be wondering how come when I click on Settings link from Home page I get a proper page, but when I type in the URL I get a 404? Well, that’s because typing in a URL sends the request to the server directly. In our case, it sends to ASP.NET MVC. MVC framework uses the current route info and tries to map it to a ‘SettingsController’ with a default ‘Index’ action method. However it doesn’t find the SettingsController and throws a 404.

This is what will happen to Search Engines and Screen readers as well. Even though they will see a URL, trying to browse to it will get them a 404. Search engines will down-rate your site and screen readers will give a poor experience to people using them.

So how do we solve this? Let’s explore two ways today.

Sending the same page back for all URLs from Server

This is a hackish technique to make sure your users can navigate to the correct page from bookmarked URLs. This doesn’t really help in Search engine optimization or screen readers. But atleast no one gets a 404 when a valid URL is accessed directly from the browser.

For this, all we have to do is, manipulate the MVC router to send all requests to HomeController’s Index action method.

Once the Index page is returned the client side router will kick in and do the ‘right thing’.

In the App_Start\RouteConfig.cs update the RegisterRoutes method as follows:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute(“{resource}.axd/{*pathInfo}”);
routes.MapRoute(
name: “Default”,
url: “{*pathinfo}”,
defaults: new { Controller=”Home”, action = “Index”, id = UrlParameter.Optional }
);
}

As you can see above, we have changed the url parameter to be a wildcard {*pathinfo}, and set the default for this wildcard path to Home Controllers’ Index action method.

If you run the application now, and navigate to the Settings page by typing in the URL you’ll see that there is no more 404 error. BUT, the client side routing seems to be taking us back to the Index page. Why?

image

Removing hard coded default path in router.js

Well, in our ‘excitement’ to get History.js going with crossroads.js in the last article, we introduced a small hardcoding that’s causing the about “BUG”.

In Router.js’ activateCrossRoads function the last line instructs crossroads to route to the root location (‘/’).

crossroads.parse(‘/’);

This is why even though the browser is pointing to settings page, the page is showing the greetings from the Index page.

Let’s fix this.

The inline function call that’s being bound to the stateChanged event actually needs to be called on first Initialization as well. So let’s refactor it out to a separate function called routeCrossroads.

function routeCrossRoads()
{
var State = History.getState();
if (State.data.urlPath) {
return crossroads.parse(State.data.urlPath);
}
else {
if (State.hash.length > 1) {
var fullHash = State.hash;
var quesPos = fullHash.indexOf(‘?’);
if (quesPos > 0) {
var hashPath = fullHash.slice(0, quesPos);
return crossroads.parse(hashPath);
}
else {
return crossroads.parse(fullHash);
}
}
else {
return crossroads.parse(‘/’);
}
}
}

I have made some changes to the code in the else section. Actually the condition to check for ‘?’ is there to handle IE9 properly. In case of IE9, History appends a state Id to the URL after ‘?’. Crossroads doesn’t need the state ID hence I strip it out. But the initial condition is incomplete because if there are no ? in the URL we need the entire path (fullHash). I have fixed this in the above code.

Once the above function is in place, the activateCrossroads function becomes as follows:

function activateCrossroads() {
History.Adapter.bind(window, “statechange”,
routeCrossRoads);
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
//crossroads.parse(‘/’);
routeCrossRoads();
}

As we can see we have commented out the default routing to the root ‘/’.

With everything in place if we run the app now, and try to type if /settings at the end of the URL we’ll get the correct settings page.

image

But again, before the Settings page is loaded you will briefly see the red banner saying ‘Index from Server’ flash briefly before navigating to the ‘’Settings’ page. This is because for all URLs we are returning the same Index page from the Server. How do we fix this, if the answer wasn’t obvious, read on Smile

Technique 2: Sending a Server Side page for every URL

Now that our SPA is capable of responding to all URLs specified in our web app, let’s dig deeper to see how we can respond to all pages equally.

This section is experimental on my part, if there are better ways to do this, please sound off in the comments section.

Updating the MVC routing

First thing to do is to update the Routetable to add independent routes for Home/Index and (say) Settings/Index pages.

We’ll keep the ‘catch all’ route that we defined earlier so that if there are any routes that are not defined on the server but defined on the client, we send them off to the home page to load the old fashioned way.

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute(“{resource}.axd/{*pathInfo}”);
    routes.MapRoute(
name: “Default”,
url: “{Controller}/{action}/{id}”,
defaults: new { action = “Index”, id = UrlParameter.Optional }
);

routes.MapRoute(
name: “
CatchAll“,
url: “{*pathinfo}”,
defaults: new { Controller=”Home”, action = “Index”, id = UrlParameter.Optional }
);
}

With the above routing in place, lets add a new controller called SettingsController and a new cshtml at Views/Settings/Index.cshtml

The Settings Index file again is a simple one saying it’s coming from server! It uses the _Layout.cshtml file for the header.

@{
Layout = “~/Views/Shared/_Layout.cshtml”;
}
<div id=”page1″ class=”container” style=”padding-top:50px” data-bind=”component: { name: route().page, params: route }”>
<div class=”container”>
<h1>Settings from Server</h1>
</div>
</div>

Now when we run the application and type in /settings we’ll get the server page first and then the client page will get loaded, so you’ll see a brief flash saying ‘Settings from Server’.

In real world the duplication of cshtml and html templates may be a significant effort, I am looking at making things easier in that front and will blog about it if I find anything better. As of now, if you want the best user experience for all types of users (as well as the search engine), this is an extra bit of work that you have to do. It’s not as bad as it sounds either, all you have to do is use the same service that you call during the HTTP GET operation from client, and bind that data to the CSHTML. You can skip elaborate styling and limit it to having correct markup laid out in an orderly fashion.

Code

As usual the code for this article is up on Github. I have branched this code off as Part5: https://github.com/sumitkm/BuildingSpaUsingKO/tree/Part5

Conclusion

With that we conclude this part of the series where we saw how we can handle bookmarked URLs on the server and client. This goes a long way in helping users who use accessibility tools use your site effectively.

More fine tuning will be required on the client side router to handle all scenarios but that’s for another day.

Next we’ll see how to handle URL parameters and more involved routing like Tabs.

Advertisements
Tagged , , , , ,

Part 4: SPA with KO Components-PushState and History JS

In the previous three parts we have seen:

At the end of the third article we saw some problems with hash based routing primarily in terms of accessibility and search engine indexing. Also the fact that hashed URLs are semantically incorrect gives us enough reason to look for alternatives.

Today we’ll explore how we can overcome some of these problems and discover some new ones Winking smile.

HTML5 and manipulating browser history

HTML5 pushState usually refers to a set of APIs that HTML5 browsers can support, to give more control to the developers on the Browser URL and the Browser’s back button behavior. These include pushState, replaceState and the event popState. Here is a nice primer on pushState on developer.mozilla.org.

Essentially, when you want to add a URL to the history you push the URL into browser history using pushState. This is done for free when you click on a link (or type in the URL) and a server returns the page. However, if you wanted to build an AJAX based navigation system where you want to intercept all link clicks and show content dynamically loaded via AJAX, the browser doesn’t know the URL’s correlation to the content. So you have to tell it explicitly what the URL is (e.g. when we are showing the ‘Settings’ page, the URL should be /settings). HTML 5 pushState and related APIs make this easier for us.

Ajax based apps also have had trouble with the browser back button. As users we are trained to hit the big honking ‘Back’ button on the browser when we want to go back to the previous page. This is correct expectation and our Web Apps should respect that. Doing AJAX based page/component loading can break this because you may have content loaded based on user interaction that is not recoverable when user hits the back button.

So ideally we should use the new found ‘pushState’ functionality to indicate on the URL the exact location of the current content that is being shown. Thus when a user hits the back button we can use the URL to load up the previous content again. Back button in HTML5 browsers fire a popState event that can be handled for these purposes.

Doing all this manually is a bit of boilerplate. In Part 3 we saw how crossroads+hasher does it (Back button is fully supported for # URLs. Today we’ll see how we can use another library called History.js. Reason for exploring History.js is that it supports the new pushState API and falls back on Hash-based routing for browsers that don’t support pushState, (one such browser, is IE9 and when I started, one of my ‘undeclared’ goals was to support IE9 and up).

Replacing Hasher.js with History.js in our sample

You can get the last version of my sample from the Part 3 branch on Github.

You can clone the History.js repo from Github here.

Your cloned folder should look like this:

image

The Scripts folder has the following the sub-folders. Mechanics behind these folder is actually pretty neat.

image

History JS Components and Dependencies

History JS depends on availability of the JSON object so for older browsers that don’t have JSON parsing built in, it refers to JSON2.js. It bootstraps itself differently based on what library you are using. So there are various adapters that bootstraps History JS correctly with the respective library.

image

The bundled* folder contains one file per-adapter. Each file contains JSON2.js, the respective Adapter and the entire HistoryJS library. So all you have to do is pick the file from either the bundled-uncompressed\ folder or if you want to use the minified version use the same file from the bundled\ folder. Note there are two subfolders under bundled\ folder – html4+html5 and html5. If you don’t want to support HTML4 the library is trimmed a little further, but we want it, so we’ll dive into the html4+html5 folder.

If you want to experiment with adapters you can pick the adapter and the History.JS file from the compressed or uncompressed folders.

In my sample I’ve picked the native.history.js file from the bundled-uncompressed\html4+html5 folder.

Adding History JS and loading via RequireJS

  1. Since it’s a base library we’ll put it in it’s own folder under Scripts

image

2. Next we update the App\boot\require.config.js to load HistoryJS after bootstrap is loaded and remove the Hasher.js dependency.

image

3. Modifying our Router: We will need multiple changes in our Router.js file to configure it to use History.js instead of Hasher.js.

a. We start by adding historyjs dependency in the module definition

image

b. Handling anchor clicks to prevent server requests directly: This is actually a big deal. Typically we navigate to a different page by specifying an Anchor tag that has a href pointing to the new page. It may be a full path or partial path. When user clicks on an anchor the request is sent to the server by the browser. When using Hasher we worked around this by using href’s that started with a #. This told the browser that the reference was in the current page itself and then we hooked into the event and did our AJAX magic to load a new page. This also had the side effect of putting a # in the URL irrespective of the browser.

To avoid # in the hrefs, we need to handle all href clicks globally and push the path to HistoryJS for managing and raising statechange events.

To do this we assign a click handler for all URLs inside the HTML body tag. We check if the href attribute starts with a #, it means it’s a deliberate reference to some place in the current page and we let the event pass through. If the href doesn’t start with a # we push the path to History using the pushState function. Note we are not using window.pushstate because we want History to manage the pushState for older browser that don’t have a native implementation.

image

c. Next we update the activateCrossroads() function to intercept HistoryJS’ statechange event and pass the new path to crossroads:

function activateCrossroads() {
History = window.History; // # 1
History.Adapter.bind(window, “statechange”, function () { // # 2
        var State = History.getState();
if (State.data.urlPath) { // # 3
return crossroads.parse(State.data.urlPath);
}
else
{
if (State.hash.length > 1) { // # 4
                var fullHash = State.hash;
var hashPath = fullHash.slice(0, fullHash.indexOf(‘?’));
return crossroads.parse(hashPath);
}
}
});
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
crossroads.parse(‘/’); // #5
}

#1: Gets instance of the HistoryJS. Next we bind a event handler for the “statechange” event.
#2: The event handler is fired every time a new URL is pushed using pushState. In a browser this happens automatically when we click on an anchor tag. But clicking on an anchor tag that doesn’t start with a hash # results in a get from the server. We want to control this
#3: If the State.data has a valid urlPath we extract the urlPath from state provided by HistoryJS and pass it to crossroads to navigate to. However if users bookmark the path, then the urlPath comes in null. The next bit of code handles this
#4: If the urlPath is undefined, then we check if the State’s hash length is > 1. This is an indicator of a bookmarked URL and we slice out the path that crossroads needs and let crossroads navigate to it.
#5: This is invoked on activation and by default our router navigates to the home page.

Testing it out

Well, we have made all changes we need so it’s a good time to test it out. I have deployed it to Azure websites via Github deploy, so as soon as I commit to the master branch it updates the http://buildingspausingko.azurewebsites.net/ (which unfortunately is down due to unscheduled Azure maintenance at the moment, so instead of my VM accessing the site, we’ll ‘fake it’ using IE11 DOM manipulation).

On IE11 the site navigates cleanly

imageimage

I emulate IE9 DOM on IE11 and I get the rather ugly hash based URL.

image

Well, old browser users have to live with some ugly stuff unfortunately. But here is the good part, if users bookmark that URL, History JS will do it’s best to return users to the same URL.

Code for this release is at https://github.com/sumitkm/BuildingSpaUsingKO/tree/Part4

Conclusion

So we’ve now replaced the Hash based routing of Hasher.js with the pushState based one by HistoryJS. We have also seen how HistoryJS gracefully falls back when using older browsers.

With that I’ll end this article here and push the server side manipulation to the next part.

Just so you know, on modern browsers that support pushstate currently if we visit the /settings page directly it will show us an invalid URL.

image

Ideally it will show a 404, but my 404 redirection is broken so it’s showing this weird error. Eitherways, we’ll end up with an error. In the next part we’ll see how we can fix this.

Tagged , , , , , ,
%d bloggers like this: