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;
});