A New Kind Of JavaScript Router

Until now, you only had two kinds of routers to choose from: lazy routers and overbearing routers. A lazy router does little more than relay requests to methods, but has the temerity to leave the hard work of hyperlink construction up to you. An overbearing router will do the jobs a lazy router won’t touch, but only if you agree to its choice of UI library and if you give it the final say on how your views are loaded. Now, there’s a new kind of router to choose from: the diligent and unassuming Navigation router.

The Navigation router doesn’t mind whatever UI library you choose to use. That isn’t to say it doesn’t care, because it works hard to accommodate your choice. It provides custom Angular directives, Knockout bindings or React components so you can create hyperlinks in native UI syntax. These hyperlinks go above and beyond what even the most conscientious of overbearing routers can provide. To demonstrate, we’ll spend the rest of this article building a mini replica of the smashingmagazine.com website as a Single Page Application.

Listing The Articles

The smashingmagazine.com website is a great one to replicate because it has plenty of different types of hyperlinks to get our teeth into: main menus, category filters, pagination and article selection. Each type will illustrate a different aspect of the Navigation router. Let’s start with the main menu hyperlinks.

Listing the Articles

Before we can build these hyperlinks, we must configure the Navigation router with a list of States. Each State represents a different view within the website. Clicking the Smashing Magazine logo takes us to a list of articles, so let’s create a State representing this view.

var articleDialog = {
    key: 'article', initial: 'list', states: [
        // The list of articles State
        { key: 'list', route: '' }
    ]
};

We’ve assigned the State an empty route because it represents the Smashing Magazine home page. Although the State only consists of a key and a route, it must be wrapped inside a Dialog. Dialogs group related States together. When we come to add the hyperlinks for the article selection, this Dialog will gain another State.

The second menu hyperlink displays a list of books. To represent this view, we’ll create a State with a ‘books’ route and wrap it in a new Dialog.

var bookDialog = {
    key: 'book', initial: 'list', states: [
        { key: 'list', route: 'books' }
    ]
};

We could continue configuring States for the remaining four menu items but, for the sake of this demo, the two States we have already are enough. The configuration is completed by passing the Navigation router an array containing the two Dialogs.

Navigation.StateInfoConfig.build([ articleDialog, bookDialog ]);

It’s time to create the hyperlinks. Make a note of the ‘article’ and ‘book’ Dialog keys because we can’t define the hyperlinks without them. The hyperlink syntax is dependent on your choice of UI library. If you’re using Angular, you can hyperlink to the articles view using the navigation-link custom directive.

<a navigation-link="article">Articles</a>

That same hyperlink in Knockout is created using the navigationLink custom binding.

<a data-bind="navigationLink: 'article'">Articles</a>

And in React, it’s the turn of the NavigationLink custom component.

<NavigationLink action="article">Articles</NavigationLink>

There’s a number of code snippets to come and, rather than writing each one out three times (for Angular, Knockout and React), they’ll be written just the once in Knockout. Even if you’re unfamiliar with Knockout, you should still be able to make sense of the snippets. But don’t give up if you’re struggling, at the end of each section are links to CodePens containing the equivalent code written in Angular and React.

Let’s flesh out the HTML by adding a list of articles below the top level menu items. We’ll add a conditional binding so the articles are only shown when the screen’s in a specific mode. The screen mode will be set when the hyperlinks are clicked. To aid readability, the similar HTML for the list of books is omitted.

<!-- The top level menus -->
<ul id="menu">
    <li><a data-bind="navigationLink: 'article'">Articles</a></li>
    <li><a data-bind="navigationLink: 'book'">Books</a></li>
</ul>

<div id="content">
    <div data-bind="if: mode() == 'articles'">
        <!-- The list of articles -->
        <ul data-bind="foreach: articles">
            <li>
                <span data-bind="text: title" ></span>
                <div>By <span data-bind="text: author"></span></div>
            </li>
        </ul>
    </div>
</div>

The accompanying View Model has properties for the screen mode and articles array. There are two functions that set the appropriate screen mode. The first one also populates the articles array from a demo list of articles returned by the getArticles function.

function ViewModel(){
    var self = this;

    // The screen mode and articles
    self.mode = ko.observable();
    self.articles = ko.observableArray();

    // Populate the screen mode and articles
    self.showArticles = function(){
        self.mode('articles');
        var articles = getArticles();
        self.articles(articles);
    };

    self.showBooks = function(){
        self.mode('books');
    };
}

