atsid/amd-plugins

View on GitHub
js/ioc.js

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * An Inversion-of-Control plugin for requirejs, allowing us to configure modules that should be initialized before being handed off.
 * Takes some inspiration from Spring and the JavaScript-like example from dojo: http://java.dzone.com/articles/inversion-control-container
 *
 * For requirejs plugin API, please see http://requirejs.org/docs/plugins.html#api
 *
 * Beans are defined as a JSON configuration object, indicating the class to load and optionally params to pass to the constructor.
 * If a bean has already been loaded, it will be cached and returned from then on (operating similarly to a spring bean).
 * Note that this only supports one 'application context' per window because it relies on built-in AMD module caching.
 * In addition, it is currently assumed that the module referenced is a constructor, and will be instantiated.
 *
 * The JSON config looks like so (with id/ref spring terminology):
 * {
 *     beans: {
 *         <id>: {
 *             type: <ref>,
 *             params: {}
 *         }
 *     }
 * }
 *
 * This config needs to be passed to the requirejs config.
 * Here's a simple example to load a logger module.
 * (see http://requirejs.org/docs/api.html#config)
 * requirejs.config({
 *     config: {
 *         'plugins/ioc': {
 *             'beans': {
 *                 'app/logging/Logger': {
 *                     'type': 'app/logging/Logger',
 *                     'params: { 'logLevel': 'DEBUG' }
 *                 },
 *                 'app/logging/HashTrackingLogger': {
 *                     'type': 'app/logging/HashTrackingLogger'
 *                     'params': { 'logger': 'ref:ioc!app/logging/Logger' }
 *                 }
 *             }
 *         }
 *     }
 * });
 *
 * Once loaded for the first time, the instance will be cached by the usual AMD loader mechanism.
 * Note the use of the module name as the bean ID - this allows us to specify 'interfaces' in a java-like manner if desired.
 * The type can then be a different class that implements the same methods, but is injected by the IOC container.
 * You can of course use whatever you'd like for the bean ID, this is just a convention we've found helpful given the dynamic nature of JavaScript.
 *
 * You may also want to use this plugin directly, invoking its load method. If you want to get point-of-use args instead
 * of the global config that requirejs will pass in, send a config object with a 'params' field containing the same params a bean config would:
 * load(name, require, callback, {
 *     params: {
 *         'logLevel: 'DEBUG'
 *     }
 * });
 *
 * A global bean config must still exist for the bean that declares its type! This mechanism just allows for runtime customization of the ctor params
 * when using the ioc plugin directly.
 *
 * Special parameter processing:
 * String parameters starting with 'ref:' are treated specially by this plugin. The remainder of the string following 'ref:' is assumed to be an
 * AMD module path (which could include the use of plugins like ioc!) and is loaded as a module, the return value of which is passed in place of
 * the string as the parameters value.
 */
define([
    'module'
], function (
    module
) {

    'use strict';

    var conf = module.config(),
        beans = conf ? conf.beans : {},
        plugin = {

            /**
             * RequireJs API method for loading a module.
             * @param {string} name - name of module to load. Will be pre-normalized by AMD.
             * @param {function} parentRequire - require function with local scope for loading modules synchronously.
             * @param {function} onload - callback function to invoke with this plugin's output
             * @param {object} config - config object for the plugin. The intent of the IOC plugin is to create object instances,
             *          so config sent in here is assumed to be constructor args for the module, attached to a 'params' field.
             *          NOTE this is not supported by dojo/bdload, but can be used when invoking ioc.load directly, such as by other plugins.
             */
            load: function (name, parentRequire, onload, config) {

                var bean = beans[name],
                    modules,
                    keys = [],
                    params = config && config.params;

                if (bean) {

                    //local bean config wins, otherwise use global
                    params = params || bean.params;

                    //collect modules, including embedded refs.
                    modules = [bean.type];
                    Object.keys(params || {}).forEach(function (key) {
                        var parts = params[key].toString().split('ref:');
                        if (parts.length > 1) {
                            modules.push(parts[1]);
                            keys.push(key);
                        }
                    });

                    parentRequire(modules, function () {
                        var instance, args = arguments;
                        keys.forEach(function (key, idx) {
                            params[key] = args[idx + 1];
                        });
                        instance = new arguments[0](params);
                        onload(instance);
                    });

                } else {
                    throw new Error('IOC bean [' + name + '] requested, but no config found.');
                }

            }

        };

    return plugin;

});