danielwertheim

danielwertheim


notes from a passionate developer

Share


Sections


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

Super simple SPA sample

This is going to be a really super simple sample showing you how to set up a single page application (SPA) using PathJs, KnockoutJs and HeadJs.

The code for this can be found in this Gist on GitHub.

Lets start with the HTML document index.html:

<html lang="en">
    
        <meta charset="utf-8">
        <title>Super simple SPA template</title>
    
    
        <!-- Just some fake menu -->
        <a href="#/hello/sire">Hello</a>
        <a href="#/bye/sire">Bye</a>

Ok, so have a quick look at the URLs in the super simple (poor) menu. It uses "hashbang" URLs, and with the help of PathJs this will let us invoke JavaScript functions without reloading the page or doing any new HTTPRequests.

Now, you probably are asking yourself:

"So, where’s the content coming from and where is it being shown?"

Excellent question! The content will be shown in a HTML element of your choice. In this simple demo, I’ve chosen this:

<div id="bindingContext"></div>

So whenever a registered route is being visited, the following will take place:

  1. The module matching the path will be used to create a view model
  2. The module name is used to identify a certain HTML-template to bind the view model to.
  3. The template is loaded into the bindingContext and then the view model is bound against the bindingContext using KnockoutJS.

Lets have a look at the module, defined in app.modules.js:

(function () {
    var module = app.modules.Hello = {
        route: 'hello/:msg',
        vm: function (req) {
            this.message = ko.observable(req.args['msg']);
        }
    };
})();

The route is what build the URL, which in this case will match index.html#/hello/world I’m using the functionality of PathJs to pass URL arguments via :paramname. The vm is the view model which will be instanciated using the new keyword. It just defines a simple Knockout``observable for holding the message. In this case, we actually don’t need the observable since it’s populated before the binding.

Now, we need a template. For keeping it simple I’ve inlined them in index.html but you could easily load them e.g via AJAX, and you should then make use of content caching of the output. The template is really simple and looks like this:

<script id="hello.tmpl" type="text/html">
    <p>Hello
    <span data-bind="text: message"></span>!</p>
</script>

If you were paying attention you noticed we had two routes. One for Hello and one for Bye. We could make the template (view) and the view model more advanced, but lets just duplicate it for now. Introduce one more module and one more template.

(function () {
    var module = app.modules.Bye = {
        route: 'bye/:msg',
        vm: function (req) {
            this.message = ko.observable(req.args['msg']);
        }
    };
})();
<script id="bye.tmpl" type="text/html">
    <p>Bye
    <span data-bind="text: message"></span>!</p>
</script>

Are we good to go? Not yet, we need to load some JavaScript and initialize the app. I’m using a simple script loader (notice the them of SIMPLE) called HeadJs.

<script type="text/javascript" src="js/head.js"></script>
<script type="text/javascript">
    head
        .js('js/knockout.js')
        .js('js/path.js')
        .js('js/app.js', 'js/app.modules.js');

    head.ready(function () {
        app.init(document.getElementById('bindingContext'));
    });            
</script>

Just pass in the reference to the DOM-node that should act as the bindingContext and we are good to go!.

But, where is the missing pieces. The glue, the magic?

Well, again, this is a quite simple sample so it’s not that much code. Lets have a look at app.js.

(function (exports) {
    var app = exports.app = {
        bindingContext: { },
        modules: { },
        router: { },
        init: function (bindingCtxNode) { }
    };
})(window)

Ok, I confess, I simplified it and did just show you the "outlining". Lets fill in the missing pieces. Lets start with the bindingContext. It’s responsible for binding the view model to the view, so it’s actually also taking care of loading the template.

bindingContext: {
    domnode: null,
    model: null, //will hold the current view model instance
    loadTemplate: function (templateName) {
        return document.getElementById(templateName).innerHTML;
    },
    bind: function (templateName, vm) {
        this.domnode.innerHTML = this.loadTemplate(templateName);
        ko.applyBindings(vm, this.domnode);
        this.model = vm;
    },
    clear: function () {
        if(this.domnode !== null) this.domnode.innerHTML = null;
        this.model = null;
    }
}

Not so bad. A domnode to hold a reference (will be set in app.init later) to the div we defined above. The model is actually something you could rip out but could be nice if you want to play around with it using the console. loadtemplate just loads the <script>template</script> defined above and bind creates the view model instance and glues together the view template and Knockout.

Lets continue with the router.

router: {            
    registerModule: function (moduleName, module) {
        if (!module.route) module.route = moduleName.toLowerCase();
        if (!module.templateName) module.templateName = moduleName.toLowerCase() + '.tmpl';

        Path.map('#/' + module.route).to(function () {
            app.bindingContext.bind(module.templateName, new module.vm({args: this.params}));
        });
    },
    start: function () {
        Path.listen();
    }
},

It makes use of PathJs to map routes to certain modules. It also ensures that each module has a route and a templateName which has a really simple convention based on the moduleName. start is just something required by PathJs, otherwise it will not intercept the routes.

The missing piece is the init function.

init: function (bindingCtxNode) {
    this.bindingContext.domnode = bindingCtxNode;

    for(var moduleName in app.modules) {
        this.router.registerModule(moduleName, app.modules[moduleName]);
    }

    this.router.start();
}

It just goes through all the modules and registers them with the router. That’s it. You now have a foundation that you can start toying around with and extend.

//Daniel

View Comments