To ensure the showArticles function is called whenever the first hyperlink is clicked, we must map this function to the State that represents the article list view. The Navigation router exposes a dialogs object which we can use to access the ‘article’ Dialog and, from there, gain access to the ‘list’ State. We can then map the function to the State by assigning the showArticles function to the State’s navigated property.

var articleDialog = Navigation.StateInfoConfig.dialogs.article;
var articleListState = articleDialog.states.list;

// Map the function to the State
articleListState.navigated = self.showArticles;

So that the showBooks function is called whenever the second hyperlink is clicked, a similar mapping is made between it and the books ‘list’ State.

We’ll place a call to Navigation.start() when the page loads. This simulates a hyperlink click for the browser URL to start us off on the list of articles. You can see the top level hyperlinks all styled up and working in the following CodePen. If Knockout isn’t to your taste, functionally identical Angular and React CodePens are also available.

Filtering The Articles

Let’s move on to the hyperlinks that filter the list of articles by category. They appear in both the article list and book list views and, on a narrow screen, sit directly below the top level menu.

Filtering the Articles

The only difference between these hyperlinks and the first top level menu hyperlink we’ve just built is that they need to pass along the category filter. We can augment the navigationLink custom binding with a toData object containing the category name. To keep the demo simple, we’ll only create the first two out of the set of six hyperlinks.

<ul id="categories">
    <li><a data-bind="navigationLink: 'article',
            toData: { category: 'coding' }">Coding</a></li>
    <li><a data-bind="navigationLink: 'article',
            toData: { category: 'design' }">Design</a></li>
</ul>

The corresponding View Model change is to use the category name to filter the list of articles. By adding a data parameter to the showArticles function, we can get our hands on the selected category.

self.showArticles = function(data){
    self.mode('articles');

    // Use the data.category to filter the articles
    var articles = getArticles().filter(function name(article) {
        if (!data.category)
            return true; 
        return article.category === data.category;
    });
    self.articles(articles);
};

Let’s pass the category name as a route parameter rather than in the query string. We’ll revisit the configuration of the State for the article list view and change the route to ‘{category?}’. The ‘?’ indicates the parameter’s optional and is needed because there’s no category passed when the top level menu hyperlink is clicked.

var articleDialog = {
    key: 'article', initial: 'list', states: [
        { key: 'list', route: '{category?}' }
    ]
};

You can take a look at the category filters in action in Angular or React or in the Knockout CodePen below.

Paging The Articles

A set of paging hyperlinks appear just below the list of articles. It’s their turn for the Navigation router treatment.

Paging the Articles

Clicking a paging hyperlink keeps us on the article list view, with only the page number changed. Because the view doesn’t change, there’s no need to specify the ‘article’ key when creating the paging hyperlinks. Instead, we can use the refreshLink custom binding which only requires the data to pass.

<a data-bind="refreshLink: { page: 2 }">2</a>

We mustn’t forget about the category filter when creating the paging hyperlinks. After filtering by ‘Coding’, for example, changing the page number must keep us within that category. We can rely on the Navigation router’s excellent memory to keep track of the chosen category for us. Telling the refreshLink binding to include the current data will ensure our paging hyperlinks include this filter.

<div id="paging" data-bind="foreach: pages">
    <a data-bind="refreshLink: { page: page }, 
        includeCurrentData: true, text: page"></a>
</div>

This HTML expects a pages array property on the View Model, containing an item for each available page.

self.pages = ko.observableArray();

Before we make the remaining changes to the View Model to accommodate the pagination section, we’ll make our lives easier by configuring a default value for the page number. Guaranteeing a value for the page number avoids those annoying existence checks that can quickly clutter up an otherwise tidy View Model.

var articleDialog = {
    key: 'article', initial: 'list', states: [
        { key: 'list', route: '{category?}', defaults: { page: 1 } }
    ]
};

We’ll change the showArticles function on the View Model to populate the array of pages. Smashing Magazine shows eight articles per page but, to keep the demo article list to a reasonable length, we’ll dial this down to three. The page number passed in is used to pick out the right slice of the articles array.

