Thursday, October 8, 2009

Ajax File Upload Utility

This is the code release of the utility I have written to upload files to API through hidden iframes.
This is a mimic of Ajax style upload, but in reality it is a good old form submit through an iframe.

I have created a Demo for the same and you can download it from the link given below

Demo for Ajax File Upload

I create a new instance of the File Uploader Utility as

var configInstance = {
keepAlive: false,
        responseType: "json",
    }
    var uploader = new fileUploader(configInstance);

The config has values keepAlive which when set true will enable you to download content from the API where you submit the form with the new file content. This can be achieved by appropriate response type of the API.
Also note in any scenario the script will return the content of the response. The responseType is by default set to text or innerHtml you can set it to json or xml depending on the API.

You have following properties available to configure an instance
1. keepAlive : Useful when you want to start a download
2. responseType : default value is 'false' , returns innerHtml. Can be set to json or xml also.
3. formatTag : Allows you to specify the tag from which you want read the content in the response.Sometimes the json might be set inside a <pre> tag in the response html.

To start an upload you need to pass a second config object and the form id .In this example the form id is 'demoFile'.

var config = {


        onComplete: function(data, returnMeData){
            //do something here
        },
        onFailure: function(){
            //do something here
        },
        returnMe: {
            "message": "I am returnMeData"
        }
    }
    uploader.startUpload('demoFile', config);



The onComplete and onFailure callback methods are self explainatory.
The returnMe configuration can be used to pass an object to the utility. The utility will return the same as a parameter to the callback for onComplete as shown above.

All comments and feedback are welcome. Pasting the utility code below also. Cheers!!! :)

The code for the utility is as follows


/* Utility Code Start*/
function fileUploader(config){
    /*
     * Default value is false.
     * Set value as  "json" to get json response.
     * Set value as "xml" to get XML response
     * By default returns response as HTML of the screen
     */
    this.responseType = (config.responseType) ? config.responseType : false;
    /*
     * if the response is enclosed in a specific tag
     */
    this.formatTag = (config.formatTag) ? config.formatTag : "";
    /*
     * by default the value is set to true to keep the iframe alive after the operation is finished
     * comes in handy if you have to start a file download from the respone of the upload
     */
    this.keepAlive = (config.keepAlive) ? true : false;
    /*
     * @description Method to create the dynamic iframe
     * @scope Private to each instance
     *
     */
    function createFrame(c){
        //generate a random value
        var n = 'f' + Math.floor(Math.random() * 99999);
        //javascript:false; prevents the IE alert in secure connections
        var iframe = toElement('<iframe  src="javascript:false;" id="' + n + '" name="' + n + '"></iframe>');
        document.body.appendChild(iframe);
        iframe.style.display = 'none';
        if (c && typeof(c.onComplete) == 'function') {
            iframe.onComplete = c.onComplete;
        }
        if (c && typeof(c.onFailure) == 'function') {
            iframe.onFailure = c.onFailure;
        }
        if (c.returnMe != undefined) {
            iframe.returnBack = c.returnMe;
        }
        iframe.delNode = n;
        iframe.fired = false;
iframe.toDeleteFlag = false;
        return iframe;
    };
    /*
     * Helper Methods Start
     * @scope All are private to each instance created
     */
    function setTarget(f, name){
        f.setAttribute('target', name);
    };
    function toElement(html){
        var div = document.createElement('div');
        div.innerHTML = html;
        var el = div.childNodes[0];
        div.removeChild(el);
        return el;
    };
    function addEvent(el, type, fn){
        if (window.addEventListener) {
            el.addEventListener(type, fn, false);
        }
        else
            if (window.attachEvent) {
                var f = function(){
                    fn.call(el, window.event);
                };
                el.attachEvent('on' + type, f)
            }
    };
    /*Helper Methods Ends*/
    var other = this;
    /*
     * @scope Public
     * @description
     * @parameters f : id of form to be submitted for file Upload
     *   c : the configuration passed with the onComplete, onFailure and returnMe data
     * Sample of this c property
     * c = {
     * onComplete : function(){},
     * onFailure : function(){}, you will recieve the error object in the parameter here
     * returnMe : {prop1:"data1",prop2:"data2"}
     * }
     */
    this.startUpload = function(fID, c){
        try {
            var f = document.getElementById(fID);
            if (!f) {
                var e = new Error();
                e.description = "Form element not found";
                throw e;
            }
            var iframe = createFrame(c);
            var that = other;

            addEvent(iframe, "load", function(e){
                //function for capturing the load
                var loaded = function(){
                    try {
                        var response;
                        if (doc.XMLDocument) {
                            response = doc.XMLDocument;
                        }
                        else
                            if (doc.body) {
                                //convert result text to JSON format
                                if (that.responseType == 'json') {
                                    if (that.formatTag != "") {
                                        response = eval("result=" + doc.body.getElementsByTagName(that.formatTag)[0].innerHTML);
                                    }
                                    else {
                                        response = eval("result=" + doc.body.innerHTML);
                                    }
                                }
                                else
                                    if (!that.responseType) {
                                        response = doc.body.innerHTML;
                                    }
                            }
                            else {
                                // response is a xml document
                                response = doc;
                            }
                      
                        if (typeof(iframe.onComplete) == 'function') {
                            if (iframe.returnBack != undefined || iframe.returnBack != null) {
                                iframe.onComplete(response, iframe.returnBack);
                            }
                            else
                                iframe.onComplete(response);
                            // Reload blank page, so that reloading main page
                            // does not re-submit the post.
                            // delete the frame
                            iframe.toDeleteFlag = true;
                            //load event fired reset the iframe
//but we will not reset the iframe url to consider
//the fact that a file can be downloaded if keepAlive is true
//Please note the file download happens only if the response type of the
//url to which the form is submitted is correct
                            if (!that.keepAlive) {
                                iframe.src = "about:blank";
                            }
                        }
                    }
                    catch (e) {
                        //suppress
                    }
                };
                if (iframe.src == "about:blank") {
                    //dont delete on first read
                    if (iframe.toDeleteFlag) {
                        // Fix busy state in FF3
                        setTimeout(function(){
                            //keepAlive is true if its a file download
                            if (!that.keepAlive) {
                                document.body.removeChild(iframe);
                            }
                        }, 0);
                    }
                    return;
                }
                //Fix Opera multiple event firing
                if (iframe.fired) {
                    return;
                }
                var doc = null
                if (iframe.contentDocument) {
                    doc = iframe.contentDocument;
                }
                else
                    if (iframe.contentWindow) {
                        doc = iframe.contentWindow.document;
                    }
                    else {
                        doc = window.frames[iframe.id].document;
                    }
                //Opera fires onload twice.
                // Once when the content of the html is 'false'
                if (window.opera) {
                    if (doc.body && doc.body.innerHTML != "false") {
                        iframe.fired = true;
                        loaded();
                    }
                }
                else {
                    if (doc.body && doc.body.innerHTML != "false") {
                        iframe.fired = true;
                        loaded();
                    }
                }
            });
            //set the target of iframe and submit
            setTarget(f, iframe.id);
            f.submit();
        }
        catch (e) {
            //suppress all errors
            //fire onFailure method with the error object as param
            if (typeof(c.onFailure) == 'function') {
                c.onFailure(e);
                if (iframe) {
                    iframe.src = "about:blank";
                }
            }
        }
    };
};


