Introduction
In Part I, we setup basic project and prepared output package for mobile device. This got us some benefits coming from statically typed system provided by Typescript, dependency loading and has given us some automation in building and possible integration with Continuous Integration systems.
In Part II, we layed out infrastructure for easy maintenance of large projects with Jade, Stylus and Q/A tools.
This part will cover bootstraping of Require.js application of data binding for applications, namely MVC and MVVC pattern with Knockout.js.
Bootstraping
In order to effectively use AMD system from Require.js library, we have to expose its components via export
keyword instead of providing global variables. Although this is becoming a standard way to distribute JS libraries, there are still libraries that do it in the old way and that might confuse Require.js.
To be able to use such libraries, we can bootstrap our application by using shims. In this part, we will be using Zepto.js (lightweight counterpart to jQuery) and Knockout.js which both exposes global objects - in case of Zepto it's $ like in jQuery and in case of Knockout it's knockout.
We will continue in the project from last time where we add minified zepto.min.js and knockout-2.3.0.js into lib folder. Now we need to tell Require.js where to look for those files; this can be easily achieved by using shim functionality and defining configuration file:
requirejs.config({
paths: {
"zepto": "libs/zepto.min",
"knockout": "libs/knockout-2.3.0"
},
shim: {
zepto: {
exports: "$"
}
}
});
Please note that we are not providing .js extension in paths
section.
Since we will be using those libraries across the project, this configuration needs to occur before first module tries to load it up. This brings interesting chicken-and-egg problem since Typescript generates declare
statements as first lines in the file so if we try to put this configuration into Typescript file and load, e.g., application.ts which in turn uses Zepto it will fail since at the moment of loading configuration was not yet processed.
There are two ways out of this problem - one is to write config.js in pure JavaScript and do imports in correct places or use mixed approach. In this example, we will use the latter one where we benefit from fact that JavaScript code is fully valid in Typescript files. We will update src/config.ts to the following:
"use strict";
requirejs.config({
paths: {
"zepto": "zepto.min",
"knockout": "knockout-2.3.0"
},
shim: {
zepto: {
exports: "$"
}
}
});
require(['zepto', 'knockout'], (zepto, knockout) => {
require(['main'], main => {
new main.Main().run();
});
});
As you can see, we are loading dependencies in two stages - reason for that is that Require.js is practicing lazy loading but in this special case we want to have all those libraries pre-loaded before continuing.
Other thing that is worth noticing is that we are referencing .d.ts files in header - without this information Typescript will not know about Require.js objects and functions. We can obtain this file from e.g. DefinitelyTyped site and put:
- require.d.ts files into /extern/require.d.ts
- zepto.d.ts into /extern/zepto.d.ts (we will use this one later)
- knockout.d.ts into /extern/knockout.d.ts (we will use this one later)
- knockout.amd.d.ts into /extern/knockout.amd.d.ts (we will use this one later - this one is AMD wrapper around standard Knockout)
Those files stands for Definition TypeScript - they contain only definitions of objects not real implementation. You can find more information about this in Typescript Language Reference.
Note: Typescript recently got support for generics and some libraries are already using this concept; previous package.json
was pointed to older version of Typescript so it should be updated (line 14) to:
"grunt-typescript": "0.2.1",
Last piece of change we need to do is to tell Grunt to copy all libraries from lib folder; change line 19 in Gruntfile.js to:
{ flatten: true, expand: true, src: ['lib/*.js'], dest: 'dist/www/js/'}
With all infrastructure in place, we can start using all libraries as usual with all benefits of Typescript and lazy-loading. Quick example could be updating src/main.ts to the following:
var $ = require("zepto");
export class Main {
constructor() {
console.log("main.init()");
$(".corpus").click(() => {
alert("hello world!");
}));
}
run() {
console.log("main.run()");
}
}
Which should display message box with message "hello world
" after clicking on text in browser.
Knockout.js
So far, it was more about talking on laying out infrastructure rather than doing the real work. In this chapter, we take a look at how to utilize all of the components we setup so far and we will build simple menu for us.
First, let's introduce Knockout.js library; it's Model-View-ViewModel pattern based binding library which will be familiar to Windows Phone developers.
For those unaware of concept, I recommend to try live examples on Knockout web pages but in our case we will split functionality as follows:
- Model - simple JSON files with data to be displayed
- View - HTML page generated from Jade and CSS styles from Stylus
- ViewModel - Typescript compiled into JavaScript binding view to model and acting on events
Let's start with Model - we create a new file src/menuModel.ts which will contain items that we wish to display:
export interface MenuItem {
name: string;
icon: string;
id: number;
};
export var items: MenuItem[] = [
{ id: 0, name: "Home", icon: "home.png" },
{ id: 1, name: "Items", icon: "items.png" },
{ id: 2, name: "Settings", icon: "settings.png" }
];
File defines interface on data displayed in menu and menu items itself - this information can also come e.g. from AJAX call.
Now to prepare ViewModel
which will establish data relation between View
and Model
. We will update src/main.ts to the following:
var $ = require("zepto");
import menuModel = module("./menuModel");
import ko = module("knockout");
export class Main {
menuItems: KnockoutObservableArray<menuModel.MenuItem>;
constructor() {
console.log("main.init()");
this.menuItems = ko.observableArray(menuModel.items);
}
run() {
console.log("main.run()");
ko.applyBindings(this);
}
}
Here we prepare observable array of our menu items for Knockout.js and we will bind this as main object. This is everything we need to do for display purposes - Knockout will handle everything else for us.
Last part is preparing View - since we most probably will be using same menu on multiple pages we will extract all relevant definition into new file views\menu.jade:
ul.menu(data-bind="foreach: menuItems")
li(data-bind="text: name")
First line created UL
element which will be bound to property named menuItems
; since we are using foreach
keyword, it is expected that this property will be Array
. Everything that is declared within this element will be copied as many times as is count of items in collection. Second line says it should be LI
element and text should be bound to property name of every item of collection menuItems
.
Since we want to create separate HTML files, not one big one, we need to update Gruntfile.js lines 45-47:
files: [
{ expand: true, ext: ".html", cwd: "views/", src: ['*.jade'], dest: 'dist/www/' }
]
Last bit of functionality is to include menu into our index view - we will update views/index.jade:
html
head
link(rel='stylesheet', href='css/index.css')
script(data-main='js/config', src='js/require.js')
body
include menu.jade
.corpus Hello, world!
That was easy, wasn't it? Unfortunately, this doesn't look too much like menu so we extend this example bit more; let's style it into more conventional way (styles/index.styl):
body
font: 100% "Trebuchet MS", sans-serif
.canvas
margin: 8px
.menu
list-style-type none
padding 0px
margin 0px
.menu li
width 64px
height 64px
display inline-block
margin 0 2px
.menu .selected
font-weight bold
So we would like to make menu item bold
when it's selected; in order to achieve this, we first need to know which menu item is selected. This should be extracted to menu component (e.g. src/menu.ts) but for the sake of simplicity, we put it into src/main.ts:
var $ = require("zepto");
import menuModel = module("./menuModel");
import ko = module("knockout");
export class Main {
menuItems: KnockoutObservableArray<menuModel.MenuItem>;
selectedMenu: KnockoutObservable<number>;
constructor() {
console.log("main.init()");
this.menuItems = ko.observableArray(menuModel.items);
this.selectedMenu = ko.observable(menuModel.items[0].id);
}
run() {
console.log("main.run()");
ko.applyBindings(this);
}
selectMenu(id: number) {
this.selectedMenu(id);
}
}
Please note that in order to update value, we need to perform function call instead. This is often source of issues.
Now we know which menu item is selected and we need to bind this information to UI and propagate these changes back to ViewModel
; we will update views/menu.jade to achieve this:
ul.menu(data-bind="foreach: menuItems")
li(data-bind="text: name, css: { selected: $root.selectedMenu() == id },
event: { click: $root.selectMenu.bind($root, id) }")
Again, please note that in order to obtain value, we need to perform function call. Object $root
in this case contains reference to top-level ViewModel
object (in our case instance of class Main
). Since Knockout.js processes events on our behalf context of methods called this will be always within Knockout unless we will bind it to correct context (in this case $root
).
Compile with grunt
and enjoy your menu that was built without doing any event handling or CSS operations!
Back to Part II.