Anyone that has used Dojo for any length of time has probably discovered three things:
- Dojo is very powerful
- Dojo can be challenging to learn
- Dojo doesn't always play well with other
Having said that, there are ways that Dojo can be coerced out of its shell to work with other JavaScript technologies. This README is intended to describe some techniques for getting the full power of Dojo to work in an environment where almost everything can take advantage of TypeScript.
Disclaimer: Dojo is VERY big framework and, as such the type definitions are generated by a tool from dojo's API docs. The generated files were then hand-polished to eliminate any import errors and clean up some obvious errors. This is all to say that the generated type definitions are not flawless and are not guaranteed to reflect the actual implementations.
A normal dojo module might look something like this:
define(['dojo/request', 'dojo/request/xhr'],
function (request, xhr) {
...
}
);
When using the TypeScript, you can write the following:
define(['dojo/request', 'dojo/request/xhr'],
function (request: dojo.request,
xhr: dojo.request.xhr) {
...
}
);
Inside of the define variable, both request
and xhr
will work as the functions that come from Dojo, only they are strongly typed.
Dojo and TypeScript both use different and conflicting class semantics. This causes some issues when trying to create custom class modules that are strongly typed in other modules. The following technique is presented as A solution to the problem, but not necessarily the best one. Other ideas a welcomed!
Using pure JavaScript, a class that has a base class and mixins can be defined in Dojo as follows:
define(['dojo/_base/declare', 'dijit/_WidgetBase', 'dijit/_TemplatedMixin', 'dojo/request'],
function(dojoDeclare, _WidgetBase, _TemplatedMixin, request) {
var Foo = dojoDeclare([_WidgetBase, _TemplatedMixin], {
templateString: '<div>Hello TypeScript</div',
message: '',
sayMessage: function() {
alert(this.message);
},
getServerInfo: function() {
request.get('http://dojoAndTypeScriptTogetherAtLast.html', function(data) {
console.log(data);
});
}
});
return Foo;
}
);
The goal is to be able to describe the Foo
class in a way that TypeScript can recognize, but also works with Dojo. This requires some hacks that will be introduced and explained as the problems are discovered.
The first challenge that we run into is how to define the class. We will define that using standard TypeScript semantics as follows:
module App {
export class Foo extends dijit._WidgetBase implements dijit._TemplatedMixin {
constructor(public templateString= "<div>Hello TypeScript</div>",
public message= "") {
super();
}
sayMessage() {
alert(this.message);
}
getServerInfo() {
request.get("http://dojoAndTypeScriptTogetherAtLast.html", (data: string) => {
console.log(data);
});
}
}
}
This class is identical to the standard Dojo method, except that it is declared inside of a TypeScript module and it is declared using TypeScript instead of Dojo's declare
method. Two problems arise however:
Foo
has an error because it doesn't honor the interface declared by dijit._TemplatedMixinrequest
is undefined
The first problem can be solved by adding the missing properties and methods, but this will only serve to clutter the code base over time. Instead, we are creating another base class that hides this requirement like so:
module App {
export class Foo extends WidgetBaseWithTemplatedMixin {
constructor(public templateString= "<div>Hello TypeScript</div>",
public message= "") {
super();
}
sayMessage() {
alert(this.message);
}
getServerInfo() {
request.get("http://dojoAndTypeScriptTogetherAtLast.html", (data: string) => {
console.log(data);
});
}
}
export class WidgetBaseWithTemplatedMixin extends dijit._WidgetBase implements dijit._TemplatedMixin {
"attachScope": Object;
"searchContainerNode": boolean;
"templatePath": string;
"templateString": string;
buildRendering(): {}
destroyRendering(): {}
getCachedTemplate(templateString: String, alwaysUseString: boolean, doc: HTMLDocument): {}
}
}
Now the base class meets TypeScript's requirements so it is happy. This class could easily be moved out to a general add-in file so that it can be created and forgotten since it is only here to make TypeScript happy.
The second problem that we had as that request
is undefined. This is going to take a bit more trickery as shown below:
module App {
export class Foo extends WidgetBaseWithTemplatedMixin {
constructor(public templateString= "<div>Hello TypeScript</div>",
public message= "") {
super();
}
public request: dojo.request;
sayMessage() {
alert(this.message);
}
getServerInfo() {
this.request.get("http://dojoAndTypeScriptTogetherAtLast.html").then((data: string) => {
console.log(data);
});
}
}
export class WidgetBaseWithTemplatedMixin extends dijit._WidgetBase implements dijit._TemplatedMixin {
public static getPrototype(deps: Object) {
if (deps) {
for (var i in deps) {
this.prototype[i] = deps[i];
}
return this.prototype;
}
}
"attachScope": Object;
"searchContainerNode": boolean;
"templatePath": string;
"templateString": string;
buildRendering(): {}
destroyRendering(): {}
getCachedTemplate(templateString: String, alwaysUseString: boolean, doc: HTMLDocument): {}
}
}
define(['dojo/_base/declare', 'dijit/_WidgetBase', 'dijit/_TemplatedMixin', 'dojo/request'],
function (dojoDeclare, _WidgetBase, _TemplatedMixin, request) {
var deps = {
request: request
};
var Foo = dojoDeclare([_WidgetBase, _TemplatedMixin], App.Foo.getPrototype(deps));
return Foo;
}
);
Yes, I know - pretty crazy right. But, we're getting close...
In the Dojo module, we are building an object that contains references to each of the dependencies. We are then passing that object into the static method getPrototype
that we have added to the base class. This method takes an object literal and mixes it into the class's prototype. In this way, the module dependencies are made available to the TypeScript class via its prototype. The last thing we need to do is change the getServerInfo()
's call to request
to be a this.request
call since it is calling through its prototype instead of the ambient object that is used in Dojo.
Okay, great. TypeScript is happy. Everything should be working right? Wrong.
We have two more problems that are not apparent until the code is actually executed. They are both related to our usage of the extends
keyword that we used to show that our Foo
class extends from dijit._WidgetBase
.
The first problem is that, as stated previously, TypeScript has its own implementation of a class system in JavaScript. When one class extends another, TypeScript injects the following snippet into the module:
var __extends = this.__extends || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
__.prototype = b.prototype;
d.prototype = new __();
};
This method is called in a closure that wraps the class definition and mixes the parent's prototype and owned properties into the child class. However, this won't work in our case, because our base class is dijit._WidgetBase
which doesn't actually exist in the global namespace (where TypeScript expects it). This is because we are still using Dojo's class system (via declare
). This is an important, and confusing, point. Our class is actually being constructed by Dojo using declare. However, we are working with the class as if it was created in the way the TypeScript expects. In short, this means that we don't actually need the __extends
function to work, but something needs to be there so that the constructor function doesn't die. The solve is actually relatively easy: In the main HTML page, add this function before the tag that includes dojo.js
:
var __extends = function (d, b) {
if (d && b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() {
this.constructor = d;
}
__.prototype = b.prototype;
d.prototype = new __();
}
};
All this does is check to see if d
and b
are defined before running. Since TypeScript won't override __extends, it will allow us to override the default implementation.
Okay, only one more thing to deal with: the call to super. This issue is also related to TypeScript's method for handling inheritance. After calling __extends
, the generated constructor function will call the parent's constructor function. Once again, we are hit by the fact that our base class (dijit._WidgetBase
) doesn't actually exist where TypeScript is expecting it. The only way around this is to give TypeScript something to call. This simplest thing to do is to add a no-op function for TypeScript to call. In short add this:
var dijit = dijit || {};
dijit._WidgetBase = function() {}
Into the page after Dojo bootstraps, but before our module loads. The simplest way to do this is to create a little module that does this and added it to the array of modules loaded in the define()
call of the module.
Okay, so things look pretty messy right now. There are several hacks and tricks that we have to play in order to allow TypeScript and Dojo to work together. The nice thing about most of this is that it can all be shoved into a single helper module and never thought of again. Here is an example of what that module would look like:
"use strict";
define([], function () { });
var __extends = function (d, b) {
if (d && b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() {
this.constructor = d;
}
__.prototype = b.prototype;
d.prototype = new __();
}
};
window['dojo'] = {};
window['dijit'] = {
_WidgetBase: function () {
}
};
module Base {
function getPrototype(type: Function, deps: Object): Object {
if (deps) {
for (var i in deps) {
type.prototype[i] = deps[i];
}
return this.prototype;
}
}
export class WidgetBaseWithTemplatedMixin extends dijit._WidgetBase implements dijit._TemplatedMixin {
public static getPrototype(deps: Object): Object {
return getPrototype(this, deps);
}
"attachScope": Object;
"searchContainerNode": boolean;
"templatePath": string;
"templateString": string;
buildRendering() { }
destroyRendering() { }
getCachedTemplate(templateString: String, alwaysUseString: boolean, doc: HTMLDocument) { }
}
}
This module can then be added to whenever we have another base class / mix-in combination (e.g. dijit/_WidgetBase, dijit/_TemplatedMixin, and dijit/_WidgetsInTemplateMixin). When done this way, the only regularly visible changes that we have to do is to compose the hash of dependencies and call the getPrototype
as the last argument to declare
.
Examples: