Sunday, May 26, 2013

YUI modules with Typescript

I finally made time this week to play with typescript - a statically typed javascript extension developed at Microsoft. I like Typescript a lot. I prefer working with statically typed code for most applications, and Typescript also improves on javascript's syntax. I also think Typescript's approach to extending javascript while maintaining direct interoperability with existing javascript code (typescript can call javascript code directly, and vice versa) is better than the "rebuild the universe" strategy taken by dart and gwt.

I've enjoyed using yui in the few small javascript modules I've written (tracked in google code), so one of my first typescript tasks was to publish typescript code as a yui module, and to use other yui modules from typescript. I'm new to typescript, so the following explanation may be wrong, but here it goes.

Typescript supports modules via module, export, and import keywords. A typescript module compiles to either an AMD or CommonJS style javascript module depending on a compiler flag (the typescript specification covers all this). So a typescript file like this:


/// <reference path="yui" />
/// <reference path="toyA" />


export module littleware.toy.b {
    
    export import modA = module( "toyA" );

    export class Greeter {
        delegate:modA.littleware.toy.a.Greeter;
    
        constructor(message: string) {
            this.delegate = new modA.littleware.toy.a.Greeter( message );
        }
        greet() {
            return this.delegate.greet();
        }
    }
    
    export function runGreeter( greeter:Greeter ) {
        var button = document.createElement('button');
        button.innerText = "Say Hello";
        button.onclick = function() {
            alert(greeter.greet());
        }
        
        document.body.appendChild(button);
    }

}

Compiles as commonjs like this:


(function (littleware) {
    (function (toy) {
        (function (b) {
            var modA = require("./toyA")
            var Greeter = (function () {
                function Greeter(message) {
                    this.delegate = new modA.littleware.toy.a.Greeter(message);
                }
                Greeter.prototype.greet = function () {
                    return this.delegate.greet();
                };
                return Greeter;
            })();
            b.Greeter = Greeter;            
            function runGreeter(greeter) {
                var button = document.createElement('button');
                button.innerText = "Say Hello";
                button.onclick = function () {
                    alert(greeter.greet());
                };
                document.body.appendChild(button);
            }
            b.runGreeter = runGreeter;
        })(toy.b || (toy.b = {}));
        var b = toy.b;
    })(littleware.toy || (littleware.toy = {}));
    var toy = littleware.toy;
})(exports.littleware || (exports.littleware = {}));
var littleware = exports.littleware;

This is the amd output for the same code:

define(["require", "exports", "toyA"], function(require, exports, __modA__) {
    (function (littleware) {
        (function (toy) {
            (function (b) {
                var modA = __modA__;

                var Greeter = (function () {
                    function Greeter(message) {
                        this.delegate = new modA.littleware.toy.a.Greeter(message);
                    }
                    Greeter.prototype.greet = function () {
                        return this.delegate.greet();
                    };
                    return Greeter;
                })();
                b.Greeter = Greeter;                
                function runGreeter(greeter) {
                    var button = document.createElement('button');
                    button.innerText = "Say Hello";
                    button.onclick = function () {
                        alert(greeter.greet());
                    };
                    document.body.appendChild(button);
                }
                b.runGreeter = runGreeter;
            })(toy.b || (toy.b = {}));
            var b = toy.b;
        })(littleware.toy || (littleware.toy = {}));
        var toy = littleware.toy;
    })(exports.littleware || (exports.littleware = {}));
    var littleware = exports.littleware;
})

The yui module system expects a module to register itself via a call to YUI.add with a closure function that accepts a YUI instance variable "Y" to use as the root of the module namespace. A YUI module for our typescript code could be implemented using the amd definition something like this:


YUI.add('littleware-littleUtil', function(Y) {
    // call the lambda passed as 2nd argument to amd define
    lambda( Y, Y, Y );
}, '0.1.1', { 'requires' : [ 'littleware-toy-a' ] } );

I was able to hack something to incorporate the amd module-definitions output from the typescript compiler into a YUI module system - there are just a few tricks.

The first trick is to introduce our own global define method that connects the amd module definitions with the YUI module system:


