I’m reviewing Angular and Knockout to determine which would fit better for a variety of upcoming projects. Let’s talk Single Page applications, and more specifically, Routing and History. While AngularJS doesn’t have the word “SPA” on their front page, the tutorial jumps straight into building one. How hard is it to give Knockout the same routing capability? Does it end up worse?

This is the eighth of eight nine posts looking at the capabilities of knockout and Angular. In the introduction post, I outlined the capabilities that I am evaluating for. In the seventh post, I looked at unit testing. This post explores routing and history.

All of the examples presented throughout the series are available in the tarwn/AngularJS-vs-Knockout repository on github.

Routing in AngularJS

Routing is built into AngularJS and is one of the 12 steps of the AngularJS Tutorial. The $route provider is used to define routes and watching the URL bar to capture and apply changes.

Routing in AngularJS

Full source available at Angular/Routing.html.

A basic use case for routing would be to provide a listing page where each item links to it’s own detail page. Our expectation would be that loading the page will show the list, that the detail pages will allow navigation back to the list page, and that putting a URL in for a details page directly will load that page properly.

For the example, we’ll need a list controller, details controller, and a listing service:

Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
sampleApp.service('ListOfStuffService', function () {
    var stuff = [
        { id: 1, name: "First Item", description: "It's the first item, woohoo!" },
        { id: 2, name: "Second Item", description: "Numero Dos!" },
        { id: 3, name: "Third Item", description: "The third item, this one is always out of stock" }
    ];
 
    return {
        getAll: function () {
            return stuff;
        },
        getById: function (id) {
            for (var i in stuff) {
                if (stuff[i].id == id)
                    return stuff[i];
            }
        }
    };
});
 
sampleApp.controller('ListOfStuffController', ['$scope', 'ListOfStuffService', function ($scope, listService) {
    $scope.test = 5;
    $scope.listOfItems = listService.getAll();
    console.log($scope.listOfItems);
}]);
 
sampleApp.controller('StuffDetailController', ['$scope', '$routeParams', 'ListOfStuffService', function ($scope, routeParams, listService) {
    $scope.item = listService.getById(routeParams.id);
}]);
sampleApp.service('ListOfStuffService', function () {
    var stuff = [
        { id: 1, name: "First Item", description: "It's the first item, woohoo!" },
        { id: 2, name: "Second Item", description: "Numero Dos!" },
        { id: 3, name: "Third Item", description: "The third item, this one is always out of stock" }
    ];

    return {
        getAll: function () {
            return stuff;
        },
        getById: function (id) {
            for (var i in stuff) {
                if (stuff[i].id == id)
                    return stuff[i];
            }
        }
    };
});

sampleApp.controller('ListOfStuffController', ['$scope', 'ListOfStuffService', function ($scope, listService) {
    $scope.test = 5;
    $scope.listOfItems = listService.getAll();
    console.log($scope.listOfItems);
}]);

sampleApp.controller('StuffDetailController', ['$scope', '$routeParams', 'ListOfStuffService', function ($scope, routeParams, listService) {
    $scope.item = listService.getById(routeParams.id);
}]);

Implementing the routes is then pretty straightforward:

sampleApp.config(['$routeProvider', function (routeProvider) {
    // unlike the documentation, I had to put quotes around my controllers - maybe they had global variables?

    routeProvider.when('/ListOfStuff', { templateUrl: 'partials/Routing/ListOfStuff.html', controller: 'ListOfStuffController' });

    routeProvider.when('/StuffDetail/:id', { templateUrl: 'partials/Routing/StuffDetail.html', controller: 'StuffDetailController' });

    routeProvider.otherwise({ redirectTo: '/ListOfStuff' });
}]);

To translate, the URL routing.html#/ListOfStuff will show us the ListOfStuff.html template, the URL routing.html#/StuffDetail/123 will show us the StuffDetail template and pass along a parameter named “id” with 123 in it, and if no route matches, redirect to the first one.

The HTML templates then look like this:

Angular/partials/Routing/ListOfStuff.html

HTML
1
2
3
4
5
<div>
    <ul ng-repeat="item in listOfItems">
        <li><a href="#/StuffDetail/{{ item.id }}">{{ item.name }}</a></li>
    </ul>
</div>
<div>
    <ul ng-repeat="item in listOfItems">
        <li><a href="#/StuffDetail/{{ item.id }}">{{ item.name }}</a></li>
    </ul>
</div>

Angular/partials/Routing/StuffDetail.html