/* Utility Code Ends*/

Wednesday, October 7, 2009

History Utility

An ideal Ajax Application for me will be Gmail. A solid back-end and an interactive front-end. The features come in abundance and the user interaction is the smoothest. The most interesting aspect is how Gmail manages browser url,back button and screen content using the Fragment URL a.k.a the Hashtag. 

Lot of utilities are available for this such as Real Simple History, YUI History and jquery.address plugin. RSH is kinda weird to use in development, YUI is the coolest. jQuery Address was pretty reliable in the initial tinkerings but definitely not half as cool to work with as YUI.

But my biggest hiccup is most projects demand that we work on jQuery. Also if I use YUI just for History management using #tag 'I be Damned'. So I sat down on a 3 day code-sprint, coded something that works as a jQuery plugin and behaves like YUI in terms of events exposed. Here is the link to the small development environment I used to build it.




HUtil is the global singleton instance for this utiliity. HUtil exposes a HUtil.initialize() method which can be used to init the HUtil in your screen. 

In the demo index.html the same is called inside $(document).ready has been fired by jQuery. It is advisable to init the HUtil only after you have determined the state of the screen in your code using the Utility methods available in HUtil  

For getting the current bookmarked state of the application use 
HUtil.getBookmarkedState(module_name);  

Also available is a utility method to get the value of a param in the search string of the URL
HUtil.getQueryStringParameter(module_name);  
You can also use this utility method as 
HUtil.getQueryStringParameter(module_name, href );  

For registering a module use this API 
HUtil.registerModule(module_name, initSection).notify('HUtil:moduleStateChange', callback);  

Please note that the notify() method is currently chained to the module being registered. 

The registerModule API returns reference to the module object instance created. HUtil will fire the callback method for any change in the state of module registered with it, with the name of section returned as a param. 
To navigate the url to a particular section state use something like 
HUtil.navigate(smodule_name , new_value);  

There is an option to multi-navigate the URL also 
HUtil.multiNavigate(module_state_array); 

For example the module_state_array [{"navbar":"multi"},{"status":""}] defines new states for two sections navbar and status. 
Please note module_name is a string value. 

For every module update the HUtil will fire the registered methods for that section. You can handle your screen content here(similar to YUI). Also if the browser URL changes due to back button click these registered methods will be fired.  
The sample usage code is found in the index.html file. Feel free to do any tinkering with it. It is a lot similar to YUI API for their History Utility.

Its in dev phase so am not adding any acknowledgements and licenses etc just yet. Will be posting details regarding documentation and usage in near future.  
Do give Feedback, nothing like it. Cheers Guys!!!  :)

HERE IS THE DOWNLOAD LINK AGAIN