AMD JSONP Loader Module

Needed an AMD module for JSONP loading of scripts. The criteria, it had to be safe-ish, had to use promises conform to CommonJS Promises/A, specifically support Q, and work — gasp — without jQuery.

I started with this article from Angus Croll describing safer JSON P handling. I added Q Promises support and script clean-up to his example for a light-weight module.

/**
 * An API Library for making JSON P calls without jQuery.
 * @author Christopher Pryce<cpryce@digitalriver.com>
 * 
 * Based on JSON and JSONP By Angus Croll
 * http://javascriptweblog.wordpress.com/2010/11/29/json-and-jsonp/
 * 
 * Copyright 2013 Christopher Pryce
 * No use restrictions. Code is distributed AS IS with no implicit or explicit warranty
 *
 *
 * Module definition
 * @dependencies Q or an other library that supports CommonJS Promises/A API
 */
define(['lib/q'], function(promisesLib) {
    "use strict";
    var jsonp = {
        // to provide unique callback identifiers
        callbackCounter: 0,

        /**
         * GETS JSON string from a remote URL
         * @param {String} url The url of the call
         * @param {Function} callback A callback function to run when the script is loaded
         * @param {Object} scope An object to provide scope for the callback
         * @return {Promise} A CommonJS API promise object
         */
        getJSON: function(url, callback, scope) {
            var defer = promisesLib.defer(),
                self = this,
                fn,
                scriptTag,
                script;

            // set a default scope if none exists
            scope = scope || self;

            // create a temporary global function with a incremental key.
            // keeps duplicate functions from being created. 
            fn = 'JSONPCallback_' + self.callbackCounter;
            self.callbackCounter += 1;

            window[fn] = self.evalJSONP(defer);

            // replace the callback name in the url with our own unique name
            url = url.replace('=apiCallback', '=' + fn);

            // append a script element to the document.
            scriptTag = document.createElement('SCRIPT');
            scriptTag.src = url;
            script = document.getElementsByTagName('HEAD')[0].appendChild(scriptTag);

            // clean up global function and remove script after the function runs.
            return defer.promise.then(function(data) {
                window[fn] = undefined;
                try {
                    delete window[fn];
                } catch (e) {
                    // ignore error
                }
                try {
                    script.parentNode.removeChild(script);
                } catch (e) {
                    // ignore error
                }

                // chain the promise
                return data;
            }).then(function(data) {
                // run the callback
                if (typeof callback === 'function') {
                    callback.call(scope, data);
                }

                // chain the promise
                return data;
            });
        },

        /**
         * Evaluates the JSON object returned from the getJSON call
         * Resolves the passed promise.
         * @param {Promise} defer A Common/JS API promise Object
         */
        evalJSONP: function(defer) {
            // return a closure to run when the script is loaded
            return function(data) {
                var validJSON = false;
                if (typeof data === "string") {
                    try {
                        validJSON = JSON.parse(data);
                    } catch (e) {
                        /* invalid JSON */
                    }
                } else {
                    validJSON = JSON.parse(JSON.stringify(data));
                }

                if (validJSON) {
                    defer.resolve(validJSON);
                } else {
                    defer.reject("JSONP call returned invalid or empty JSON");
                }
            };
        }
    };
    return jsonp;
});

Leave a Reply

Your email address will not be published. Required fields are marked *