HTML
1
2
3
4
5
6
7
<div>
    <a href="#/ListOfStuff">Back to list</a><br />
    <br />
    <b>Id:</b> {{ item.id }}<br />
    <b>Name:</b> {{ item.name }}<br />
    <b>Description:</b> {{ item.description }}<br />
</div>
<div>
    <a href="#/ListOfStuff">Back to list</a><br />
    <br />
    <b>Id:</b> {{ item.id }}<br />
    <b>Name:</b> {{ item.name }}<br />
    <b>Description:</b> {{ item.description }}<br />
</div>

In contrast to some of the other AngularJS examples in earlier posts, this one just worked. I didn’t have to worry about how to make hash URLs work, detecting changes and writing code to parse the URLs, or anything, just a few simple, direct rules and a controller and template for each one.

Routing in … Knockout?

Well, crap. Up until now there has been some clear, obvious answers when I needed to pull in an extra library for Knockout. When it comes to routing and SPA-like behavior, the two answers I see the most frequently are Durandal (see Avi, I spelled it right that time) and Sammy.js. Knockout has dynamic template bindings, so for this post maybe Sammy will be enough? And why get locked in, let’s pick a few others at random and do them too. So here we go.

In all of these examples, we will need a ListOfStuffService, a ListOfStuffViewModel, a StuffDetailViewModel, and an overall viewmodel to represent the page and have these viewmodels (and their associated template) assigned to it. To reduce the amount of necessary example code, I’ve assigned the template names to the viewmodels.

Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// basic fake service with a GetAll and GetById call
define("ListOfStuffService", function () {
    var stuff = [
                { id: 1, name: "First Item", description: "It's the first item, woohoo!" },
                { id: 2, name: "Second Item", description: "Numero Dos!" },
                { id: 3, name: "Third Item", description: "The third item, this one is always out of stock" }
    ];
 
    return {
        getAll: function () {
            return stuff;
        },
        getById: function (id) {
            for (var i in stuff) {
                if (stuff[i].id == id)
                    return stuff[i];
            }
        }
    };
});
 
// and a module that has a controller that depends on the ListOfItemsService
define("ko-app",
    ["knockout"],
    function (ko) {
        return function () {
            this.viewmodel = ko.observable();
        }
    }
);
 
// and a module that has a controller that depends on the ListOfItemsService
define("ListOfStuffViewModel",
    ["knockout", "ListOfStuffService"],
    function (ko, listOfItemsService) {
        return function () {
            this.template = "ListOfStuffViewModel";
            var items = listOfItemsService.getAll();
            this.listOfItems = ko.observableArray(items);
        }
    }
);
 
// and a module that has a controller that depends on the ListOfItemsService
define("StuffDetailViewModel",
    ["knockout", "ListOfStuffService"],
    function (ko, listOfItemsService) {
        return function (id) {
            this.template = "StuffDetailViewModel";
            var item = listOfItemsService.getById(id);
            this.item = ko.observable(item);
        }
    }
);
// basic fake service with a GetAll and GetById call
define("ListOfStuffService", function () {
    var stuff = [
                { id: 1, name: "First Item", description: "It's the first item, woohoo!" },
                { id: 2, name: "Second Item", description: "Numero Dos!" },
                { id: 3, name: "Third Item", description: "The third item, this one is always out of stock" }
    ];

    return {
        getAll: function () {
            return stuff;
        },
        getById: function (id) {
            for (var i in stuff) {
                if (stuff[i].id == id)
                    return stuff[i];
            }
        }
    };
});

// and a module that has a controller that depends on the ListOfItemsService
define("ko-app",
    ["knockout"],
    function (ko) {
        return function () {
            this.viewmodel = ko.observable();
        }
    }
);

// and a module that has a controller that depends on the ListOfItemsService
define("ListOfStuffViewModel",
    ["knockout", "ListOfStuffService"],
    function (ko, listOfItemsService) {
        return function () {
            this.template = "ListOfStuffViewModel";
            var items = listOfItemsService.getAll();
            this.listOfItems = ko.observableArray(items);
        }
    }
);

// and a module that has a controller that depends on the ListOfItemsService
define("StuffDetailViewModel",
    ["knockout", "ListOfStuffService"],
    function (ko, listOfItemsService) {
        return function (id) {
            this.template = "StuffDetailViewModel";
            var item = listOfItemsService.getById(id);
            this.item = ko.observable(item);
        }
    }
);

And the HTML for the page and the viewmodels looks like this:

HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<body>
<!-- ko if: viewmodel() -->
<div data-bind="template: { name: viewmodel().template, data: viewmodel }">
</div>
<!-- /ko -->
 
<!-- ... -->
 
