Easier Titanium XHR and AJAX Requests

One question I see a lot on the Appcelerator Titanium Developer Q&A is how to perform AJAX requests and/or work with APIs, etc. There is a built-in way to do this with the Ti.Network.HTTPClient module that is pretty easy, but it does have a few drawbacks and “gotchas”, like executing the “success” event for ANY returned status code - even 500 errors. Since working with APIs is so common with mobile apps, I made a wrapper function modeled after jQuery’s $.ajax method that I use in all my apps. It shortens the syntax quite a bit and is much more familiar to those who are used to using jQuery.

The usage looks like this:

// Geocode input text location
app.utils.ajax({
    url: 'http://maps.googleapis.com/maps/api/geocode/json?address=' + txtLocation.value +'&region=us&sensor=true',
    method: 'get',
    success: function(xhr) {
        var data = JSON.parse(xhr.responseText);
        Ti.API.info(data);

        if("OK" == data.status) {
            var res = data.results[0];
            if(res) {
                alert("Location: " + res.geometry.location.lat + ', ' + res.geometry.location.lng);
            }
            // Do something with coordinates
        } else {
            Ti.UI.createAlertDialog({
                title: 'Geocode Error',
                message: 'Unable to geocode location input'
            }).show();
        }
    },
    error: function(xhr) {
        Ti.UI.createAlertDialog({
            title: 'Geocode Error',
            message: 'No location matches found. Please try something else.'
        }).show();
    }
});

And the actual code for the utility function:

/**
 * Application Utilities and Helper Methods
 **/
(function(_app) {
    _app.utils = {};

    // AJAX method that mimmicks jQuery's
    _app.utils.ajax = function(_props) {

        // Merge with default props
        var o = _app.combine({
            method: 'GET',
            url: null,
            data: false,
            contentType: 'application/json',

            // Ti API Options
            async: true,
            autoEncodeUrl: true,

            // Callbacks
            success: null,
            error: null,
            beforeSend: null,
            complete: null
        }, _props);

        Ti.API.info("XHR " + o.method + ": \n'" + o.url + "'...");
        var xhr = Ti.Network.createHTTPClient({
            autoEncodeUrl: o.autoEncodeUrl,
            async: o.async
        });

        // URL
        xhr.open(o.method, o.url);

        // Request header
        xhr.setRequestHeader('Content-Type', o.contentType);

        if(o.beforeSend) {
            o.beforeSend(xhr);
        }

        // Errors
        xhr.setTimeout(10000);
        xhr.onerror = function() {
            Ti.API.info('XHR "onerror" ['+this.status+']: '+this.responseText+'');
            if(null !== o.error) {
                return o.error(this);
            }
        };

        // Success
        xhr.onload = function() {
            // Log
            Ti.API.info('XHR "onload" ['+this.status+']: '+this.responseText+'');

            // Success = 1xx or 2xx (3xx = redirect)
            if(this.status < 400) {
                try {
                    if(null !== o.success) {
                        return o.success(this);
                    }
                } catch(e) {
                    Ti.API.info('XHR success function threw Exception: ' + e + '');
                    return;
                }
            // Error = 4xx or 5xx
            } else {
                Ti.API.info('XHR error ['+this.status+']: '+this.responseText+'');
                if(null !== o.error) {
                    return o.error(this);
                }
            }
        };

        // Send
        if(o.data) {
            Ti.API.info(o.data);
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
            xhr.send(o.data);
        } else {
            xhr.send();
        }

        // Completed
        if(null !== o.complete) {
            return o.complete(this);
        }
    };

    // And it does depend on this code below to combine object properties:

    // Extend an object with the properties from another
    // (thanks Dojo - http://docs.dojocampus.org/dojo/mixin)
    var empty = {};
    function mixin(/*Object*/ target, /*Object*/ source){
        var name, s, i;
        for(name in source){
            s = source[name];
            if(!(name in target) || (target[name] !== s && (!(name in empty) || empty[name] !== s))){
                target[name] = s;
            }
        }
        return target; // Object
    };
    _app.mixin = function(/*Object*/ obj, /*Object...*/ props){
        if(!obj){ obj = {}; }
        for(var i=1, l=arguments.length; i<l; i++){
            mixin(obj, arguments[i]);
        }
        return obj; // Object
    };

    // Create a new object, combining the properties of the passed objects with the last arguments having
    // priority over the first ones
    _app.combine = function(/*Object*/ obj, /*Object...*/ props) {
        var newObj = {};
        for(var i=0, l=arguments.length; i<l; i++){
            mixin(newObj, arguments[i]);
        }
        return newObj;
    };
})(app);

The particular organization of this utilities file assumes that your app structure follows the organization model shown in the Tweetanium example app (using the ‘app’ namespace within a single window context), but is easy to adapt if you are not.

Posted on Sep 15, 2011