Thursday, September 06, 2012

knockout.composite: Putting it All Together

*UPDATE* Tribe is here! Check out http://tribejs.com/ for guides and API reference.

This is part 4 of a multi-part post. It’s assumed that you have a basic knowledge of the data binding and observability features of knockout.js.

Part 1: Introducing knockout.composite
Part 2: The Basics
Part 3: Getting Around
Part 4: Putting it All Together
Part 5: Unit Testing: Model Citizens 
Part 6: Integration Testing: Playing Nicely Together
Part 7: Functional Testing: Automated UI Tests, Anywhere, Any Time
Part 8: Pack Your App and Make it Fly
Part 9: Parting Thoughts and Future Directions

 

We’ve now seen some of the basic ways to use knockout.composite, so let’s build something functional with it. If you’ve run through the knockout.js webmail tutorial, you might be getting some deja vu very shortly! We’re going to replicate this using knockout.composite – you can check out the final version here.

So, either grab the source and head to the “Examples/6. Webmail” folder where you’ll find the code for each step, or head to github and view the source online. We’ll also post links to the working version of each step.

Again, it’s highly recommended using Chrome as you work your way through, you’ll find it much easier to see things working in the dev tools. If you are tinkering and have troubles getting things working, the console is your friend. knockout.composite dumps a bit of stuff there and you can see any errors that may have occurred.

Piece By Piece

Let’s build our components one by one.

Show Me Some Folders

If you look at the first step in the example folder, we’ve added a boilerplate index.html and some placeholders for our first pane.

Webmail1

Let’s have a crack at a model.

ko.composite.registerModel(function (pubsub, data, pane) {
    this.folders = ['Inbox', 'Archive', 'Sent', 'Spam'];
    this.selectedFolder = ko.observable('Inbox');
});

Pretty simple stuff. An array of folder names and an observable to hold the selected one. The template?

<ul class="folders" data-bind="foreach: folders">
    <li data-bind="text: $data, 
                   css: { selected: $data === $root.selectedFolder() }, 
                   click: $root.selectedFolder"></li>
</ul>

OK. Give me a list element with a list item for each array item in folders. Set the “selected” CSS class if the selectedFolder property matches the current folder name. When we click, chuck the value in the selectedFolder property.

No worries, mate! See it here.

What’s in a Folder

So now we’re going to build a grid to display the list of emails in each folder. This time, since we’re dealing with more than one pane, we’ve added a simple HTML only layout pane as well as the one for viewing the contents of the folder.

Webmail2

The template for the mail grid is pretty straightforward. Table, couple of bindings, whatever. Let’s have a look at the model.

ko.composite.registerModel(function (pubsub, data, pane) {
    var self = this;

    self.data = ko.observable();

    pubsub.subscribe('folderSelected', function(folder) {
        $.getJSON('../data/folder/' + folder, self.data);
    });
});

When someone tells me via our pubsub event bus thing-o that they’ve selected a folder, let’s load it up and push it into an observable.

Cool, let’s rig up the model for our list of folders to do just that.

ko.composite.registerModel(function (pubsub, data, pane) {
    var self = this;
    
    self.folders = ['Inbox', 'Archive', 'Sent', 'Spam'];
    self.selectedFolder = ko.observable();

    self.selectFolder = function (folder) {
        self.selectedFolder(folder);
        pubsub.publish('folderSelected', folder);
    };

    self.childrenRendered = function() {
        self.selectFolder(self.folders[0]);
    };
});

The first thing you’ll probably notice over the previous version is the selectFolder function. Selecting a folder now consists of two things, setting the observable and publishing a message. This will now be the target of our click binding.

The second function, childrenRendered, is a special “lifecycle” function (described previously) that is executed automatically when all panes in the current rendering cycle have completed. In a nutshell, once we know the other pane is ready for our message, select a default folder.

Too easy! Check it out here.

You’ve Got Mail

Right. We wouldn’t have much of a webmail system if it didn’t let us read mail, would we?

Following the same pattern, let’s publish a message when we click on a row in the mails grid, but now we need to navigate to another pane to show the email. If we move our message subscribers up to the layout pane, we have a central place to trigger the navigation from. The layout pane becomes a kind of “controller” (a note about this pattern later in the post).

First of all, we need to set up the layout pane for navigation.

<div data-bind="pane: 'folders'"></div>
<div data-bind="pane: { handlesNavigation: true }"></div>

Then add a model for the layout pane and add the message subscribers.

ko.composite.registerModel(function (pubsub, data, pane) {
    pubsub.subscribePersistent('folderSelected', function(folder) {
        pane.navigate('mails', { folder: folder });
    });

    pubsub.subscribePersistent('mailSelected', function (mail) {
        pane.navigate('viewMail', { mailId: mail.id });
    });
});

Looks simple enough. When someone selects a folder, show the mails pane, when someone selects a mail, show the viewMail pane. We have to use subscribePersistent here or our subscriptions will be cleaned up when we navigate.

All we need now is for our mails pane to publish the mailSelected message when someone clicks on a row in the mails grid. Add a quick function to our view model…

ko.composite.registerModel(function (pubsub, data, pane) {
    var self = this;

    self.data = ko.observable();

    self.initialise = function () {
        $.getJSON('../data/folder/' + data.folder, self.data);
    };
    
    self.selectMail = function (mail) {
        pubsub.publish('mailSelected', mail);
    };
});

…and bind it to each table row…

<tr data-bind="click: $root.selectMail">

You’ll notice we also changed the call to pubsub.subscribe to an initialise function. Now that we’ll be navigating to this pane and passing in the folder name, we can use the initialise function to load up the data.

All that’s left is the pane to actually view the email. It’s pretty simple boilerplate stuff, not worth showing here, so have a squiz at what our app looks like now.

The Final Pieces

So you probably noticed by now, we ain’t got no back button or refresh support. As promised, this is a piece of cake. Adding a script reference to the jQuery hashchange library takes care of most of it, and will for most applications.

The data that is stored in the URL is used to reconstruct the navigation pane. Since the folders pane is outside our navigation pane, it also needs to remember which folder is selected. Applying the history observable extension to the selectedFolder observable makes this easy.

self.selectedFolder = ko.observable().extend({ history: { key: 'folder' } });

We’re also going to enable the “fade” pane transition. This is as simple as adding an option to our navigation pane declaration in layout.htm:

<div data-bind="pane: { handlesNavigation: true, transition: 'fade' }"></div>

So make sure that the pane is fully loaded and rendered before we fade it in, we’re also going to take advantage of a little trick – returning a jQuery deferred object (such as those returned by $.ajax) from the initialise model method will cause the framework to wait for the deferred to resolve or reject before completing the rendering operation. This is from mails.js:

self.initialise = function () {
    return $.getJSON('../data/folder/' + data.folder, self.data);
};

See it here.

Some Observations…

Publish / Subscribe

It’s worth noting we could have done away with the whole pubsub thing in the end. Instead of publishing messages, we could just call pane.navigate directly in the click event handlers. This would result in a bit less code, but using the messaging pattern decreases the coupling in our application.

When we added the viewMail pane, we significantly changed the behaviour of our application, but we didn’t need to change a single line of code in the folders pane. It’s a small win in such a trivial example, but on a significantly larger scale, it’s a significantly larger win!

“Controller” Pattern

The “controller” pattern described above is a simple and useful pattern, the handlers can be implemented and grouped wherever (it doesn’t have to be within a pane) and it promotes a bit of flexibility, but it doesn’t scale particularly well. I’m currently using finite state machines (machina.js) to control site navigation and multi-step user processes, and it’s awesome, fits well with the pubsub model. More in a future post.

More!

What would a JavaScript framework be without the obligatory todo app? TodoMVC have a great little todo app that’s been ported to a large variety of frameworks. Check out the knockout.composite version.

Next up, testing, from unit testing your models to functional testing your whole app, all hosted within a browser.

2 Comments:

Blogger Paul said...

Dude, this is freaking cool!

A few small suggestions:
- You should start with the post with "this is what we're going to build" and link to the final version.
- The link to the final version is broken (it points to "the last piece" but it should be "the final pieces")

1:51 PM  
Blogger Dale Anderson said...

Thanks man! Good suggestions, updated.

4:20 PM  

Post a Comment

<< Home