function define( argNames, moduleThunk ) {
  var name = null;
  // hacky way to get the YUI module name ...
  try { moduleThunk( null, null ); } catch ( v ) { name = v; }
  YUI.add(name, function(Y) {
    var thunkArgs = [];
    for( var i=0; i < argNames.length; ++i ) {
      thunkArgs.push( Y );
    }
    moduleThunk.apply( Y, thunkArgs );
  }, '0.1.1' );

Second trick - the YUI.add call that registers a module's closure with YUI requires a module name. The hack I came up with is to call into the amd module-definition with null arguments, and add a block of code to the typescript that checks for nulls, and throws a "module name" exception:


declare var exports:Y;

//declare module littleware.toy.a;
if ( null == exports ) {
    // Hook to communicate out to YUI module system a YUI module-name for this typescript file
    throw "littleware-toy-toyB";
}

var Y:Y = exports;

This is a complete hack that exposes the javascript compiler output to the typescript code, but it works. The
var Y:Y = exports;
exposes the YUI instance "Y" to the typescript code. Juan Dopazo posted his node script that generates typescript type-definitions for YUI. I saved the output file he posted as "yui.d.ts", cleaned it up a bit, and included that in my build, so the type-script compiler can perform type-checking on YUI calls. I modified the typescript toy to take advantage of YUI DOM utilities just to verify all this module magic worked:

  var counter = 0;

  export function runGreeter( greeter:Greeter ) {
      var toyNode = Y.one( "div.toy" );
      var messageNode = toyNode.one( "p" );
      var button = document.createElement('button');
      button.innerText = "Say Hello";
      button.onclick = function() {
          messageNode.setHTML( "" + counter + ": " + greeter.greet()  );
          counter += 1;
      }

      toyNode.append(button);
  }

The "YUI.add" module registration includes options for specifying a module's dependencies on other YUI modules, but I prefer to specify that information in the YUI-load configuration that tells YUI which javascript file contains which module. I include that code along with my global "define" amd-hack function in a little bootstrap.js file that I include in the html host file via a <script> tag.

/*
 * Copyright 2011 catdogboy at yahoo.com
 *
 * The contents of this file are subject to the terms of the
 * Lesser GNU General Public License (LGPL) Version 2.1.
 * http://www.gnu.org/licenses/lgpl-2.1.html.
 */

if( window.littleware == undefined ) {
    window.littleware = {};
}

/**
 * littleYUI module, see http://yuiblog.com/blog/2007/06/12/module-pattern/
 * YUI doc comments: http://developer.yahoo.com/yui/yuidoc/
 * YUI extension mechanism: http://developer.yahoo.com/yui/3/yui/#yuiadd
 * Provides convenience method for loading YUI with the littleware extension modules,
 * so client javascript code just invokes:
 *      littleware.littleYUI.bootstrap()... 
 *
 * @module littleware.littleYUI
 * @namespace auburn.library
 */
littleware.littleYUI = (function() {
    
    /**
     * Get the YUI.config groups entry that registers the littleware javascript
     * modules with - YUI( { ..., groups: { littleware: getLittleModules(), ... } )
     * @method getLittleModules
     * @return dictionary ready to add to YUI config's groups dictionary
     */
    var getLittleModules = function() {
        return {
            combine: false,
            base: '/btrack/resources/js/littleware/',
            modules:  { 
                'littleware-littleUtil': {
                    path: "littleUtil.js",
                    requires: [ "array-extras" ]
                },
                'littleware-littleId': {
                    path: "littleId.js",
                    requires: ['anim', 'base', 'node-base', 'node']
                },
                'littleware-littleTree': {
                    path: "littleTree.js",
                    requires: ['anim', 'base', 'node', 'node-base', 'test']
                },
                'littleware-littleMessage': {
                    path: "littleMessage.js",
                    requires: [ 'io-base', 'node', 'node-base', 
                           'littleware-littleUtil', 'test']
                },
                'littleware-feedback-model': {
                    path: "feedback/littleFeedback.js",
                    requires: [ 'node', 'base', 'littleware-littleUtil', 'test']
                },
                'littleware-feedback-view': {
                    path: "feedback/FbWidget.js",
                    requires: [ 'node', 'base', 'littleware-littleUtil', 
                       'littleware-feedback-model', 'test']
                },
                'littleware-toy-toyA': {
                    path: "toy/toyA.js",
                    requires: [ 'node', 'base', 'littleware-littleUtil', 'test']
                },
                'littleware-toy-toyB': {
                    path: "toy/toyB.js",
                    requires: [ 'littleware-toy-toyA']
                }                                                
            }
        }    ;
    };

    
    /**
     * littleYUI - wrapper around YUI3 YUI() method that
     * registers local littleware modules.
     */
    var bootstrap = function () {
        return YUI({
            //lang: 'ko-KR,en-GB,zh-Hant-TW', // languages in order of preference
            //base: '../../build/', // the base path to the YUI install.  Usually not needed because the default is the same base path as the yui.js include file
            //charset: 'utf-8', // specify a charset for inserted nodes, default is utf-8
            //loadOptional: true, // automatically load optional dependencies, default false
            //combine: true, // use the Yahoo! CDN combo service for YUI resources, default is true unless 'base' has been changed
            filter: 'raw', // apply a filter to load the raw or debug version of YUI files
            timeout: 10000, // specify the amount of time to wait for a node to finish loading before aborting
            insertBefore: 'yuiInsertBeforeMe', // The insertion point for new nodes

            // one or more groups of modules which share the same base path and
            // combo service specification.
            groups: {
                // Note, while this is a valid way to load YUI2, 3.1.0 has intrinsic
                // YUI 2 loading built in.  See the examples to learn how to use
                // this feature.
                littleware: getLittleModules()
            }
        });
    };
    return {
        bootstrap: bootstrap,
        getLittleModules: getLittleModules
    };
})();


/**
 * AMB module hook - still needs work ...
 * @param argNames {Array[String]}
 * @param moduleThunk {function(requires,exports,import1, import2, ...)}
 */
function define( argNames, moduleThunk ) {
  var name = null;
  // hacky way to get the YUI module name ...
  try { moduleThunk( null, null ); } catch ( v ) { name = v; }
  YUI.add(name, function(Y) {
    var thunkArgs = [];
    for( var i=0; i < argNames.length; ++i ) {
      thunkArgs.push( Y );
    }
    moduleThunk.apply( Y, thunkArgs );
  }, '0.1.1' );
}

Anyway - that's a typical javascript mess, but it lets me bootstrap a javascript "main" in a new page with code like this:

   littleware.littleYUI.bootstrap().use( 
                'node', 'node-base', 'event', 'test', 'scrollview',
                'littleware-toy-toyB',
                function (Y) {
                    var util = Y.littleware.littleUtil;
                    var log = new util.Logger( "events.html" );
                    
                    log.log( "main() running" );
                    var toy = Y.littleware.toy.b
                    var greeter = new toy.Greeter( "Dude" );
                    toy.runGreeter( greeter );
                }
        );

Hopefully that makes some sense. I pushed the code up to the littleware repo I've been committing to lately. That repo is a mess, but feel free to take a look.

No comments: