- Updated type definitions to correct various function prototypes

- updated README.md with a more efficient way to integrate Dojo and TypeScripts' class systems
This commit is contained in:
Mike Van Sickle
2014-12-10 15:56:45 -05:00
parent 0e84156577
commit d6fa105a4f
4 changed files with 61 additions and 241 deletions
+45 -228
View File
@@ -24,12 +24,11 @@ A normal dojo module might look something like this:
When using the TypeScript, you can write the following:
```ts
define(['dojo/request', 'dojo/request/xhr'],
function (request: dojo.request,
xhr: dojo.request.xhr) {
...
}
);
import request = require("dojo/request");
import xhr = require("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.
@@ -68,231 +67,49 @@ When using the TypeScript, you can write the following:
The first challenge that we run into is how to define the class. We will define that using standard TypeScript semantics as follows:
```ts
module App {
export class Foo extends dijit._WidgetBase implements dijit._TemplatedMixin {
constructor(public templateString= "<div>Hello TypeScript</div>",
public message= "") {
super();
}
import dojoDeclare = require("dojo/_base/declare");
import _WidgetBase = require("dijit/_WidgetBase");
import _TemplatedMixin = require("dijit/_TemplatedMixin");
sayMessage() {
alert(this.message);
}
import template = require("dojo/text!./_templates/View.html");
getServerInfo() {
request.get("http://dojoAndTypeScriptTogetherAtLast.html", (data: string) => {
console.log(data);
});
}
class Foo extends dijit._WidgetBase {
message: String = "";
constructor(args?: Object, element?: HTMLElement) {
return new FooType(args, element);
}
}
sayMessage() {
alert(this.message);
}
getServerInfo() {
request.get('http://dojoAndTypeScriptTogetherAtLast.html', function(data) {
console.log(data);
});
}
}
var FooType = dojoDeclare("", <Function[]>[_WidgetBase, _TemplatedMixin], {
templateString: '<div>Hello TypeScript</div',
sayMessage: Foo.prototype.sayMessage,
getServerInfo: Foo.prototype.getServerInfo
});
export =Foo;
```
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:
1. `Foo` has an error because it doesn't honor the interface declared by dijit._TemplatedMixin
2. `request` is undefined
In short, we create the class using the standard TypeScript methodology. The constructor, howevever, defers to
Dojo-style class declaration (i.e. FooType) that is created afterward. Conversely, the Dojo-style class pulls
the implementation of its methods from the prototype of the TypeScript class. It is a little
cumbersome to have to do this, but I haven't been able to find another way to satisfy both TypeScript and
Dojo's class systems otherwise. If you find a better way, feel free to let me know!
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:
```ts
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:
```ts
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:
```js
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`:
```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:
```js
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:
```ts
"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`.
## Appendix
Examples:
* https://github.com/craigstjean/typescript-dojo-sample
+11 -8
View File
@@ -1,3 +1,6 @@
declare function define(dependencies: String[], factory: Function): any;
declare function require(config?:Object, dependencies?: String[], callback?: Function): any;
declare module dojox.dtl {
interface __StringArgs { }
interface __ObjectArgs { }
@@ -20,28 +23,28 @@ declare module dojo {
* @param url URL to request
* @param options OptionalOptions for the request.
*/
del(url: String, options: dojo.request.__BaseOptions): dojo.request.__Promise;
del(url: String, options?: dojo.request.__BaseOptions): dojo.request.__Promise;
/**
* Send an HTTP GET request using the default transport for the current platform.
*
* @param url URL to request
* @param options OptionalOptions for the request.
*/
get(url: String, options: dojo.request.__BaseOptions): dojo.request.__Promise;
get(url: String, options?: dojo.request.__BaseOptions): dojo.request.__Promise;
/**
* Send an HTTP POST request using the default transport for the current platform.
*
* @param url URL to request
* @param options OptionalOptions for the request.
*/
post(url: String, options: dojo.request.__BaseOptions): any;
post(url: String, options?: dojo.request.__BaseOptions): any;
/**
* Send an HTTP POST request using the default transport for the current platform.
*
* @param url URL to request
* @param options OptionalOptions for the request.
*/
put(url: String, options: dojo.request.__BaseOptions): dojo.request.__Promise;
put(url: String, options?: dojo.request.__BaseOptions): dojo.request.__Promise;
}
module request {
@@ -581,7 +584,7 @@ declare module dojo {
*
*/
"checkString": string;
/**
/**dojo
* Data to transfer. This is ignored for GET and DELETE
* requests.
*
@@ -980,7 +983,7 @@ declare module dojo {
* @param errback OptionalCallback to be invoked when the promise is rejected.Receives the rejection error.
* @param progback OptionalCallback to be invoked when the promise emits a progressupdate. Receives the progress update.
*/
then(callback: Function, errback: Function, progback: Function): dojo.promise.Promise;
then(callback?: Function, errback?: Function, progback?: Function): dojo.promise.Promise;
/**
*
*/
@@ -3196,7 +3199,7 @@ declare module dojo {
* @param superclass May be null, a Function, or an Array of Functions. This argumentspecifies a list of bases (the left-most one is the most deepestbase).
* @param props An object whose properties are copied to the created prototype.Add an instance-initialization function by making it a propertynamed "constructor".
*/
interface declare{(className?: String, superclass?: Function, props?: Object): void}
interface declare { (className?: String, superclass?: Function, props?: Object): any}
/**
* Permalink: http://dojotoolkit.org/api/1.9/dojo/_base/declare.html
*
@@ -3271,7 +3274,7 @@ declare module dojo {
* @param superclass May be null, a Function, or an Array of Functions. This argumentspecifies a list of bases (the left-most one is the most deepestbase).
* @param props An object whose properties are copied to the created prototype.Add an instance-initialization function by making it a propertynamed "constructor".
*/
interface declare{(className?: String, superclass?: Function[], props?: Object): void}
interface declare{(className?: String, superclass?: Function[], props?: Object): any}
interface declare {
/**
* Mix in properties skipping a constructor and decorating functions
+4 -4
View File
@@ -22103,19 +22103,19 @@ declare module "dojox/dgauges/components/classic/SemiCircularLinearGauge" {
export=exp;
}
declare module "dojox/dgauges/components/default/CircularLinearGauge" {
var exp: dojox.dgauges.components.default.CircularLinearGauge
var exp: dojox.dgauges.components.default_.CircularLinearGauge
export=exp;
}
declare module "dojox/dgauges/components/default/HorizontalLinearGauge" {
var exp: dojox.dgauges.components.default.HorizontalLinearGauge
var exp: dojox.dgauges.components.default_.HorizontalLinearGauge
export=exp;
}
declare module "dojox/dgauges/components/default/SemiCircularLinearGauge" {
var exp: dojox.dgauges.components.default.SemiCircularLinearGauge
var exp: dojox.dgauges.components.default_.SemiCircularLinearGauge
export=exp;
}
declare module "dojox/dgauges/components/default/VerticalLinearGauge" {
var exp: dojox.dgauges.components.default.VerticalLinearGauge
var exp: dojox.dgauges.components.default_.VerticalLinearGauge
export=exp;
}
declare module "dojox/dgauges/components/green/HorizontalLinearGauge" {
+1 -1
View File
@@ -1023,6 +1023,6 @@ declare module "dojox/embed/flashVars" {
export=exp;
}
declare module "dojox/embed/Object" {
var exp: dojox.embed.Object
var exp: dojox.embed.Object_
export=exp;
}