Fornt-end with Knockout.js, require.js and TypeScript

Let’s talk about how to correctly organize front-end with Knockout.js require.js and TypeScript.

The problem

If we will read TypeScript handbook we will find a lot information about how to load modules with AMD and require.js, and everywhere in samples we will find something like this

[code lang=javascript]
import module=require('./module');
[/code]

But in real application we always have some folder structure to keep the files organized, we are using different package managers and so on, thus in most cases import should look like

[code lang=javascript]
import ko=require('./node_modules/knockout/build/output/knockout-latest')
[/code]

Unfortunately for some unknown reason this is not working with TypeScript, at least with versions 1.3 and 1.4. Really, why current folder path is working but more complex path is not? We have to deal somehow with this.

An the only way is to use import ko=require(knockout) instead of full path to knockout.

In this post I will describe the way how to build HTML application with MS VisualStudio and I will use node package manger to load all the libraries, but the same idea will work for nuget or any other package managed and IDE.

Application structure

  • node_modules
    • knockout
      • build
        • output
          • knockout-latest.js
    • requirejs
      • require.js
    • moment
      • moment.js
  • typings
    • knockout
      • knockout.d.ts
    • moment
      • moment.d.ts
  • config.js
  • application.ts
  • mainViewmodel.ts
  • bindings.ts
  • index.html

Require.js enabled JavaScript (or TypeScript) application should start with single “ tag in html. In our case it looks like:

[code lang=html]
<script data-main='config.js' src='node_modules/requirejs/require.js'></script>
[/code]

This config.js is the only JavaScript file, all other logic is done in TypeScript. May be there is some way to write it on TypeScript, but I’m not sure that it makes any sense, because you have to do JS specific low level things here. The config.js looks like following:

[code lang=javascript]
require.config({
baseUrl: "",
paths: {
knockout: "./node_modules/knockout/build/output/knockout-latest",
moment: "./node_modules/moment/moment"
}
});

define(["require", "exports", 'application'], function (require, exports, app) {
app.Application.instance = new app.Application();
});
[/code]

First of all in this file we are configuring require.js to make it understand where to search for libraries. We will load our index.html from file system and of course in real app you should not use folder structure but think about URLs. Please note that you should not specify file extension.

Now require.js will understand how to load knockout. But this will tell nothing our TypeScript compiler and compiler will report errors about undefined module.

To fix this problem with compiler simply add corresponding typings to the project. Now TypeScript will build everything without errors. Please note that in this case TypeScript will not verify correctness of path to modules because it can’t determine the real URL structure of the application. That may be the reason why complex path is not working in import.

Note: don’t forget to switch TypeScript module type to AMD (Asynchronous Module Definition). This will conflict with node.js and next time I will explain how to deal with node.js and AMD.

Application startup

Our application entry point (after config.js) is application.ts file with following content:

[code lang=javascript]
import vm = require('mainViewModel');
import ko = require('knockout');
import bindings = require('bindings');

export class Application{
public static instance: Application;

constructor(){
bindings.register();
ko.applyBindings(new vm.mainViewModel());
}
}
[/code]

Here we load module(s) (as dependency) with all custom bindings, create main view model and apply it to whole page.

Note that we don’t need to specify path to bindings and mainViewModel in config.js because they are located at the same directory. You can use more complex structure and everything will work with TypeScript just don’t forget to explain require.js how to find all your modules.

Custom bindings

Custom binding are wrapped in single module and can be loaded as any other module. Binding handlers will be registered with bindings.register() call. This can be done with following content of bindings.ts:

[code lang=javascript]
import ko = require("knockout")
import moment = require("moment")

export function register(): void {

ko.expressionRewriting["_twoWayBindings"].datevalue = true;

var formatValue = function (value, format) {
format = ko.unwrap(format);
if (format == null) {
format = "DD.MM.YYYY";
}
return moment(ko.unwrap(value).toString()).format(format);
}

ko.bindingHandlers["datevalue"] = {
init: function (element: HTMLInputElement, valueAccessor, allBindings, viewModel) {
element.value = formatValue(valueAccessor(), allBindings.get("dateFormat"));

element.addEventListener("change", function (event) {
var dateValue: any
= moment(element.value, ko.unwrap(allBindings.get("dateFormat")))
.format("YYYY-MM-DD") + "T00:00:00";

if (ko.unwrap(valueAccessor()) instanceof Date) {
dateValue = new Date(dateValue);
}

if (ko.isObservable(valueAccessor())) {
valueAccessor()(dateValue);
}
else {
allBindings()._ko_property_writers.datevalue(dateValue);
}
});
},
update: function (element: HTMLInputElement, valueAccessor, allBindings) {
element.value = formatValue(valueAccessor(), allBindings.get("dateFormat"));
}
}
}
[/code]

Here we create very useful datevalue binding, which allows to edit and display dates as string in specific format. This binding is able to work with observables and flat values and store date in JSON compatible format or Date, depending on initial value of bound property. This binding contains some knockout and TypeScript tricks like ko.expressionRewriting["_twoWayBindings"].datevalue = true and allBindings()._ko_property_writers.datevalue(dateValue) but let’s talk in next blog posts about these tricks.

ViewModel

Nothing special just usual view model organized as module

[code lang=javascript]
import ko = require('knockout');

export class mainViewModel{

constructor(){
}

public name = ko.observable("John Doe");
public birthday = ko.observable("1983-01-01");
}
[/code]

Conclusion

Everybody are waiting for ECMASript 6 support in all browsers with all sweet things like classes, arrows, modules and so on. Life is too short to wait – let’s use TypeScript today! I’ve tested it in big project and yes sometime it looks a little raw but it’s working and make our life easier with type check and better intellisense.