self.showArticles = function(data){
    self.mode('articles');
    var articles = getArticles().filter(function name(article) {
        if (!data.category)
            return true; 
        return article.category === data.category;
    });

    // Calculate the number of pages
    self.pages.removeAll();
    for(var i = 0; i < Math.ceil(articles.length / 3); i++){
        self.pages.push({ page: i + 1 });
    }

    // Use the data.page to pick out the articles to display
    var start = (data.page - 1) * 3;
    articles = articles.slice(start, start + 3);
    self.articles(articles);
};

You can play with the pagination and filters in the Knockout CodePen below, or in the corresponding CodePens for Angular and React.

Selecting An Article

The last hyperlink in our sights is the article title which, when clicked, takes us to the full text of the article. First, let’s create a State for this new view and add it to the ‘article’ Dialog that we already have. This Dialog will then group together the States representing our two article-centric views. Because the two views are connected together by the title hyperlink, we’ll connect up the two States using a Transition. The Transition, representing the direction of travel of the hyperlink, is added to the first State and points at the second.

var articleDialog = {
    key: 'article', initial: 'list', states: [
        { key: 'list', route: '{category?}', defaults: { page: 1 },
          transitions: [
            // The Transition from the article list to details
            { key: 'select', to: 'details' }
        ]},
        // The article details State
        { key: 'details', route: 'article/{slug}' }
    ]
};

We gave this new State a route of ‘article/{slug}’ because Smashing Magazine URLs contain slugs, or user-friendly keywords, to identify the article selected. We’ll use the ‘select’ key of the Transition that connects the States to create the title hyperlink, passing along the article’s slug in the toData.

<ul id="articles" data-bind="foreach: articles">
    <li>
        <a data-bind="navigationLink: 'select', 
            toData: { slug: slug }, text: title" ></a>
        <div>By <span data-bind="text: author"></span></div>
    </li>
</ul>

The HTML for the article view starts with a screen mode check, followed by bind statements that pull together the properties of interest from the selected article.

<div data-bind="if: mode() === 'article'">
    <div id="article" data-bind="with: article">
        <h2 data-bind="text: title"></h2>
        <div>By <span data-bind="text: author"></span></div>
        <p data-bind="text: text"></p> 
    </div>
</div>

The supporting View Model changes are the addition of an article property and a showArticle function. The function sets the screen mode, then populates the article using the slug passed in.

self.article = ko.observable();
self.showArticle = function(data){
    self.mode('article');

    // Use the data.slug to find the article
    var article = getArticles().filter(function name(article) {
        return article.slug === data.slug;
    })[0];
    self.article(article);
}

If we forget to map the showArticle function to the new State, the function won’t be called when the title hyperlink is clicked.

var articleDetailsState = articleDialog.states.details; 
articleDetailsState.navigated = self.showArticle;

It would be handy to have a hyperlink below the article that remembers our place in the list, so that, once we’ve finished reading, we can return to the exact category and page we were on before selecting the article. The Navigation router’s photographic memory let’s us create such time-travelling hyperlinks using the navigationBackLink custom binding.

<a data-bind="navigationBackLink: 1">Continue browsing</a>

The Navigation router achieves this feat of memory by storing data in the URL. So, for views that don’t need navigationBackLink bindings, it’s best to disable this feature by setting trackCrumbTrail to false against the view’s State representation. We can safely turn it off for both the article list and book list views.

var bookDialog = {
    key: 'book', initial: 'list', states: [
        { key: 'list', route: 'books', trackCrumbTrail: false }
    ]
};

Put the Navigation router’s memory to the test by clicking around in the Angular, React or Knockout CodePens.

Can Your Router Do That?

Thanks to the diligent and unassuming Navigation router, we’ve built a mini replica of the smashingmagazine.com website as a Single Page Application. In fact, we’ve built three mini replicas, one in each of Angular, Knockout and React. We’ve created all the hyperlinks using the UI syntax native to each library. The end result is a lean and mean code base, with no hyperlink construction code leaking into the Knockout View Model, for example.

We wouldn’t have fared nearly so well if we’d used a lazy or overbearing router. With a lazy router, we would’ve soon grown tired of the URL manipulation code required to build the variety of hyperlinks on show. An overbearing router might have started out fine but, because it has such a short memory when compared to the Navigation router, would’ve ended up struggling just as much as any lazy router. If you cherish hyperlinks and the freedom to create your UI however you wish, the Navigation router’s your only choice.

Advertisements

One thought on “A New Kind Of JavaScript Router

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s