<script type="text/html" id="ListOfStuffViewModel">
    <ul data-bind="foreach: listOfItems">
        <li><a data-bind="attr: { href: '#/StuffDetail/' + id }, text: name"></a></li>
    </ul>
</script>
<script type="text/html" id="StuffDetailViewModel">
    <div>
        <a href="#/ListOfStuff">Back to list</a><br />
        <br />
        <b>Id:</b> <span data-bind="text: item().id"></span><br />
        <b>Name:</b> <span data-bind="text: item().name"></span><br />
        <b>Description:</b> <span data-bind="text: item().description"></span><br />
    </div>
</script>
</body>
<body>
<!-- ko if: viewmodel() -->
<div data-bind="template: { name: viewmodel().template, data: viewmodel }">
</div>
<!-- /ko -->

<!-- ... -->

<script type="text/html" id="ListOfStuffViewModel">
    <ul data-bind="foreach: listOfItems">
        <li><a data-bind="attr: { href: '#/StuffDetail/' + id }, text: name"></a></li>
    </ul>
</script>
<script type="text/html" id="StuffDetailViewModel">
    <div>
        <a href="#/ListOfStuff">Back to list</a><br />
        <br />
        <b>Id:</b> <span data-bind="text: item().id"></span><br />
        <b>Name:</b> <span data-bind="text: item().name"></span><br />
        <b>Description:</b> <span data-bind="text: item().description"></span><br />
    </div>
</script>
</body>

So far, the only major addition over the Angular example above is that outer viewmodel and the HTML to conditionally render it above. That “if” binding means that Knockout will not evaluate/display that area’s contents when the bound value is falsey.

I’ve made the examples slightly more complex than they needed to be because I’m still using RequireJS throughout them. This wasn’t necessary and probably serves as a little extra noise, but oh well.

Knockout Routing w/ Sammy.js

Full source available at Knockout/Routing.html.

So we have our viewmodels, we have a bare app viewmodel they will get socketed into, and we have HTML templates. Let’s define the routes using the Sammy.js library:

Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// define route and outer ko viewmodel
require(['knockout', 'ko-app', 'sammy'], function (ko, AppViewModel, sammy) {
    var app = new AppViewModel();
    ko.applyBindings(app);
 
    var routing = sammy(function () {
 
        this.get('#/ListOfStuff', function (context) {
            require(['ListOfStuffViewModel'], function (ViewModel) {
                app.viewmodel(new ViewModel());
            });
        });
 
        this.get('#/StuffDetail/:id', function (context) {
            var id = this.params['id'];
            require(['StuffDetailViewModel'], function (ViewModel) {
                app.viewmodel(new ViewModel(id));
            });
        });
 
        var self = this;
        this.get('', function (context) {
            self.runRoute('get', '#/ListOfStuff');
        });
 
    });
    routing.run();
});
// define route and outer ko viewmodel
require(['knockout', 'ko-app', 'sammy'], function (ko, AppViewModel, sammy) {
    var app = new AppViewModel();
    ko.applyBindings(app);

    var routing = sammy(function () {

        this.get('#/ListOfStuff', function (context) {
            require(['ListOfStuffViewModel'], function (ViewModel) {
                app.viewmodel(new ViewModel());
            });
        });

        this.get('#/StuffDetail/:id', function (context) {
            var id = this.params['id'];
            require(['StuffDetailViewModel'], function (ViewModel) {
                app.viewmodel(new ViewModel(id));
            });
        });

        var self = this;
        this.get('', function (context) {
            self.runRoute('get', '#/ListOfStuff');
        });

    });
    routing.run();
});

Using the Sammy library, I’ve defined the same three cases I had in Angular. Each route has a callback that it calls when the route is matched, which I used to create the appropriate ViewModel and assign it to my main “app” viewmodel, causing the template to be changed. If the empty route is matched, I explicitly run the ListOfStuff route.

Knockout Routing w/ Finch.js

Full source available at Knockout/Routing.FinchJS.html.

