Part 7: Sharing Data in Knockout Components (2/2) – Events and Messages


In my previous articles I’ve shown how to build KO components that start off with a data source of their own and are pretty self contained with respect to interactions with the rest of the page. But, in the first part of this article we built a tree control that’s I am planning to use as an Index page. So an action like ‘Clicking’ a link needs to affect another part of the application/web page.

Beauty (and often the bane) of JavaScript is that there are multiple ways to do this. You could go low down to core JavaScript event handling or jQuery event handling quite easily. However, the beautiful View-ViewModel separation that you have created using KO Components would be completely ruined if you had to know the ID of the UI element to hook an event handler in another component or in the parent page for that matter.

Solution? A light weight message passing system, that subscribes to events for you and publishes the event to all who have subscribed.

Message passing whaa…

Did you just wince at the sound of ‘message passing system’, fear not, it’s not rocket science, in fact it’s pretty simple. Take the two scenarios below.

Scenario 1

The good ol’ fashioned jQuery way of handling events. You take a document element and attach an event handler function to it. When the event occurs the attached function is called. You can do this anywhere in your JavaScript code, only requirement is that the element referred to by the id is available in the HTML DOM.

Here, reference by the id of the element, breaks the model view separation where the View Model has explicit knowledge of the View’s elements. This is hard coupling and should be avoided.

Scenario 2

Instead of attaching our event handler code directly, we list our function in a centrally located – let’s call it a ‘registry’ for now, using a unique string key.

Next when the particular event happens we tell the ‘registry’ that a ‘named’ event has happened and that all subscribers should be notified.

The registry takes the name and looks up to see if any functions where registered against it, if so, those functions are called.

Thus, by using a central location for registering event names and their handlers, we have decoupled View and ViewModel. Now View can raise an event, ViewModel handles the event via KO’s event handling mechanism and then tells the central registry to dispatch a named event to all the subscribers.

The subscribers need not be a part of this ViewModel, they could be part of any ViewModel on the page. They would register to listen of the ‘named event’ by providing an event handler function. This is the key, no ‘ids’ need to be shared across ViewModels.

Once the named event occurs the event handler functions are called.

Using AmplifyJS

Now that we’ve got an idea of how we can decouple the UI from the view model and still pass data around components, let’s use AmplifyJS as our ‘events broker’ and see what it takes to implement it.

Amplify was created by the team at http://appendto.com/team and is made available under MIT and GNU OSS licenses.

Installing AmplifyJS

Installing AmplifyJS is easy. Use nuget package manager console and install package using the following command

install-package AmplifyJS

This will create a Scripts/amplify folder and put each amplify component in a separate file here. It will also place amplify.js and amplify.min.js in the Scripts folder. Since we’ll be using the entire library we’ll delete the separate component files and move the amplify.* files from Scripts to Scripts/amplify

Since we’ll need Amplify globally to watch over all messages passed to and from it, we’ll set it up in the configuration require.config.js as highlighted below

var require = {
    baseUrl: "/",
    paths: {
        "bootstrap": "Scripts/bootstrap/bootstrap",
        "historyjs": "Scripts/history/native.history",
        "crossroads": "Scripts/crossroads/crossroads",
        "jquery": "Scripts/jquery/jquery-1.9.0",
        "knockout": "Scripts/knockout/knockout-3.2.0beta.debug",
        "knockout-projections": "Scripts/knockout/knockout-projections.min",
        "signals": "Scripts/crossroads/signals",
        "text": "Scripts/require/text",
        "app": "app/app",
        "underscore": "Scripts/underscore/underscore",
        "amplify": "Scripts/amplify/amplify.min",
    },
    shim: {
        "bootstrap": {
            deps: ["jquery"]
        }
    }
}

Updating tree-node to handle click event

We update the tree-node view to wrap the text span in an anchor, and attach a click handler

<ul class="nav nav-stacked" style="padding-left:10px">
    <li class="nav list-group-item-heading selected">
        <div class="row">
            <div class="col-sm-1 hidden-xs">
                <!-- ko if: nodes().length > 0 -->
                <!-- ko if: expanded() -->
                <a data-bind="click: changeState" role="button" href="">
                    <i class="glyphicon-minus"></i>
                </a>
                <!-- /ko -->
                <!-- ko ifnot: expanded() -->
                <a data-bind="click: changeState" role="button" href="">
                    <i class="glyphicon-plus"></i>
                </a>
                <!-- /ko -->
                <!-- /ko -->
            </div>
            <div class="col-sm-5">
                <a href="" data-bind="click: nodeClicked">
                    <span data-bind="text: title"></span>
                </a>
            </div>
        </div>
    </li>
    <!-- ko if: nodes().length > 0 && expanded() === true -->
    <!-- ko foreach: nodes -->
    <li>
        <tree-node params="node: $data"></tree-node>
    </li>
    <!-- /ko -->
    <!-- /ko -->
</ul>

In the viewModel we handle the click event and raise

The following snippet handles the click event

