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:
- The module matching the path will be used to create a view model
- The module name is used to identify a certain HTML-template to bind the view model to.
- The template is loaded into the
bindingContext
and then the view model is bound against thebindingContext
usingKnockoutJS
.
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