The tagline for FinchJS is “Powerfully Simple Javascript Routing”, and I have to agree that the library meets expectations. Implementing the same routes as the last two exmaples:

Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// define route and outer ko viewmodel
require(['knockout', 'mainApp', 'finch'], function (ko, AppViewModel, finch) {
    var app = new AppViewModel();
 
    finch.route("/", function () {
        finch.call("/ListOfStuff");
    });
 
    finch.route("/ListOfStuff", function () {
        require(['ListOfStuffViewModel'], function (ViewModel) {
            app.viewmodel(new ViewModel());
        });
    });
 
    finch.route("/StuffDetail/:id", function (bindings) {
        require(['StuffDetailViewModel'], function (ViewModel) {
            app.viewmodel(new ViewModel(bindings.id));
        });
    });
 
    ko.applyBindings(app);
    finch.listen();
});
// define route and outer ko viewmodel
require(['knockout', 'mainApp', 'finch'], function (ko, AppViewModel, finch) {
    var app = new AppViewModel();

    finch.route("/", function () {
        finch.call("/ListOfStuff");
    });

    finch.route("/ListOfStuff", function () {
        require(['ListOfStuffViewModel'], function (ViewModel) {
            app.viewmodel(new ViewModel());
        });
    });

    finch.route("/StuffDetail/:id", function (bindings) {
        require(['StuffDetailViewModel'], function (ViewModel) {
            app.viewmodel(new ViewModel(bindings.id));
        });
    });

    ko.applyBindings(app);
    finch.listen();
});

The route logic looks almost the same as Sammy.js, proving that I probably picked too easy of an example for this post.

Knockout Routing w/ flatiron director

Full source available at Knockout/Routing.Director.html.

One of the key goals for the flatiron director library is to work as seamlessly as possible in both the node.js and browser environments. director has HTML5 History API support, but I’ve configured it off for this example to match the others:

Javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require(['knockout', 'mainApp', 'director'], function (ko, AppViewModel, director) {
    var app = new AppViewModel();
 
    var routes = {
        '/ListOfStuff': function () {
                            require(['ListOfStuffViewModel'], function (ViewModel) {
                                app.viewmodel(new ViewModel());
                            })},
        '/StuffDetail/:id': function (id) {
                            require(['StuffDetailViewModel'], function (ViewModel) {
                                app.viewmodel(new ViewModel(id));
                            })}
    };
 
    ko.applyBindings(app);
    var router = director(routes);
    router.init('/ListOfStuff')
            .configure({
                html5history: false
            });
});
require(['knockout', 'mainApp', 'director'], function (ko, AppViewModel, director) {
    var app = new AppViewModel();

    var routes = {
        '/ListOfStuff': function () {
                            require(['ListOfStuffViewModel'], function (ViewModel) {
                                app.viewmodel(new ViewModel());
                            })},
        '/StuffDetail/:id': function (id) {
                            require(['StuffDetailViewModel'], function (ViewModel) {
                                app.viewmodel(new ViewModel(id));
                            })}
    };

    ko.applyBindings(app);
    var router = director(routes);
    router.init('/ListOfStuff')
            .configure({
                html5history: false
            });
});

In this case, the routes are defined as an array and I only define the two real routes. Once I’ve applied my bindings, I start up the routing by calling init with the default URL to use if there isn’t a hash address in the path.

This example does have an error, in that it doesn’t actually work if I load a hashed address from scratch. All three other examples work fine, so I suspect it’s something I’ve done wrong.

Some Differences

Angular’s injection kept the routing cleaner for that example, but really the biggest issue is that I didn’t pick a complex enough example. All of the methods I tried were easy to implement.

Templating

Angular’s routing automatically uses templating, whereas the others are using Knockout and using the standard templating that is built into knockout. Although, like so much of the rest of this series, composing external templates onto the knockout side is just a library away, in this case the Knockout.js External Template Engine.

I put together a copy of the Finch example above with external templating here: Knockout/Routing.FinchJS.ExtTemplates.html

The Details
As I mentioned above, all of these examples were really easy to put together, so I don’t think I really exercised the libraries to show off their differences. That being said, which package you choose (I’ve had people suggest I use custom routing for Angular, even) is probably going to depend on those details and which flavor makes the most sense for your scenario.

Final Thoughts

Man that was easy.There are a ton of good routing libraries out there and it seems like even if you made the wrong decision when you picked one, it would be relatively easy to switch to a different one later. I personally liked Finch.js the best, but if I was using AngularJS I’d probably ride the default library as long as I could (and may never have a reason to change).

Using the dynamic template to change “pages” was also easy. I banged on the links a while and Chrome said I was using a lot of memory, but as soon as I let the GC clean up, it all cleared up. Banging on AngularJS also used memory, but it had GC’s occurring along the way too, so I suspect if I were to use the knockout ‘sorta-SPA’ viewmodel in a production environment, I would actually want to use a method that delete()-ed the oold model as the new one was assigned.

Post 8 of 8 9

I’ll be posting one last post for this series that is a round up of all the comparisons and feelings I had throughout the series, my opinion on which framework I would use, and a list of things I should have also looked at in the series. Keep an eye out on LessThanDot (and twitter, google plus, etc) for that post.