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 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 have to expose it's components via export keyword instead of providing global variables. Although this is becoming standard way how to distribute JS libraries there are still libraries that does it in 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 project from last time where 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 latter one where we benefit from fact that Javascript code is fully valid in Typescript files. We will update src/config.ts to following:
/// <reference path='../extern/require.d.ts'/>
"use strict";
requirejs.config({
paths: {
"zepto": "zepto.min",
"knockout": "knockout-2.3.0"
},
shim: {
zepto: {
exports: "$"
}
}
});
// Stage one
require(['zepto', 'knockout'], (zepto, knockout) => {
// Stage two
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 following:
/// <reference path='../extern/zepto.d.ts'/>
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 real work. In this chapter we take a look on 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 try 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 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 following:
/// <reference path='../extern/knockout.amd.d.ts'/>
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 sake of simplicity we put it into src/main.ts:
/// <reference path='../extern/knockout.amd.d.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 this 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! As usual you can find zipped archive with complete example here.
Back to Part II.
Žiadne komentáre:
Zverejnenie komentára