Making JSLint AIR: Part 1 - Working with Ext

2007 September 10
tags: AIR · Ext JS · Javascript
by Paul Marcotte
In my introduction to JSLint AIR Edition, I listed the tools and resources used to build the application. In this post, I'll detail my experience with Ext. I chose the Ext library for this project, because it provides components that can be combined to create a complete desktop look and feel and the documentation center has a nice API and plenty of examples and demos for building individual Ext components. This was my first time using Ext for more than a simple form or menu, so combining individual elements to design a complete UI required a bit of forethought and exploration.For the UI, I wanted to use four basic Ext components: BorderLayout, Toolbar (as a menu), Form and BasicDialog. I also wanted to adhere to the principle of separation of concerns by defining each component as an individual object with it's own init() method. The practice of defining a component and initializing it via Ext.onReady () is used throughout the Ext examples and demos. Unfortunately, as a Javascript developer of limited experience, I found much of the code in the examples obtuse. This was due in large part to the fact that my understanding of object creation in javascript was limited to the constructor method. So let's look at some ways of defining an object in Javascript. 1. Constructor method
// constructor function
function Student(name)
{
    this.name = name;
    this.greeting = function () { return 'Hello, my name is ' + this.name + '.';};
}
// instance creation
var student = new Student('George');
student.greeting(); // returns 'Hello, my name is George.'
2. Object Literal method
// instance creation
var student = { name : 'George', greeting : function () {return 'Hello, my name is ' + this.name + '.';}};
student.greeting(); // returns 'Hello, my name is George.'
3. Using a closure
// instance creation
var student = function () {
    var name = 'George';
    return{
        introduce : function () {return 'Hello, my name is ' + name + '.';}
    }
}();

student.introduce(); // returns 'Hello, my name is George.'
Using a closures in this way not only give you the power to create single instances of objects (or Singletons), they also provide a means to define private and public methods and variables for the object instance. The real power of the 'private' variables is, IMHO, that they are essentially global to nested, or callback functions. Thus, allowing one to harness the flexibility of a 'global' scope within the object instance, but without cluttering the true global scope. Why is this meaningful? It promotes writing cohesive, maintainable code. Which, is a desirable trait of object-oriented programming, but is often overlooked in Javascript. When I initially set out to create component objects that could be initialized at application startup, my first take was to create object literals that I would add to an 'Application' object and initialize during the application init() call. It works for a single method, but is pretty limited. To expand on the concept of invoking methods on components from a single application, I took a page out of the broadcast style of method invocation. Back in the day, a popular way to implement event broadcasting in Actionscript 1 was to use the undocumented ASBroadcaster which provided a means for objects to register event listeners and broadcast event messages. This idea was ported to Javascript as JSBroadcaster and it was from here that I aped the code to broadcast events to my application components. Take two of the Application object was to define an object literal, add other object literal components and then broadcast messages to them from the parent application object. The result was still a messy mix of global object references, because I wanted a component object to be able to broadcast events to it's siblings. After a few more laboratory experiments, I finally landed on a solution that I liked. It starts with the following application definition:
var App = function () {
    var components = {};
    return {
        init : function ()
        {
            /*
             * Broadcast 'init' event to initialize all components in the associative array
             */
            this.broadcastEvent('init');
        },
       
        /*
         * Adds a component to the components associative array
         *
         * @param     name {string}    the key reference for the component
         * @param     component {object}    an function literal object          
         */
        addComponent : function (name, component)
        {
            // ensure that component is not already in associative array
            for (var k in components)
            {
                if (components[k] === component)
                {
                    break;
                }   
            }
            component.setApp(this);
            components[name] = component;
        },
       
        /*
         * Removes a component from the associative array
         *
         * @param     name {string}    an function literal object
         *            
         */
        removeComponent : function (name)
        {
            for (var k in components)
            {
                // if component match found, remove it
                if (components[k] === name)
                {
                    delete components[k];
                }
            }
        },
       
        /*
         * Provides a general message broadcast implementation
         * not unlike a public address system.
         *
         * @param    event {string} the event to broadcast
         */
        broadcastEvent : function (event)
        {
            for (var k in components)
            {
                // if component implements the event as a function, call it
                if (typeof components[k][event] === 'function')
                {
                    components[k][event]();
                }
            }       
        }
    };
}();
In the code above, I declare a new global variable, 'App' which has a private variable 'components' which is an object that will hold all components in key : value pairs (otherwise know as an associative array). The declaration returns an object with public methods,init(), addComponent(), removeComponent() and broadcastEvent(). One item to note is that the App will invoke setApp(this) on each component added. Let's look at the component code that defines an Ext BorderLayout.
App.addComponent('Layout', function () {
    var app = null;
    var getApp = function () {
            return app;
        };
    var layout = null;
    return {
        init : function () {
            layout = new Ext.BorderLayout(document.body, {
                north: { split: false, initialSize: 29 },
                center: { titlebar: false, tabPosition: 'top' }
            });
            layout.beginUpdate();
            layout.add('north', new Ext.ContentPanel('north-div', { fitToFrame: true, closable: false }));
            layout.add('center', new Ext.ContentPanel('center-form', { fitToFrame: false, title: 'JSLint', closable: false }));
            layout.add('center', new Ext.ContentPanel('center-report', { fitToFrame: false, title: 'Report', closable: false }));
            layout.add('center', new Ext.ContentPanel('center-listing', { fitToFrame: false, title: 'Listing', closable: false }));
               layout.getRegion('center').showPanel('center-form');
            layout.endUpdate();       
        },
              
        onVerify : function () {
            layout.beginUpdate();
            layout.getRegion('center').showPanel('center-report');
            layout.endUpdate();
        },
       
        onOpenFile : function ()
        {
            layout.beginUpdate();
            layout.getRegion('center').showPanel('center-form');
            layout.endUpdate();
        },
       
        onFixWhitespace : function ()
        {
            layout.beginUpdate();
            layout.getRegion('center').showPanel('center-form');
            layout.endUpdate();
        },

        setApp : function (a)
        {
            app = a;
        }
    };
}());
The code for the component above sets the key 'Layout' and defines the object's private and public variables and methods. The public method setApp(a) sets the private variable, app, which can then be accessed at any time via the private method getApp(); The methods onSaveFile() and onVerify() are event listeners like init(). What I like about defining the component object as way is that it is not placed in global scope. Notice that there is no reference to getApp() within the 'Layout' component. A simple update to both code examples would be to add a private variable and accessor method in the App definition
var baseLayoutElement = document.body;

return {
    ...
    getBaseLayoutElement : function ()
    {
        return baseLayoutElement;
    }
    ...
}
and change
layout = new Ext.BorderLayout(document.body...
to
layout = new Ext.BorderLayout(getApp().getBaseLayoutElement()...
Therefore 'App' can act as a 'model' for the components. The benefit of using this design is that I was able to refactor my original code dramatically to reduce global references and repetition in the components and focus the code toward individual concerns. For example, the 'Menu' component defines only the toolbar menu and a menuHandler. The menuHandler then broadcasts events through App (with the exception of exit, which I haven't figured out how to gracefully relocate).
App.addComponent('Menu', function () {
    var app = null;
    var getApp = function () {
        return app;
    };
    var tb = null;   
    return {
        init : function () {
            tb = new Ext.Toolbar('toolbar', [
                {
                    text: 'File',
                    menu: {
                        id: 'fileMenu',
                        items: [                           
                            {
                                text: 'Open',
                                handler: menuHandler
                            },
                            {
                                text: 'Save',
                                id: 'saveItem',
                                handler: menuHandler
                            },
                            '-',
                            {
                                text: 'Exit',
                                handler: menuHandler
                            }
                        ]
                    }
                },
                {
                    text: 'Options',
                    menu: {
                        id: 'optionsMenu',
                        items: [
                            {
                                text: 'Clear All',
                                handler: menuHandler
                            },
                            {
                                text: 'Recommended',
                                handler: menuHandler
                            },
                            {
                                text: 'Good Parts',
                                handler: menuHandler
                            }
                        ]
                    }
                },
                {
                    text: 'Tools',
                    menu: {
                        id: 'toolsMenu',
                        items: [
                            {
                                text: 'Change tabs to spaces',
                                handler: menuHandler
                            }
                        ]
                    }
                },
                {
                    text: 'About',
                    handler: menuHandler
                }      
            ]);

            function menuHandler(item, e)
            {
                switch (item.text) {
                case 'Open' :
                    getApp().broadcastEvent('onOpenFile');
                    break;
                case 'Save' :
                    getApp().broadcastEvent('onSaveFile');
                    break;
                case 'Exit' :
                    exitApp();
                    break;
                case 'About' :
                    getApp().broadcastEvent('onShowAbout');
                    break;
                case 'Clear All' :
                    getApp().broadcastEvent('onClearAll');
                    break;
                case 'Recommended' :
                    getApp().broadcastEvent('onRecommended');
                    break;
                case 'Good Parts' :
                    getApp().broadcastEvent('onGoodParts');
                    break;
                case 'Change tabs to spaces' :
                    getApp().broadcastEvent('onFixWhitespace');
                    break;
                }
            }

            function exitApp()
            {
                air.Shell.shell.exit();
            }

        },

        setApp : function (a)
        {
            app = a;
        }
    };
}());
The code for each component is separated into individual scripts. Within the tag of my html document, is the following: <script type="text/javascript" src="lib/ext-1.1/adapter/ext/ext-base.js"></script>
<script type="text/javascript" src="lib/ext-1.1/ext-all.js"></script>
<script type="text/javascript" src="js/Application.js">&amp;lt;/script>
<script type="text/javascript" src="js/components/Layout.js"></script>
<script type="text/javascript" src="js/components/Menu.js"></script>
<script type="text/javascript" src="js/components/Form.js"></script>
<script type="text/javascript" src="js/components/Report.js"></script>
<script type="text/javascript" src="js/components/DialogManager.js"></script>
<script type="text/javascript" src="js/components/LinkManager.js"></script>
<script language="javascript" type="text/javascript">
Ext.onReady(function(){
App.init();
});
</script>
The names of the component files hint at the Ext components therein. Layout.js defines the BorderLayout, Menu.js the Toolbar menu, Form.js defines the JSLint form and so forth... In Part 2, I'll write about working with the AIR API.