syringejs
Version:
An ultra-lightweight dependency injection framework for JavaScript
811 lines (681 loc) • 22 kB
JavaScript
// > http://syringejs.org
// > syringe.js v0.6.5. Copyright (c) 2013-2015 M Holt
// > holt.org. Distributed under the MIT License
/* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:false, strict:true,
undef:true, unused:true, curly:true, indent:4, maxerr:50, laxcomma:true, evil: true,
laxbreak:true, multistr: true, camelcase:true, immed: true, latedef: true, nonew:true,
quotmark: true, node: true, newcap: true, browser:true */
(function () {
'use strict';
// Globals
var
root = this,
store = {},
hasProp = {}.hasOwnProperty,
slice = [].slice;
// Utility methods used by the API
var utils = {
// Get an object from an (optional) context `ctx` using delimited
// string notation. The `sep` parameter determines the delimiter
// (a period `.` by default).
getObj: function (str, ctx, sep) {
return str.split((sep || '.')).filter(function (num) {
return num.length;
}).reduce(function (prev, curr, index, list) {
if (prev) {
return prev[list[index]];
}
}, (ctx || this));
},
// Create an object within an (optional) context `ctx` using
// delimited string notation. The `sep` parameter determines
// the delimiter (period by default).
setObj: function (str, ctx, sep) {
return str.split((sep || '.')).reduce(function (prev, curr) {
return (prev[curr]) ? (prev[curr]) : (prev[curr]) = {};
}, (ctx || this));
},
// In cases where no context is provided, we just want simple partial
// application and no clobbering of the original `this` context. This
// utility function allows .call() and .apply() to continue to work
// properly on bound Syringe functions.
bindArgs: function () {
var
args = slice.call(arguments),
fn = this;
return function () {
return fn.apply(this, args.concat(slice.call(arguments)));
};
},
// RFC 4122 GUID generator
makeId: function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (b) {
var a = 16 * Math.random() | 0;
return ('x' === b ? a : a & 3 | 8).toString(16);
});
},
// Get the object type as a string. If an `istype` value is passed the comparison
// is against this value and returns `true` or `false`, otherwise the type itself
// is returned.
getType: function (obj, istype) {
var
ret = 'Undefined',
types = ['Window', 'HTMLDocument', 'Global', 'Document'];
if (obj) {
ret = ({}).toString.call(obj).match(/\s([a-z|A-Z]+)/)[1];
types = types.some(function (item) {
return item.toLowerCase() === ret.toLowerCase();
});
ret = types ? 'Object' : ret;
} else {
if (obj === null) {
ret = 'Null';
} else if (obj === false) {
ret = 'Boolean';
} else if (typeof obj === 'string') {
ret = 'String';
} else if (obj === 0) {
ret = 'Number';
} else if (isNaN(obj) && typeof obj === 'number') {
ret = 'NaN';
}
}
if (typeof istype === 'string') {
return (istype.toLowerCase() === ret.toLowerCase());
} else {
return ret;
}
},
// Return an array that describes the type of items contained inside an arguments
// object, or match an arguments object to an array of type names in order to
// validate the payload
matchArgs: function (args, istype) {
istype = istype || [];
args = [].slice.call(args);
if (!istype.length) {
return args.map(function (item) {
return utils.getType(item);
});
} else if (istype.length === args.length) {
return args.reduce(function (prev, curr, idx) {
if (!prev && utils.getType(istype[idx], 'string')) {
return false;
}
return utils.getType(curr, istype[idx]);
}, true);
} else {
return false;
}
},
// Return a map of any items in the passed array that match items
// in the registry object
getReg: function (arr, id) {
var reg = store[id].registry;
return arr.map(function (item) {
switch (item) {
case '':
return undefined;
case '*':
return reg;
case 'this':
return this;
default:
return utils.getObj(item, reg, store[id].sep);
}
}, this);
},
// Standard ajax retrieval operation
getData: function (url, callback) {
var xhr;
if (!utils.getType(XMLHttpRequest, 'undefined')) {
xhr = new XMLHttpRequest();
} else {
[
'MSXML2.XmlHttp.5.0',
'MSXML2.XmlHttp.4.0',
'MSXML2.XmlHttp.3.0',
'MSXML2.XmlHttp.2.0',
'Microsoft.XmlHttp'
].forEach(function (item) {
try {
xhr = new window.ActiveXObject(item);
return;
} catch (e) {
throw new Error(e);
}
});
}
xhr.onreadystatechange = function () {
if (xhr.readyState < 4) {
return;
}
if (xhr.status !== 200) {
callback(null);
} else if (xhr.readyState === 4) {
callback(xhr);
}
};
xhr.open('GET', url, true);
xhr.setRequestHeader('Accept', 'application/json, text/javascript, */*; q=0.01');
xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
xhr.send('');
},
// Returns `true` if an object contains no enumerable propeties
isEmpty: function (obj) {
return Object.keys(obj).length ? false : true;
},
// Asynch fetch
fetch: function (arr, props, ctx) {
props = props || {};
props.success = props.success || false;
props.xss = props.xss || false;
var
self = this,
count = 0,
url = '';
// Test to see if a passed URL is local
var isLocalURL = function (url) {
var regexp = new RegExp('//' + location.host + '($|/)');
return 'http' === url.substring(0, 4) ? regexp.test(url) : true;
};
// Keep a count of the script load events and reconcile it
// against the length of the script list
var stack = function (xhr) {
if (xhr && xhr.responseText) {
var data = JSON.parse(xhr.responseText);
if (data) {
// Handle "add" versus "set" scenarios. If the target binding path
// exists then the object is extended, otherwise it is added. Note
// that if the returned data is an array and the binding already
// exists, the data is added to the object as a key named "json".
if (self.get(arr[count].bind)) {
if (utils.getType(data, 'array')) {
self.add(arr[count].bind + '.json', data);
}
else if (utils.getType(data, 'object')) {
for (var key in data) {
if (data.hasOwnProperty(key)) {
self.add(arr[count].bind + '.' + key, data[key]);
}
}
}
}
else {
self.add(arr[count].bind, data);
}
}
}
if (++count === arr.length) {
if (utils.getType(props.success, 'function')) {
props.success.apply(self, [(ctx || self), xhr]);
}
}
};
arr.forEach(function (item) {
if (isLocalURL(url = item.path) || props.xss === true) {
utils.getData(item.path, stack);
}
});
},
// The `run` function resolves the dependencies of a bound method.
// When it executes is retrieves the original `fn` method from the
// `cabinet` object, and applies both the injected and free arguments
// to it.
run: function (arr, fn, syr) {
var
args = slice.call(arguments),
props, match, ins, res;
// Remove the id from the arguments
args.splice(2, 1);
// Locate the stored injection target function
match = store[syr.id].cabinet.filter(function (item) {
return item.fn === fn;
})[0];
fn = match ? match.fn : fn;
props = utils.getReg
.apply(syr, [arr, syr.id])
.concat(args.slice(2, args.length));
// Replace pointers to the global object with actual instances
arr.forEach(function (item, idx) {
if (utils.getType(item, 'string') && item.indexOf('global:') === 0) {
props[idx] = utils.getObj(item.slice(7, item.length), root, '.');
}
});
if (!utils.isEmpty(fn.prototype)) {
ins = Object.create(fn.prototype);
res = fn.apply(ins, props);
return (utils.getType(res, 'object')) ? res : ins;
}
// Assume a regular function
else {
return fn.apply(this, props);
}
},
// Execute any events associated with a type of passed action
fire: function (type, id, arr) {
// Fire the events attached to this action
store[id].events[type].forEach(function (event) {
if (utils.getType(event.fn, 'function')) {
// Add the event type
arr.unshift(type);
// Match paths that terminate with a wildcard
if (event.path && event.path.split(store[id].separator).pop() === '*') {
var
wldpath = event.path.split(store[id].separator),
arrpath = arr[1].split(store[id].separator);
wldpath.pop();
arrpath.pop();
wldpath = wldpath.join(store[id].separator);
arrpath = arrpath.join(store[id].separator);
if (arrpath === wldpath) {
event.fn.apply(this, arr);
}
}
else if (
// Match all types of event
!event.path ||
// Events matching a deep path
arr[1] === event.path ||
// Events matching a shallow path
arr[1].split(store[id].separator).pop() === event.path
) {
event.fn.apply(this, arr);
}
// Remove the event type
arr.shift();
}
}, this);
}
};
// Syringe base constructor
var Syringe = function (props) {
store[this.id = utils.makeId()] = {
cabinet : [],
registry : (props && utils.getType(props, 'object')) ? props : {},
separator : '.',
events : {
add : [],
set : [],
get : [],
remove : [],
listops : [],
all : []
}
};
};
// Syringe object prototype methods
var proto = Syringe.prototype = {
// Set the separator character used for creating, specifying, and
// retrieving objects. Whitespace and alphanumeric characters are
// not permitted. By default, the period '.' character is used.
separator: function (val) {
return (utils.getType(val, 'string') &&
(1 === val.replace(/[?a-zA-Z\d]|\s/g, '').length)) ?
(store[this.id].separator = val, this) : false;
},
// Convenience function that allows you to process array items directly
// and which raises an event on completion.
listops: function (name, fn) {
var
reg = store[this.id].registry,
sep = store[this.id].separator,
arr = name.split(sep),
obj = utils.getObj(name, reg, sep),
res = null;
if (utils.getType(obj, 'array') && typeof fn === 'function') {
res = fn(obj);
arr = [name, obj];
if (res) {
arr.push(res);
}
utils.fire('listops', this.id, arr);
}
else {
throw new Error('Key "' + name + '" is not an array!');
}
return this;
},
// Add a new item to the Syringe registry. The name can be provided
// in dot-notation, in which case a deep reference is built within
// the registry. If `value` is a function, the optional `bindings`
// parameter can contain an array of all the registry properties
// with which to bind this function. In this way, registry methods
// can be automatically bound to other registry methods.
add: function (name, value, bindings) {
var
reg = store[this.id].registry,
sep = store[this.id].separator;
switch ((utils.getType(name))) {
case 'Array':
name.forEach(function (item) {
this.add.apply(this, [item]);
}, this);
return this;
case 'Object':
Object.keys(name).forEach(function (key) {
this.add.apply(this, [key, name[key]]);
}, this);
return this;
}
if (!utils.getType(name, 'string')) {
throw new Error('Name must be a string!');
}
name = name.trim();
if (name.indexOf('global:') === 0) {
throw new Error('You can\'t add a key with this prefix!');
}
if (utils.getObj(name, reg, sep)) {
throw new Error('Key "' + name +
'" already exists in the map; use .remove() to unregister it first!');
} else {
if (utils.getType(value, 'function') && bindings) {
value = this.on(bindings, value);
}
var
arr = name.split(sep),
str = (arr.length > 1) ? arr.pop() : false;
if (str) {
utils.setObj(arr.join(sep), reg, sep)[str] = value;
} else {
reg[arr.toString()] = value;
}
}
// Raise an "add" event, passing the name and value
utils.fire('add', this.id, [name, value]);
return this;
},
// Remove a named item from the registry
remove: function (name) {
// Remove an array of items
if (utils.getType(name, 'array')) {
name.forEach(function (item) {
if (utils.getType(item, 'string')) {
this.remove(item);
}
}, this);
return this;
}
// Remove a single item
if (utils.getType(name, 'string')) {
name = name.trim();
var
reg = store[this.id].registry,
sep = store[this.id].separator,
snm = name.trim().split(sep),
lst = snm.pop(),
nrg = {},
obj = {};
snm = snm.join(sep);
obj = snm ? utils.getObj(snm, reg, sep) : reg;
if (utils.getType(obj, 'object')) {
name = lst || snm;
Object.keys(obj).forEach(function (key) {
if (key !== name) {
nrg[key] = obj[key];
}
});
// Deep removal (delimited name)
if (snm) {
this.set(snm, nrg);
}
// Shallow removal (non-delimited name)
else {
store[this.id].registry = nrg;
}
}
// Raise a remove event, passing the name
utils.fire('remove', this.id, [snm + sep + name]);
}
return this;
},
// Bind a method to the dependency registry. This function accepts
// a variety of different arguments, the formulation of which
// determine what type of binding takes place. The variations are
// described below.
on: function ( /* 2, 3, or 4 params */ ) {
var
cab = store[this.id].cabinet,
args = slice.call(arguments),
ctx = root,
obj = { args: args },
gtp = utils.getType,
mtc = utils.matchArgs,
anon, anonctx, named, namedctx;
// Bind arguments only, no context - used when a context is
// provided
var bindArgsOnly = utils.bindArgs.bind(utils.run);
// Utility that adds named methods to a provided context
var namedFuncFactory = function (name, fn, target) {
var
sep = store[this.id].separator,
arr = name.split(sep),
str = (arr.length > 1) ? arr.pop() : false;
target = gtp(target, 'object') ? target : root;
if (str) {
utils.setObj(arr.join(sep), target, sep)[str] = fn;
} else {
target[arr.join(sep)] = fn;
}
}.bind(this);
// __Two__ parameters: the registry array `args[0]` and method
// `args[1]`. No name or context object is provided. The
// bound function will be returned as an anonymous function.
// __Three__ parameters (1): the registry array `args[0]`, the
// method `args[1]`, and a context object `args[2]`.
// When the bound method executes the provided context
// will be used.
// __Three__ parameters (2): a name `args[0]`, the registry array
// `args[1]`, and method `args[2]`. No context object
// is provided. The bound function will be assigned to
// whatever the root object is.
// __Four__ parameters: a name `args[0]`, the registry array
// `args[1]`, the method `args[2]`, and a context object
// `args[3]`. When the bound method executes the provided
// context will be used.
anon = mtc(args, ['array', 'function']);
anonctx = mtc(args, ['array', 'function', 'object']);
named = mtc(args, ['string', 'array', 'function']);
namedctx = mtc(args, ['string', 'array', 'function', 'object']);
if (anon || anonctx || named || namedctx) {
var n = (named || namedctx) ? 1 : 0;
obj.fn = args[n + 1];
obj.ctx = anonctx ? args[n + 2] : ctx;
if (anon || named) {
obj.bind = bindArgsOnly(args[n + 0], args[n + 1], this);
} else if (anonctx || namedctx) {
obj.bind = utils.run.bind(args[n + 2], args[n + 0], args[n + 1], this);
}
// Store a copy of this binding in the `cabinet` object.
// This is useful if we want to copy an existing bound
// function but use new registry items.
cab.push(obj);
// if this is a named method?, If so, add the bound function to the name
if (n) {
namedFuncFactory(args[0], obj.bind, ctx);
return this;
} else {
return obj.bind;
}
}
// If nothing gets returned, just return the Syringe object
return this;
},
// Sometimes you need to call an executable registry item directly.
// You can use this method to do just that. You can also (optionally)
// pass an array of arguments and a context.
exec: function (name, args, ctx) {
ctx = ctx || this;
var fn = this.get(name);
var _fn = store[this.id].cabinet.filter(function (item) {
return item.bind === fn;
})[0];
args = (utils.getType(args, 'array')) ? args : [args];
if ((utils.getType(name, 'string')) && (utils.getType(fn, 'function'))) {
if (_fn) {
fn = _fn ? _fn.fn : fn;
return utils.run
.apply(ctx, [_fn.args[0], fn, this]
.concat(args));
}
return fn.apply(ctx, args);
}
return false;
},
// Retrieve a named item from the registry. You can use dot-notation
// in the passed string. The method will return `false` if the item
// does not exist.
get: function (name) {
var reg = store[this.id].registry;
// Raise a "get" event, passing the name
utils.fire('get', this.id, [name]);
if (utils.getType(name, 'string')) {
var obj = utils.getObj(name, reg, store[this.id].separator);
if (!utils.getType(obj, 'undefined')) {
return obj;
}
return false;
}
return reg;
},
// Set a named item from the registry. As with `get`, you can
// use dot-notation in the passed string. The method will throw
// an exception if you try to set something that doesn't
// exist.
set: function (name, value, bindings) {
var
reg = store[this.id].registry,
sep = store[this.id].separator,
arr = name.split(sep),
str = (arr.length > 1) ? arr.pop() : false,
prn;
// Test the existence of the key we're trying to set. The getObj function
// simply returns the key value, which could be `undefined`. Thus, we need to
// first establish if the value is undefined because the key doesn't exist,
// or if it *does* exist but its value is `undefined`. In the former case we
// throw an error.
if (utils.getObj(name, reg, sep) === undefined) {
prn = utils.getObj(arr.join(sep), reg, sep);
if (str) {
if ((prn && !hasProp.call(prn, str)) || !prn) {
throw new Error('Key "' + name +
'" does not exist in the map!');
}
} else if (!hasProp.call(reg, arr.toString())) {
throw new Error('Key "' + name +
'" does not exist in the map!');
}
}
if (utils.getType(value, 'function') && bindings) {
value = this.on(bindings, value);
}
if (str) {
utils.setObj(arr.join(sep), reg, sep)[str] = value;
} else {
reg[arr.toString()] = value;
}
// Raise a "set" event, passing the name and value
utils.fire('set', this.id, [name, value]);
return this;
},
// Wrap a previously bound method in another `wrapper` function.
// The original function is passed as the first argument to the
// wrapper.
wrap: function (fn, wrapper, ctx) {
ctx = ctx || this;
var match = store[this.id].cabinet.filter(function (item) {
return item.bind === fn;
})[0];
if (match) {
return function () {
var args = slice.call(arguments);
return wrapper.apply(ctx, [
function () {
args = arguments.length ? arguments : args;
return match.bind.apply(ctx, args);
}
].concat(args));
};
}
return false;
},
// Copy an existing bound function `fn` where the copy
// has different registry bindings, provided by the
// `bindings` argument. You can also provide an optional
// execution context.
copy: function (bindings, fn, ctx) {
ctx = ctx || this;
var cab = store[this.id].cabinet;
var match = cab.filter(function (item) {
return item.bind === fn;
})[0];
if (match) {
var obj = {
fn: fn,
ctx: slice.call(arguments)[0],
bind: utils.run.bind(match.ctx, bindings, match.fn, this)
};
cab.push(obj);
return obj.bind;
}
return false;
},
// Attach callbacks to add, set, get, remove, or all action types. The
// event stack associated with each action will be called when
// the action is executed.
listen: function (name, fn) {
var path = name.split(':');
name = path.shift();
if (utils.getType(name, 'string') && utils.getType(fn, 'function')) {
var proc = function (path, arr) {
if (utils.getType(arr, 'array')) {
arr.push({
path: path.length ? path[0] : false,
fn : fn
});
}
};
var events = store[this.id].events;
if (name !== 'all') {
proc(path, events[name]);
}
else {
for (var key in store[this.id].events) {
if (hasProp.call(events, key)) {
proc(path, events[key]);
}
}
}
}
return this;
},
// Create a new Syringe object
create: function (props) {
return new Syringe(props);
}
};
// Allow mixins to be added to the prototype
proto.mixin = function (obj) {
if (utils.getType(obj, 'object')) {
Object.keys(obj).forEach(function (key) {
if (utils.getType(obj[key], 'function')) {
proto[key] = obj[key];
}
});
return this;
}
return false;
};
// Create some method aliases
proto.bind = proto.on;
proto.register = proto.add;
proto.unregister = proto.remove;
// Add the current semver
proto.VERSION = '0.6.5';
// Determine local context
if (this.window) {
proto.fetch = utils.fetch;
root.Syringe = new Syringe();
} else if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = new Syringe();
}
}.call(this));