I got bored had time to kill in work and was creating a few things using plain JS (no frameworks allowed! I wanted to get used to progressive enhancement). I realized I would run into a problem if I wanted to access things in other scopes that might be loaded after my current file, and I didn't want to have to explicitly order the script tags.
I also figured that assigning to the window is kinda 'meh' and I would like to avoid it where possible to avoid exposing global state.
As a result, I wrote a quick n dirty dependency injector for the browser. It doesn't load files asynchronously (i.e, it's not RequireJS), but it's just meant to be simple enough for a small website.
Unfortunately it will only work with ES6-compliant browsers, unless you have a Promise
polyfill. I consider this a fair enough trade off since I much prefer promises to callbacks.
;(function(window, Promise, exports) {
'use strict';
var types = {};
/**
* Register a type with the injector.
* @param {String} name The name of the type to register.
* @param {Function} constructor The constructor for the injector. Constructors can be regular function; `this` will be bound to the constructor function itself. Constructs may register their dependencies by placing an $inject field that is a string array. Each dependency will be resolved before instantiating the constructor. If the dependency cannot be fulfilled, an Error will be thrown.
*/
exports.register = function(name, constructor) {
var bundle = {
dependencies: constructor.$inject || [],
constructor: constructor
};
types[name] = bundle;
};
var resolveQueue = [];
/**
* Resolve a type. Note that this function will do nothing until the DOM has been loaded (this allows for the injector to be sure it has found every dependency).
* @param {String} name The name of the type to resolve.
* @return {Promise<Object>} A promise that will resolve to the resolved object, or reject upon error.
*/
exports.resolve = function(name) {
return new Promise(function(resolve, reject) {
resolveQueue.push(function() {
var bundle = types[name];
if(bundle === undefined) {
reject(new Error('could not find type: '+ name));
return;
}
var dependencies = [];
for (var i = 0; i < bundle.dependencies.length; i++) {
var dependency = exports.resolve(bundle.dependencies[i]);
dependencies.push(dependency);
};
return Promise.all(dependencies)
.then(function(resolvedDependencies) {
resolve(bundle.constructor.apply(bundle.constructor, resolvedDependencies));
});
});
});
};
// Only process the resolve queue after we have loaded.
// This is because we may not have the guarantee that all of our scripts
// (and thus types) have been loaded yet, so some scripts loaded in the incorrect
// order may depend on other scripts that are not loaded yet.
window.addEventListener('load', function() {
for (var i = 0; i < resolveQueue.length; i++) {
resolveQueue[i]();
};
});
})(window, Promise, window.inject = window.inject || {});
Should be fairly simple and lightweight to use: inject.register(typeName, constructor)
to register a type, inject.resolve(name)
to resolve a type.
Here is a working example (you may review this, although it's just here to provide an example for how I want this to be used, rather than actual review code):
;(function(inject, window) {
'use strict';
if(inject === undefined) {
throw new Error('I require my dependency injector to function!');
}
var factories = {};
function create(querySelector, onCreated) {
factories[querySelector] = onCreated;
};
var createDirectives = function(document) {
for (var querySelector in factories) {
if (factories.hasOwnProperty(querySelector)) {
var factory = factories[querySelector];
var elements = document.querySelectorAll(querySelector);
for (var i = 0; i < elements.length; i++) {
factory(elements[i]);
};
}
}
};
inject.register('directive', function() {
return {
create: create
};
});
window.addEventListener('load', function() {
createDirectives(document);
})
})(window.inject, window);