self.nodeClicked = function (currentNode) {
	currentNode.expanded(!currentNode.expanded())
	alert("hello " + currentNode.title());
}

First up it simply raises an alert that says hello with the name of the node. We have also setup the node to flip the expanded flag.

If we run the application now and click a node, we’ll see the popup.

image

So far so good. The node has directly responded to the click event.

Raising Event using Amplify

In the tree-node.js’ nodeClicked function we can ‘raise’ an event or pass message to our queue that the click event has happened. To do this we first add reference to amplify in our module. Since we have registered it globally all we have to do is use its alias:

define(["knockout", "text!./tree-node.html", "amplify"], function (ko, treeNodeTemplate) {

Next we upload the clicked event function as follows:

self.nodeClicked = function (currentNode) {
	currentNode.expanded(!currentNode.expanded());
   var nodeName = currentNode.title();
   amplify.publish(&amp;quot;CurrentNode-NodeClicked&amp;quot;, nodeName );
}

As you can see the call to ‘raise’ or ‘publish’ the event via amplify is pretty simple. All we have done is specified a string key, and then passed on the value we want to event receiver to get, which in this case is the nodeTitle. We could have passed the entire node if we wanted.

Note: I have removed the alert from the nodeClicked Event. Now we need another component to subscribe to this event and handle it when it occurs.

The ‘Content Pane’

The node click is supposed to be handled by a ‘Content Pane’ component that pulls content from another source. For brevity, I’ll ‘re-use’ the greeter component. We update the greeter component such that it handles the click event and updates the greeting.

First we add reference to Amplify

define(["knockout", "text!./greeting.html", "amplify"], function (ko, greeterTemplate) {

Next we add a ‘subscription’ for the event and update the greeting text

    function greeterViewModel(params) {
        var self = this;
        self.greeting = ko.observable(params.name);
        self.date = ko.observable(new Date());
        amplify.subscribe("CurrentNode-NodeClicked", function (value) {
            self.greeting(value);
        });
        return self;
    }

As we can see, subscribing to the ‘event’ is as simple as

– Specifying the same key that we published it with,

– Providing a function that will be called when the event happens.

In the function we have changed the greeting property of the viewModel.

Now we add the greeter page component in the docs.html

<div class="container">
    <h1>SilkThread Documentation</h1>
    <p>
        This page serves as the root for SilkThread documentation.
        It is itself built with SilkThread and we'll see how muliple
        web-components can be put together to create a neat App.
    </p>
    <div class="row">
        <div class="col-lg-3">
            <tree params="data: data"></tree>
        </div>
        <div class="col-lg-9">
            <greeter></greeter>
        </div>
    </div>
</div>

Demo Time

We are all set, when we run the app initially we come up with this

image

When we click on Node 0 the greeting changes to

image

Also note that the node has collapsed because of the code we wrote to toggle the expanded state.

Conclusion

This light weight message Publishing-Subscribing system is an established pattern often referred to as Pub-Sub. It is a very useful pattern in terms of decoupling interactions. It is used not just in JavaScript, it can be used in highly scalable backend systems.

SignalR is another example of a Pub-Sub system that maintains subscribers on the server and publishes actions to all connected clients. But it is used across server and client systems.

So as you can see the Pub-Sub system is a versatile software development pattern that we leveraged on the client side today.

That concludes this sub-series of my overall Silkthread series. Next we will see how we can use Amplify to make http requests and send data to server and retrieve data back.

The code as always is on Github. This specific branch is saved as Part7-2.

Advertisements
Tagged , , ,

7 thoughts on “Part 7: Sharing Data in Knockout Components (2/2) – Events and Messages

  1. robert says:

    now it starts to get really exciting. i used to have a bunch of global observables that were shared across models but that seemed always muddy to me. can’t wait for the server data source handling…

  2. Salil Junior says:

    This has been extremely informative, thanks big! A stupid question: Is there an advantage in using Hasher instead of History JS? Also, just like Robert commented, I can’t wait to see how you recommend server data source handling… Thanks big!

    • Sumit says:

      Hi Salil, Hasher I believe is the default library with crossroads. Using HistoryJS allows you to use clean urls in browsers that support pushstate and fallback to # based URL for browsers that don’t support pushstate.

      Hasher uses # based routes only.

      Apologies, the next article in the series is very delayed. Will get it out of the door one of these days.

      • saliljnr says:

        Thanks a lot Sumit! Indeed I read more on Hasher and you are spot on, it users # based routes only. I am building and administration back end for an application and I will stick with Hasher on this one because I have no need for SEO or any of the other advantages that come with History JS.
        I am looking forward to your next series – your articles are superb. thanks!

  3. bong says:

    It’s been several months now, are you still continuing on this series? Its such an informative series, I hope you are able to finish it.

  4. Ken Hadden says:

    I know this is an old series, but I enjoyed reading it. Have you ever used DI in javascript? Was wondering if you have any thoughts on decent libraries that would work well with the libraries you’ve mentioned in your posts.

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

%d bloggers like this: