Archive for 'JavaScript'

Just to announce although a bit late for April, but I’ve recently wrote two articles for JSMag. I highly recommend JSMag to any JavaScript developers who may be reading this.

My first article in April details with using on-demand JavaScript loaded with ExtJS - http://www.jsmag.com/main.issues.description/id=31/

The second article, for May details implementing client-side caching of JavaScript - http://www.jsmag.com/main.issues.description/id=32/

I had some spare time this evening so I started to play around and wrote my own client side JavaScript object cache for use with ExtJS. It’s fairly simple to use with get/set functions to call and works in terms of seconds, for example to cache a simple JavaScript object you can do:

Ext.ux.Cache.set("myJsonData",{test: 1234},10);

This would cache it for ten seconds before having it removed. I’ve even implemented some events. To be notified when items expire for example:

Ext.ux.Cache.on("expired",function(cache,removed) {
  alert(removed.length + " items expired.");
});

This comes into its own with the likes of AJAX requests, one test scenario:

function dataSuccess(data) {
    ...
};

if (Ext.ux.Cache.has("data")) {
    // Process cached data
    dataSuccess(Ext.ux.Cache.get("data"));
} else {
    // Request new data
    Ext.Ajax.request({
        url: "whatever.ashx",
        scope: this,
        success: function(response,options) {
            // Decode data
            var data = Ext.decode(response.responseText);

            // Cache data for 30 seconds
            Ext.ux.Cache.set("data",data,30);

            // Process data
            dataSuccess(data);
        }
    });
}

Anyhow enough of the examples here’s the actual code for you to play with:

Ext.ux.CacheEngine = Ext.extend(Ext.util.Observable,{

    cache: [],

    constructor: function(config) {

        this.addEvents({
            "added": true,
            "updated": true,
            "removed": true,
            "expired": true
        });

        Ext.apply(this,config);

        var cache_task = {
            run: function() {
                // Get current time
                var now = new Date().getTime();

                // Process cache items to remove
                var to_remove = [];

                for(var i = 0, len = this.cache.length; i < len; i++) {
                    // Get cached item
                    var item = this.cache[i];

                    // Check time
                    if (item.expires < now) to_remove.push(item);
                }

                // Remove items
                var removed = [];

                for(var i = 0, len = to_remove.length; i < len; i++) {
                    var item = to_remove[i];
                    var removed_item = {
                        key: item.key,
                        value: item.value
                    };

                    removed.push(removed_item);
                    this.cache.remove(item);
                }

                // Fire event
                if (removed.length > 0) this.fireEvent("expired",this,removed);
            },
            interval: 1000,
            scope: this
        };

        Ext.TaskMgr.start(cache_task);

        // Call our superclass constructor to complete construction process.
        Ext.ux.CacheEngine.superclass.constructor.call(config)
    },

    clear: function()
    {
        this.cache = [];
    },

    get: function(key)
    {
        // Get item
        for(var i = 0, len = this.cache.length; i < len; i++) {
            var item = this.cache[i];

            if (key == item.key) return item.value;
        }

        // Return
        return null;
    },

    set: function(key,value,timeout)
    {
        timeout = timeout || 10;

        // Find item
        for(var i = 0, len = this.cache.length; i < len; i++) {
            var item = this.cache[i];

            if (key == item.key) {
                // Update item
                item.value = value;
                item.expires = new Date().getTime() + (timeout * 1000);

                // Fire event
                this.fireEvent("updated",this);

                // Return
                return;
            }
        }

        // Add new item
        var item = {
            key: key,
            value: value,
            expires: new Date().getTime() + (timeout * 1000)
        };
        this.cache.push(item);

        // Fire event
        this.fireEvent("added",this);
    },

    remove: function(key)
    {
        // Find item
        for(var i = 0, len = this.cache.length; i < len; i++) {
            var item = this.cache[i];

            if (key == item.key) {
                // Remove item
                this.cache.remove(item);

                // Fire event
                this.fireEvent("removed",this);

                // Return
                return true;
            }
        }

        // Return
        return false;
    },

    has: function(key)
    {
        // Look for key
        for(var i = 0, len = this.cache.length; i < len; i++) {
            // Get item
            var item = this.cache[i];

            // Compare keys
            if (key == item.key) return true;
        }

        // Return
        return false;
    }

});

Ext.ux.Cache = new Ext.ux.CacheEngine();

I’m going to attempt to create a cached DataProxy and DataReader at some point, that should provide a more useful feature for ExtJS users so they can plug it in directly into existing code.

Bored this evening I decided to tinker with Aptana Studio as it has a nice plugin to create Adobe AIR applications. Using this I combined it with ExtJS to produce an AIR application of the documentation which saves you having to use the temperamental ExtJS servers (at the moment) or browse locally, and is also very snappy. The build I’ve done is for ExtJS 3.1 though I might fiddle and see if I can get one for 2.x and one for core going later.



You can download the AIR application here.
If the above link does not work, try this one.

I haven’t posted in quite a while so I thought I’d pass up a quick alteration to the standard Ext.MessageBox.

Now the original Ext.MessageBox works well however a lot of my windows have buttons with images, including the buttons like “OK” and “Cancel”. In order to keep with the look and feel I decided to add this functionality to ExtJS. Why they haven’t done this already is beyond me, the change was relatively simple.

You will need to patch the Ext.MessageBox code, there is a function called updateButtons you will need to change like so:

var updateButtons = function(b){
        var width = 0,
            cfg;
        if(!b){
            Ext.each(buttonNames, function(name){
                buttons[name].hide();
            });
            return width;
        }
        dlg.footer.dom.style.display = '';
        Ext.iterate(buttons, function(name, btn){
            cfg = b[name];
            if(cfg){
                btn.show();

                if (Ext.isObject(cfg)) {
                    if (Ext.isString(cfg.icon)) btn.setIconClass(cfg.icon);

                    btn.setText(Ext.isString(cfg.text) ? cfg.text : Ext.MessageBox.buttonText[name]);
                } else {
                    btn.setText(Ext.isString(cfg) ? cfg : Ext.MessageBox.buttonText[name]);
                }

                width += btn.getEl().getWidth() + 15;
            }else{
                btn.hide();
            }
        });
        return width;
    };

And that’s basically it. You can then do this:

Ext.MessageBox.show({
    ...
    buttons: {
        ok: {
            text: "Whatever",
            icon: "whatever-css-class"
        }
    },
    ...
});

To prove it works here is two screenshots of before and after the fix:

Before After



It should be completely backwards compatible, all your original Ext.MessgeBox code will work as originally intended with no modification.

If you don’t want to patch ExtJS directly I’ve ripped the Ext.MessageBox code completely and formed a new variant called Ext.ux.MessageBox which you can use instead. You can get the code to this here.

One of the things I like in .NET is the StringBuilder class and since like .NET, JavaScript strings are immutable it makes sense to have one for JavaScript too. I’ve wrote a simple approximation that uses Array and Array.join().

I’ve built this version for use with Ext however you can easily adapt it to run outside of Ext:

Ext.namespace("Ext.util");

Ext.util.StringBuilder = function()
{

    var buffer = [];
    var length = 0;

    this.getLength = function()
    {
        return length;
    };

    this.clear = function()
    {
        buffer = [];
        length = 0;
    };

    this.append = function(s)
    {
        if (s == null) return;

        length += s.length;
        buffer.push(s);
    };

    this.appendLine = function(s)
    {
        if (s == null) return;

        var _s = s + "\r\n";

        length += _s.length;
        buffer.push(_s);
    };

    this.toString = function()
    {
        return buffer.join("");
    };

}

You can use it like this:

var sb = new Ext.util.StringBuilder();

sb.append("Hello");
sb.append(" ");
sb.append("World");

var s = sb.toString();

alert(s);

One of my annoyances with Ext is sometimes some of the widgets only half do what you want and only half support features you’d expect.

Take the TreeLoader, responsible for loading nodes into a Tree. It works well most of the time and I don’t usually need to complain about it, until today. I needed to increase the time-out for the request only the TreeLoader exposes just two configuration options for the actual request, a requestMethod so you can switch between GET and POST and the actual url. Reading the documentation and looking through Firebug there seemed no way of doing what I wanted so I had to delve into the actual source code.

It turns out yet again I need to override a core function of the component, requestData and alter the AJAX request myself. Rather than do a complete override which would affect ALL TreeLoader’s I instead extended it, the full code can be seen below:

    DCStorm.IQ.Profiles.TreeLoader = Ext.extend(Ext.tree.TreeLoader,
    {

        requestData : function(node, callback, scope){
            if(this.fireEvent("beforeload", this, node, callback) !== false){
                if(this.directFn){
                    var args = this.getParams(node);
                    args.push(this.processDirectResponse.createDelegate(this, [{callback: callback, node: node, scope: scope}], true));
                    this.directFn.apply(window, args);
                }else{
                    this.transId = Ext.Ajax.request({
                        method:this.requestMethod,
                        url: this.dataUrl || this.url,
                        timeout: this.requestTimeout,
                        success: this.handleResponse,
                        failure: this.handleFailure,
                        scope: this,
                        argument: {callback: callback, node: node, scope: scope},
                        params: this.getParams(node)
                    });
                }
            }else{
                // if the load is cancelled, make sure we notify
                // the node that we are done
                this.runCallback(callback, scope || node, []);
            }
        }

    });

The only part I actually modified was the AJAX request, I simply added a timeout configuration option and had it pull the requestTimeout value, as can be seen on line 14. So in my new TreeLoader I can do this (line 5):

        loader: new DCStorm.IQ.Profiles.TreeLoader({
             preloadChildren: true,
             clearOnLoad: true,
             requestMethod: "POST",
             requestTimeout: (60 * 2) * 1000,
             dataUrl: "profiles/backend/profiles.ashx?action=columnstree",
             baseParams: {
                        //action: "columnstree",
                        view: this.viewName,
                        level: this.profile.profile.level,
                        agency: ((this.profile.profile.level == 1) ? this.profile.profile.agency : 0),
                        site: ((this.profile.profile.level == 2) ? this.profile.profile.site : 0),
                        sitegroup: ((this.profile.profile.level == 1) ? this.profile.profile.sitegroup : 0),
                        type: "groupings",
                        filter: "",
                        selected: ""
             }
       }),

And that’s pretty much it.

If you use the Ext.grid.GridPanel or descendants you might have come across this. If you have multiple columns and for some reason decide to make all your columns unhideable then you will notice how the Columns menu item does not go away, regardless of the fact all your columns are no longer manageable in that way.

I spent 5 minutes Friday pondering on this issue and wrote a fix for the Ext.grid.GridView, which is the component actually responsible for rendering the menu. The crux of the fix is:

            if(g.enableColumnHide !== false){
                if (this.cm.config.length > 0) {
                    // Build count of columns that do not have menu disabled
                    var colCount = 0;

                    for (var i = 0; i < this.cm.config.length; i++) {
                        if (this.cm.config[i].menuDisabled == false) colCount++;
                    }

                    // Build count of columns that are hideable
                    var hideCount = 0;

                    for (var i = 0; i < this.cm.config.length; i++) {
                        if (this.cm.config[i].hideable == false) hideCount++;
                    }

                    //
                    if (hideCount < colCount) {
                        this.hmenu.add('-',
                            {id:"columns", text: this.columnsText, menu: this.colMenu, iconCls: 'x-cols-icon'}
                        );
                    }
                }
            }

To prove it works here is two screenshots of before and after the fix:

Before After



And that’s basically it. Below you’ll find the full code to override the Ext.grid.GridView.

Ext.override(Ext.grid.GridView,{

    renderUI : function(){

        var header = this.renderHeaders();
        var body = this.templates.body.apply({rows:''});

        var html = this.templates.master.apply({
            body: body,
            header: header
        });

        var g = this.grid;

        g.getGridEl().dom.innerHTML = html;

        this.initElements();

        // get mousedowns early
        Ext.fly(this.innerHd).on("click", this.handleHdDown, this);
        this.mainHd.on("mouseover", this.handleHdOver, this);
        this.mainHd.on("mouseout", this.handleHdOut, this);
        this.mainHd.on("mousemove", this.handleHdMove, this);

        this.scroller.on('scroll', this.syncScroll,  this);
        if(g.enableColumnResize !== false){
            this.splitZone = new Ext.grid.GridView.SplitDragZone(g, this.mainHd.dom);
        }

        if(g.enableColumnMove){
            this.columnDrag = new Ext.grid.GridView.ColumnDragZone(g, this.innerHd);
            this.columnDrop = new Ext.grid.HeaderDropZone(g, this.mainHd.dom);
        }

        if(g.enableHdMenu !== false){
            if(g.enableColumnHide !== false){
                this.colMenu = new Ext.menu.Menu({id:g.id + "-hcols-menu"});
                this.colMenu.on("beforeshow", this.beforeColMenuShow, this);
                this.colMenu.on("itemclick", this.handleHdMenuClick, this);
            }
            this.hmenu = new Ext.menu.Menu({id: g.id + "-hctx"});
            this.hmenu.add(
                {id:"asc", text: this.sortAscText, cls: "xg-hmenu-sort-asc"},
                {id:"desc", text: this.sortDescText, cls: "xg-hmenu-sort-desc"}
            );
            if(g.enableColumnHide !== false){
                if (this.cm.config.length > 0) {
                    // Build count of columns that do not have menu disabled
                    var colCount = 0;

                    for (var i = 0; i < this.cm.config.length; i++) {
                        if (this.cm.config[i].menuDisabled == false) colCount++;
                    }

                    // Build count of columns that are hideable
                    var hideCount = 0;

                    for (var i = 0; i < this.cm.config.length; i++) {
                        if (this.cm.config[i].hideable == false) hideCount++;
                    }

                    //
                    if (hideCount < colCount) {
                        this.hmenu.add('-',
                            {id:"columns", text: this.columnsText, menu: this.colMenu, iconCls: 'x-cols-icon'}
                        );
                    }
                }
            }
            this.hmenu.on("itemclick", this.handleHdMenuClick, this);

            //g.on("headercontextmenu", this.handleHdCtx, this);
        }

        if(g.trackMouseOver){
            this.mainBody.on("mouseover", this.onRowOver, this);
            this.mainBody.on("mouseout", this.onRowOut, this);
        }
        if(g.enableDragDrop || g.enableDrag){
            this.dragZone = new Ext.grid.GridDragZone(g, {
                ddGroup : g.ddGroup || 'GridDD'
            });
        }

        this.updateHeaderSortState();
    }

});

So today I toyed around with the idea of loading JavaScript files on-demand rather than all at once on page load. The idea is it will not slow down page loading and also you could make it conditional so you only load JavaScript if it’s needed, say if a user makes a certain choice.

My initial effort was:

function loadScript(url)
{
	var el = document.createElement("script");

	el.type = "text/javascript";
	el.src = url;

	document.getElementsByTagName("head")[0].appendChild(el);
}

Fairly simple, create a new <script> tag, set up its attributes then append it to the <head> tag. However when I came to run it with the following code it didn’t want to play ball:

<html>
	<head>
		<title>Test</title>
	</head>
	<body>
		<script type="text/javacript" src="scriptloader.js"></script>
		<script type="text/javascript">
			alert("Start");

			loadScript("otherscript.js");

			// Call something in other script
			DoOther();

			alert("Stop");
		</script>
	</body>
</html>

I’m not sure why this didn’t work but I suspect it’s because the JavaScript file isn’t actually completely loaded by the time I call DoOther() which means DoOther() doesn’t exist yet. Thankfully trawling around online I found somebody else had solved this issue by using a callback and waiting for the script to become ready.

His code is:

/**
 * function loadScript
 * Copyright (C) 2006-2007 Dao Gottwald
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Contact information:
 *   Dao Gottwald  <dao@design-noir.de>
 *
 * @version  1.6
 * @url      http://design-noir.de/webdev/JS/loadScript/
 */

function loadScript(url, callback) {
	var f = arguments.callee;
	if (!("queue" in f))
		f.queue = {};
	var queue =  f.queue;
	if (url in queue) { // script is already in the document
		if (callback) {
			if (queue[url]) // still loading
				queue[url].push(callback);
			else // loaded
				callback();
		}
		return;
	}
	queue[url] = callback ? [callback] : [];
	var script = document.createElement("script");
	script.type = "text/javascript";
	script.onload = script.onreadystatechange = function() {
		if (script.readyState && script.readyState != "loaded" && script.readyState != "complete")
			return;
		script.onreadystatechange = script.onload = null;
		while (queue[url].length)
			queue[url].shift()();
		queue[url] = null;
	};
	script.src = url;
	document.getElementsByTagName("head")[0].appendChild(script);
}

As you can see this is way more extensive and complete than my paltry effort. Plugging this into my little example we have:

<html>
	<head>
		<title>Test</title>
	</head>
	<body>
		<script type="text/javacript" src="scriptloader.js"></script>
		<script type="text/javascript">
			alert("Start");

			loadScript("otherscript.js",function() {
				// Call something in other script
				DoOther();
			});

			alert("Stop");
		</script>
	</body>
</html>

Of course, it will get to alert(”Stop”) long before it calls DoOther() due to the asynchronous nature of the call but it will be able to call DoOther() now.

With Ext if you do not specify the HTTP request type then it will attempt to make this decision for you, the Ext documentation says that if the request has no parameters it will use GET otherwise it will use POST. When you’re starting out this is fine but later on you will soon find you want to change this, especially in relation to optimization, POST is a more expensive version than GET and GET is cachable.

You can easily adapt a standard Ext.Ajax request by specifying the request method like this:

Ext.Ajax.request({
	method: "GET",
	url: "http://www.example.com/",
	...
});

Simple. The above code is nice for plain AJAX requests however for Ext.data.Store requests things are a little trickier, for that you need to specify a new proxy in the config for the store to use:

var store: new Ext.data.Store({
	autoLoad: false,
	proxy: new Ext.data.HttpProxy({
		method: "GET",
		url: "http://www.example.com/timezones.php"
	}),
	reader: new Ext.data.JsonReader(
		{root: "list", totalProperty: "count"},
		[{name: "timezone_id", type: "int"},{name: "timezone_name", type: "string"}]
	)
});

Wouldn’t the store request using GET anyhow you ask? Yes it would. However, here comes the crunch, what if you added params? This is especially so when using for example the paging toolbar which passes it’s information as such.

Hopefully this will help you in your quest to write better web applications.

Note: There is a slight caveat to using GET over POST and that is because GET makes all the params part of the URL so anything that restricts URL length will not like obscenely long lists of params. In the naming and shaming game I single IE out here.