Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

refactor to a plugin arch, add options & attrs injection

  • Loading branch information...
commit 7b7129283f8ea898ce15aeb07cb16cc8552ce60c 1 parent f42785d
@iamnoah authored
View
89 README.md
@@ -24,6 +24,11 @@ JMVC Inject is inspired by AMD. The reasons you'd want to inject functions are v
* Identifiers can be remapped easily, so you can swap out implementations. This is great for testing and makes code more reusable.
* Encapsulation. For functions, encapsulation means the function code can focus on producing a result instead of worrying about getting all its dependencies in order. This also makes writing [pure functions](http://en.wikipedia.org/wiki/Pure_function) a lot easier.
+## New Features
+
+ * Injecting Controller options - Controllers can have their options set to injected values. Controller instantiation will be deferred.
+ * Injecting Attributes - Like controllers, Model/Observe style Classes can have their attributes set with values from the injector (but creation will be deferred).
+
# Usage
To create an injector, call `Inject` with the inject and dependency definitions. The return value of `Inject` (i.e., the injector) is a function.
@@ -141,6 +146,8 @@ You can use `Inject.useCurrent` to define a function that will rebind the contex
}),500);
})();
+Note that `useCurrent` will throw an exception if there is no current context. If you want to capture the context if there is one, but otherwise proceed as normal, then pass `true` as the second argument.
+
### Controller Action Handlers
Controllers action handlers will not generally be called inside a bound function, so they have the same problem as an async function call. Any unbound handler function has to get the injector some other way. `Inject.setupControllerActions` will setup the action handlers such that they are bound to the injector context that was active when the controller instance was created:
@@ -326,7 +333,7 @@ Using a selector, we didn't have to pass an option to the 2nd controller to inje
Factories used by controllers can take options as parameters, to allow for very flexible injection:
var injector = Inject({
- name: 'foo()', // note that the name must end with ()
+ name: 'foo',
// a = optionA and b = optionB below
factory: function(a,b) {
return a + b;
@@ -347,6 +354,86 @@ Factories used by controllers can take options as parameters, to allow for very
Parameter names correspond to controller options but should not be contained in `{}`.
+### Injecting controller options
+
+The injector can set values on the options object passed to your controller by using the `Inject.setupController` method as your static setup method. This enables templated event binding on injected values (JMVC 3.2+).
+
+Note that the controller instance itself is not modified, just the initial options hash that is passed in and the injector will *not* override options that are already defined, because they have been passed in to the controller at the point of creation:
+
+ $.Controller('Foo',{
+ // note this is the Static setup method
+ setup: Inject.setupController // also fixes controller actions
+ },{
+ init: function() {
+ // alerts "Hello Bob!"
+ alert(this.options.foo + this.options.bar);
+ },
+ "{model} foo": function(model,event) {
+ // foo changed! do something!
+ }
+ });
+
+ Inject({
+ name: 'bar',
+ factory: function() {
+ return 'Hello ';
+ }
+ },{
+ name: 'baz',
+ factory: function() {
+ return 'World!';
+ }
+ },{
+ name: 'someModel',
+ factory: // ...
+ },{
+ name: 'Foo',
+ options: {
+ foo: 'bar',
+ bar: 'baz',
+ model: 'someModel'
+ }
+ })(function() {
+ $('#foo').foo({bar: 'Bob!'});
+ }).call(this);
+
+### Injecting Attributes
+
+Similarly, classes that take a hash of attribute values as the first argument to their contructor can have injected values set.
+
+Note that the class instance itself is not modified, just what is passed to the constuctor and the injector will *not* override values that are already defined:
+
+ $.Observe('Foo',{
+ // note this is the Static setup method
+ setup: Inject.setup
+ },{
+ init: function() {
+ // baz = "Hello Bob!"
+ this.baz = this.foo + this.bar;
+ }
+ });
+
+ Inject({
+ name: 'bar',
+ factory: function() {
+ return 'Hello ';
+ }
+ },{
+ name: 'baz',
+ factory: function() {
+ return 'World!';
+ }
+ },{
+ name: 'Foo',
+ attrs: {
+ foo: 'bar',
+ bar: 'baz'
+ }
+ })(function() {
+ new Foo({bar:'Bob!'}).done(function(foo) {
+ alert(foo.baz);
+ });
+ }).call(this);
# Misc
View
43 attrs.coffee
@@ -0,0 +1,43 @@
+setup = (support)->
+ exports = Inject
+
+ injectConstructor = (_super,index,getConfig) ->
+ (args...) ->
+ # get the definition and find the map of attr -> dependency
+ config = getConfig(this,support,args)
+ target = args[index]
+
+ keys = (key for key, dep of config)
+ deps = (dep for key, dep of config)
+
+ # resolve the dependencies and set each one on the target object iff it is undefined
+ # then continue construction
+ Inject.require.apply(this,deps.concat( (values...) ->
+ for value, i in values
+ key = keys[i]
+ target[key] = value unless has.call(target,key)
+ _super.apply(this,args)
+ )).call(this)
+
+ exports.setup = ->
+ Inject.setup.arg().apply(this,arguments)
+
+ exports.setup.arg = (index = 0,getConfig = getAttrs) ->
+ ->
+ @newInstance = injectConstructor @newInstance, index, getConfig
+
+getAttrs = (Class,support) ->
+ support.definition(Class.fullName).attrs ? {}
+
+
+has = Object.prototype.hasOwnProperty
+
+(if steal.plugins
+ steal('inject-core.js')
+else
+ steal('./inject-core.js')).then ->
+
+ Inject.plugin
+ init: setup
+
+
View
76 attrs.js
@@ -0,0 +1,76 @@
+// Generated by CoffeeScript 1.3.3
+(function() {
+ var getAttrs, has, setup,
+ __slice = [].slice;
+
+ setup = function(support) {
+ var exports, injectConstructor;
+ exports = Inject;
+ injectConstructor = function(_super, index, getConfig) {
+ return function() {
+ var args, config, dep, deps, key, keys, target;
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+ config = getConfig(this, support, args);
+ target = args[index];
+ keys = (function() {
+ var _results;
+ _results = [];
+ for (key in config) {
+ dep = config[key];
+ _results.push(key);
+ }
+ return _results;
+ })();
+ deps = (function() {
+ var _results;
+ _results = [];
+ for (key in config) {
+ dep = config[key];
+ _results.push(dep);
+ }
+ return _results;
+ })();
+ return Inject.require.apply(this, deps.concat(function() {
+ var i, value, values, _i, _len;
+ values = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
+ for (i = _i = 0, _len = values.length; _i < _len; i = ++_i) {
+ value = values[i];
+ key = keys[i];
+ if (!has.call(target, key)) {
+ target[key] = value;
+ }
+ }
+ return _super.apply(this, args);
+ })).call(this);
+ };
+ };
+ exports.setup = function() {
+ return Inject.setup.arg().apply(this, arguments);
+ };
+ return exports.setup.arg = function(index, getConfig) {
+ if (index == null) {
+ index = 0;
+ }
+ if (getConfig == null) {
+ getConfig = getAttrs;
+ }
+ return function() {
+ return this.newInstance = injectConstructor(this.newInstance, index, getConfig);
+ };
+ };
+ };
+
+ getAttrs = function(Class, support) {
+ var _ref;
+ return (_ref = support.definition(Class.fullName).attrs) != null ? _ref : {};
+ };
+
+ has = Object.prototype.hasOwnProperty;
+
+ (steal.plugins ? steal('inject-core.js') : steal('./inject-core.js')).then(function() {
+ return Inject.plugin({
+ init: setup
+ });
+ });
+
+}).call(this);
View
96 controller.coffee
@@ -0,0 +1,96 @@
+support = null
+
+factoryName = ///
+ ^ # match the whole string
+ ([^(]+) # everything up the ( or the end is the real name
+ (\( # 2nd capture is the ()
+ (.*?)? # 3rd capture is the arguments, unparsed
+ \))?
+ $
+///
+
+
+
+getOptions = (Class,support,args) ->
+ el = args[0]
+ def = support.definition(Class.fullName)
+ # XXX not a typo, we want the last def that matches selector
+ def = d for selector, d of def.controllerDefs when not selector or el.is(selector)
+ def?.options
+
+processDef = (target,defs) ->
+ # only process controllers
+ return unless target?.element and target?.Class
+
+ # group and collapse
+ # XXX must do them in the same order they are defined
+ grouped = []
+ byKey = {}
+ for d in defs
+ key = d.controller || ''
+ merged = byKey[key]
+
+ if not merged
+ merged = byKey[key] = {}
+ grouped.push(merged)
+
+ $.extend(true,merged,d)
+
+ # find the def that matches the controller's element, or use the default
+ def = d for d in grouped when d.controller and target.element.is(d.controller)
+ def ||= byKey['']
+
+ # XXX! return a copy so we don't create a loop in the object graph
+ def = $.extend(true,{},def);
+
+ # save the other definitions (for getOptions)
+ def.controllerDefs = grouped if def
+ def
+
+resolveFactory = (target,name,targetDef) ->
+ # only process controllers
+ return unless target?.element and target?.Class
+
+ # make options substitutions and resolve parameterize factory arguments
+ get = (path) ->
+ $.String.getObject(path,[target.options])
+
+ parts = factoryName.exec(substitute(name,target.options) || name)
+ realName = parts[1]
+ args = (get(path) for path in parts[3]?.split(',') ? [] when path)
+
+ fn = support.definition(realName)?.factory
+ -> fn.apply(this,args) if fn
+
+substitute = (string,options) ->
+ string.replace /\{(.+?)\}/g, (param,name) ->
+ $.String.getObject(name,[options])
+
+(if steal.plugins
+ steal('inject-core.js','attrs.js').plugins('jquery/lang')
+else
+ steal('./inject-core.js','./attrs.js','jquery/lang')).then ->
+
+ $ = this.$ || this.can
+
+ exports = Inject
+
+ ###
+ Important note: jQuery.is is required to use a controller selector.
+ ###
+ Inject.plugin
+ processDefinition: processDef
+ resolveFactory: resolveFactory
+ init: (s) ->
+ support = s
+
+ exports.setupController = ->
+ Inject.setup.arg(1,getOptions).apply(this,arguments)
+ # TODO add setupControllerActions somehow
+
+ exports.setupControllerActions = ->
+ for funcName, action of this.Class.actions
+ this[funcName] = Inject.useCurrent(this[funcName])
+
+ @_super.apply(this,arguments)
+
View
119 controller.js
@@ -0,0 +1,119 @@
+// Generated by CoffeeScript 1.3.3
+(function() {
+ var factoryName, getOptions, processDef, resolveFactory, substitute, support;
+
+ support = null;
+
+ factoryName = /^([^(]+)(\((.*?)?\))?$/;
+
+ getOptions = function(Class, support, args) {
+ var d, def, el, selector, _ref;
+ el = args[0];
+ def = support.definition(Class.fullName);
+ _ref = def.controllerDefs;
+ for (selector in _ref) {
+ d = _ref[selector];
+ if (!selector || el.is(selector)) {
+ def = d;
+ }
+ }
+ return def != null ? def.options : void 0;
+ };
+
+ processDef = function(target, defs) {
+ var byKey, d, def, grouped, key, merged, _i, _j, _len, _len1;
+ if (!((target != null ? target.element : void 0) && (target != null ? target.Class : void 0))) {
+ return;
+ }
+ grouped = [];
+ byKey = {};
+ for (_i = 0, _len = defs.length; _i < _len; _i++) {
+ d = defs[_i];
+ key = d.controller || '';
+ merged = byKey[key];
+ if (!merged) {
+ merged = byKey[key] = {};
+ grouped.push(merged);
+ }
+ $.extend(true, merged, d);
+ }
+ for (_j = 0, _len1 = grouped.length; _j < _len1; _j++) {
+ d = grouped[_j];
+ if (d.controller && target.element.is(d.controller)) {
+ def = d;
+ }
+ }
+ def || (def = byKey['']);
+ def = $.extend(true, {}, def);
+ if (def) {
+ def.controllerDefs = grouped;
+ }
+ return def;
+ };
+
+ resolveFactory = function(target, name, targetDef) {
+ var args, fn, get, parts, path, realName, _ref;
+ if (!((target != null ? target.element : void 0) && (target != null ? target.Class : void 0))) {
+ return;
+ }
+ get = function(path) {
+ return $.String.getObject(path, [target.options]);
+ };
+ parts = factoryName.exec(substitute(name, target.options) || name);
+ realName = parts[1];
+ args = (function() {
+ var _i, _len, _ref, _ref1, _ref2, _results;
+ _ref2 = (_ref = (_ref1 = parts[3]) != null ? _ref1.split(',') : void 0) != null ? _ref : [];
+ _results = [];
+ for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
+ path = _ref2[_i];
+ if (path) {
+ _results.push(get(path));
+ }
+ }
+ return _results;
+ })();
+ fn = (_ref = support.definition(realName)) != null ? _ref.factory : void 0;
+ return function() {
+ if (fn) {
+ return fn.apply(this, args);
+ }
+ };
+ };
+
+ substitute = function(string, options) {
+ return string.replace(/\{(.+?)\}/g, function(param, name) {
+ return $.String.getObject(name, [options]);
+ });
+ };
+
+ (steal.plugins ? steal('inject-core.js', 'attrs.js').plugins('jquery/lang') : steal('./inject-core.js', './attrs.js', 'jquery/lang')).then(function() {
+ var $, exports;
+ $ = this.$ || this.can;
+ exports = Inject;
+ /*
+ Important note: jQuery.is is required to use a controller selector.
+ */
+
+ Inject.plugin({
+ processDefinition: processDef,
+ resolveFactory: resolveFactory,
+ init: function(s) {
+ return support = s;
+ }
+ });
+ exports.setupController = function() {
+ return Inject.setup.arg(1, getOptions).apply(this, arguments);
+ };
+ return exports.setupControllerActions = function() {
+ var action, funcName, _ref;
+ _ref = this.Class.actions;
+ for (funcName in _ref) {
+ action = _ref[funcName];
+ this[funcName] = Inject.useCurrent(this[funcName]);
+ }
+ return this._super.apply(this, arguments);
+ };
+ });
+
+}).call(this);
View
171 inject-core.coffee
@@ -2,23 +2,11 @@
Requirements:
jQuery or DoneJS/CanJS ($.when and $.extend)
- Optional:
- JavaScriptMVC/DoneJS/CanJS
- $.String.getObject - required for parameterized factories
###
window = this
exports = window
-factoryName = ///
- ^ # match the whole string
- ([^(]+) # everything up the ( or the end is the real name
- (\( # 2nd capture is the ()
- (.*?)? # 3rd capture is the arguments, unparsed
- \))?
- $
-///
-
error = if window.console and console.error
(args...) -> console.error.apply(console,args)
else
@@ -35,16 +23,11 @@ bind = (obj,name) ->
D = if window.can
when: bind(can,'when')
extend: bind(can,'extend')
- getObject: bind(can,'getObject') #optional, but we know it's there
else
unless window.jQuery or window.$?.when and window.$?.extend
- throw new Error("Either JavaScriptMVC, DoneJS, CanJS or jQuery is required.")
- when: bind(jQuery,'when')
- extend: bind(jQuery,'extend')
- getObject: if jQuery.String?.getObject
- bind(jQuery.String,'getObject')
- else -> throw new Error("Cannot use controllers without JMVC")
-
+ throw new Error("Either JavaScriptMVC, DoneJS, CanJS or jQuery/Zepto is required.")
+ when: bind(window.jQuery or window.$,'when')
+ extend: bind(window.jQuery or window.$,'extend')
@@ -52,6 +35,7 @@ else
# the global stack of injectors
CONTEXT = []
+PLUGINS = []
inject = (defs...)->
factories = {}
@@ -59,46 +43,43 @@ inject = (defs...)->
defs = groupBy(defs,'name')
eager = []
- resolver = (name) ->
- def = {}
-
- # the factories are already built, so we just need to get the inject definitions
- # to create the mapping
- if(name && name.controller)
- controller = name.controller
- name = getName(controller)
-
- # find matching definitions and collapse them into def
- ###
- Important note: jQuery.is is required to use a controller selector.
- ###
- D.extend(true,def,d) for d in defs[name] || [] when \
- !controller || !d.controller || controller.element.is(d.controller)
+ resolver = (obj) ->
+ def = definition(obj)
+ controller = def.controllerInstance
# def just tells us how to map the dependency names to global names
mapping = mapper(def)
resolve = (name) ->
- # TODO enable for non-controllers? would be accessing globals...
- sub = (name) ->
- controller && substitute(name,controller.options) || name
+ realName = mapping(name)
+ factory = factories[realName]
- get = (path) ->
- unless controller
- throw new Error("parameterized factories can only be used on controllers. Cannot resolve '#{path}' for '#{name}' AKA '#{realName}'")
- D.getObject(path,[controller.options])
+ # let the plugins override the factory
+ for plugin in PLUGINS when plugin.resolveFactory
+ factory = plugin.resolveFactory(obj,realName,def) || factory
+ unless factory
+ throw new Error("Cannot resolve '#{realName}' AKA '#{name}'")
- parts = factoryName.exec(mapping(sub(name)))
- realName = parts[1]
- args = (get(path) for path in parts[3]?.split(',') ? [] when path)
+ factory.call(this)
- unless factories[realName]
- throw new Error("Cannot resolve '#{realName}' AKA '#{name}'")
+ definition = (target) ->
+ context = last(CONTEXT)
+ name = context.name || getName(target)
+ def = {}
+
+ definitions = (defs[name] || []).slice(0)
+
+ # let the plugins add additional definitions
+ for plugin in PLUGINS when plugin.processDefinition
+ definitions.push(plugin.processDefinition(target,definitions) || {})
+
+ # collapse all the definitions
+ D.extend(true,def,d) for d in definitions
- factories[realName].apply(this,args)
+ def
- injector = whenInjected(resolver)
+ injector = whenInjected(resolver,definition)
# pre-create factories
for name, configs of defs
@@ -106,7 +87,8 @@ inject = (defs...)->
D.extend(true,def,d) for d in configs
- [name,factory] = makeFactory(def)
+ name = def.name
+ factory = def.factory
eager.push(factory) if def.eager
@@ -114,7 +96,7 @@ inject = (defs...)->
# run the eager factories (presumably they cache themselves)
# eager factories are built in to make it easier to resolve dependencies of the eager factory
- useInjector(injector, ->
+ useInjector({injector:injector,definition:definition}, ->
factory() for factory in eager
).call(this)
@@ -127,7 +109,7 @@ injectUnbound = (name) ->
context = last(CONTEXT)
unless context
noContext()
- injected = context.named(name).apply(this,args) # create an injected function
+ injected = context.injector.named(name).apply(this,args) # create an injected function
injected.apply(this,arguments) # and call it
injectCurrent.andReturn = andReturn
injectCurrent
@@ -147,7 +129,7 @@ useInjector = (injector,fn) ->
CONTEXT.pop()
inject.useCurrent = (fn,ignoreNoContext) ->
- context = last(CONTEXT);
+ context = last(CONTEXT)
unless context or ignoreNoContext
noContext()
if context then useInjector(context,fn) else fn
@@ -158,6 +140,7 @@ noContext = ->
# cache offers a simple mechanism for creating (and clearing) singletons
# without caching, the injected values are recreated/resolved each time
+# TODO plugin
cache = inject.cache = ->
results = {}
@@ -189,39 +172,22 @@ cache = inject.cache = ->
singleton
-inject.setupControllerActions = ->
- for funcName, action of getClass(this).actions
- this[funcName] = inject.useCurrent(this[funcName])
-
- @_super.apply(this,arguments)
-
-makeFactory = (def)->
- [fullName, name, params] = factoryName.exec(def.name)
- fn = def.factory
- [name, ->
- unless fn
- throw new Error("#{fullName} does not have a factory function so it cannot be injected into a function.");
- if arguments.length && !params
- throw new Error("#{fullName} is not a parameterized factory, it cannot take arguments. If you want to pass it arguments, the name must end with '()'.")
- fn.apply(this,arguments)
- ]
-
-substitute = (string,options) ->
- string.replace /\{(.+?)\}/g, (param,name) ->
- D.getObject(name,[options])
-
-whenInjected = (resolver) ->
+whenInjected = (resolver,definition) ->
destroyed = false
# injectorFor creates requires(), which is what the user sees as the injector
injectorFor = (name) ->
requires = (dependencies...,fn)->
- fn = useInjector injector, fn # make sure the function retains the right context
+ injectContext =
+ injector: injector
+ name: name
+ definition: definition
+ fn = useInjector injectContext, fn # make sure the function retains the right context
# when takes a list of the dependencies and the function to inject
# and returns a function that will resolve the dependencies and pipe them into the function
- injected = useInjector injector, (args...) -> # set the context when the injected function is called
+ injected = useInjector injectContext, (args...) -> # set the context when the injected function is called
return if destroyed
target = this
- resolve = resolver(name || nameOf(target))
+ resolve = resolver(target)
try
deferreds = (resolve(d) for d in dependencies)
catch e
@@ -247,14 +213,8 @@ andReturn = (afterAdvice) ->
def = fn.apply(this,args)
afterAdvice.apply(this,[def].concat(args))
-nameOf = (target) ->
- if target.element && getClass(target)
- controller: target
- else
- getName(target)
-
getName = (target) ->
- target?.options?.inject?.name || getClass(target)?.fullName
+ getClass(target)?.fullName
getClass = (target) ->
target?.Class || target?.constructor
@@ -277,6 +237,47 @@ matchArgs = (results,args,del) ->
exports.Inject = inject
+
+## Plugins ##
+
+###
+ Plugins can define 3 methods:
+
+ * init(pluginSupport) - passed the plugin support object, which has some helper functions.
+ * processDefinition(target,definitions) -
+ can return an additional definition object that will override the other definitions.
+ DO NOT call pluginSupport.definition inside this method.
+ * resolveFactory(target,name,targetDefinition) -
+ can return a factory function that will override the defined factory.
+###
+
+pluginSupport =
+ ###
+ Helper for getting a copy of the definition used to inject the given object in the current context.
+
+ @param {Object|String} target the thing to be injected, or its name. Some plugins may
+ return a different definition for an instance than they would for the name.
+ @return {Object} a copy of the injection definition for target in the current injector.
+ ###
+ definition: (target) ->
+ context = last(CONTEXT)
+
+ unless context
+ noContext()
+
+ # fake an object it for names
+ if typeof target == 'string'
+ target =
+ Class:
+ fullName: target
+
+ context.definition(target)
+
+inject.plugin = (plugin)->
+ PLUGINS.push(plugin)
+ if plugin.init
+ plugin.init(pluginSupport)
+
## Support Functions ##
groupBy = (array,fn) ->
View
271 inject-core.js
@@ -1,23 +1,19 @@
+// Generated by CoffeeScript 1.3.3
/*
Requirements:
jQuery or DoneJS/CanJS ($.when and $.extend)
-
- Optional:
- JavaScriptMVC/DoneJS/CanJS
- $.String.getObject - required for parameterized factories
*/
+
(function() {
- var CONTEXT, D, andReturn, bind, cache, error, exports, factoryName, find, getClass, getName, groupBy, inject, injectUnbound, last, makeFactory, mapper, matchArgs, nameOf, noContext, substitute, useInjector, whenInjected, window,
- __slice = Array.prototype.slice;
+ var CONTEXT, D, PLUGINS, andReturn, bind, cache, error, exports, find, getClass, getName, groupBy, inDef, inject, injectUnbound, last, mapper, matchArgs, noContext, pluginSupport, useInjector, whenInjected, window,
+ __slice = [].slice;
window = this;
exports = window;
- factoryName = /^([^(]+)(\((.*?)?\))?$/;
-
error = window.console && console.error ? function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
@@ -27,7 +23,9 @@
bind = function(obj, name) {
var fn;
fn = obj[name];
- if (!fn) throw new Error("" + name + " is not defined.");
+ if (!fn) {
+ throw new Error("" + name + " is not defined.");
+ }
return function() {
var args;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
@@ -36,84 +34,74 @@
};
D = (function() {
- var _ref, _ref2, _ref3;
+ var _ref, _ref1;
if (window.can) {
return {
when: bind(can, 'when'),
- extend: bind(can, 'extend'),
- getObject: bind(can, 'getObject')
+ extend: bind(can, 'extend')
};
} else {
- if (!(window.jQuery || ((_ref = window.$) != null ? _ref.when : void 0) && ((_ref2 = window.$) != null ? _ref2.extend : void 0))) {
- throw new Error("Either JavaScriptMVC, DoneJS, CanJS or jQuery is required.");
+ if (!(window.jQuery || ((_ref = window.$) != null ? _ref.when : void 0) && ((_ref1 = window.$) != null ? _ref1.extend : void 0))) {
+ throw new Error("Either JavaScriptMVC, DoneJS, CanJS or jQuery/Zepto is required.");
}
return {
- when: bind(jQuery, 'when'),
- extend: bind(jQuery, 'extend'),
- getObject: ((_ref3 = jQuery.String) != null ? _ref3.getObject : void 0) ? bind(jQuery.String, 'getObject') : function() {
- throw new Error("Cannot use controllers without JMVC");
- }
+ when: bind(window.jQuery || window.$, 'when'),
+ extend: bind(window.jQuery || window.$, 'extend')
};
}
})();
CONTEXT = [];
+ PLUGINS = [];
+
inject = function() {
- var configs, d, def, defs, eager, factories, factory, injector, name, resolver, results, _i, _len, _ref;
+ var configs, d, def, definition, defs, eager, factories, factory, injector, name, resolver, results, _i, _len;
defs = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
factories = {};
results = {};
defs = groupBy(defs, 'name');
eager = [];
- resolver = function(name) {
- var controller, d, def, mapping, resolve, _i, _len, _ref;
- def = {};
- if (name && name.controller) {
- controller = name.controller;
- name = getName(controller);
- }
- /*
- Important note: jQuery.is is required to use a controller selector.
- */
- _ref = defs[name] || [];
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- d = _ref[_i];
- if (!controller || !d.controller || controller.element.is(d.controller)) {
- D.extend(true, def, d);
- }
- }
+ resolver = function(obj) {
+ var controller, def, mapping, resolve;
+ def = definition(obj);
+ controller = def.controllerInstance;
mapping = mapper(def);
return resolve = function(name) {
- var args, get, parts, path, realName, sub;
- sub = function(name) {
- return controller && substitute(name, controller.options) || name;
- };
- get = function(path) {
- if (!controller) {
- throw new Error("parameterized factories can only be used on controllers. Cannot resolve '" + path + "' for '" + name + "' AKA '" + realName + "'");
+ var factory, plugin, realName, _i, _len;
+ realName = mapping(name);
+ factory = factories[realName];
+ for (_i = 0, _len = PLUGINS.length; _i < _len; _i++) {
+ plugin = PLUGINS[_i];
+ if (plugin.resolveFactory) {
+ factory = plugin.resolveFactory(obj, realName, def) || factory;
}
- return D.getObject(path, [controller.options]);
- };
- parts = factoryName.exec(mapping(sub(name)));
- realName = parts[1];
- args = (function() {
- var _j, _len2, _ref2, _ref3, _ref4, _results;
- _ref4 = (_ref2 = (_ref3 = parts[3]) != null ? _ref3.split(',') : void 0) != null ? _ref2 : [];
- _results = [];
- for (_j = 0, _len2 = _ref4.length; _j < _len2; _j++) {
- path = _ref4[_j];
- if (path) _results.push(get(path));
- }
- return _results;
- })();
- if (!factories[realName]) {
+ }
+ if (!factory) {
throw new Error("Cannot resolve '" + realName + "' AKA '" + name + "'");
}
- return factories[realName].apply(this, args);
+ return factory.call(this);
};
};
- injector = whenInjected(resolver);
+ definition = function(target) {
+ var context, d, def, definitions, name, plugin, _i, _j, _len, _len1;
+ context = last(CONTEXT);
+ name = context.name || getName(target);
+ def = {};
+ definitions = (defs[name] || []).slice(0);
+ for (_i = 0, _len = PLUGINS.length; _i < _len; _i++) {
+ plugin = PLUGINS[_i];
+ if (plugin.processDefinition) {
+ definitions.push(plugin.processDefinition(target, definitions) || {});
+ }
+ }
+ for (_j = 0, _len1 = definitions.length; _j < _len1; _j++) {
+ d = definitions[_j];
+ D.extend(true, def, d);
+ }
+ return def;
+ };
+ injector = whenInjected(resolver, definition);
for (name in defs) {
configs = defs[name];
def = {};
@@ -121,14 +109,20 @@
d = configs[_i];
D.extend(true, def, d);
}
- _ref = makeFactory(def), name = _ref[0], factory = _ref[1];
- if (def.eager) eager.push(factory);
+ name = def.name;
+ factory = def.factory;
+ if (def.eager) {
+ eager.push(factory);
+ }
factories[name] = factory;
}
- useInjector(injector, function() {
- var factory, _j, _len2, _results;
+ useInjector({
+ injector: injector,
+ definition: definition
+ }, function() {
+ var _j, _len1, _results;
_results = [];
- for (_j = 0, _len2 = eager.length; _j < _len2; _j++) {
+ for (_j = 0, _len1 = eager.length; _j < _len1; _j++) {
factory = eager[_j];
_results.push(factory());
}
@@ -145,8 +139,10 @@
injectCurrent = function() {
var context, injected;
context = last(CONTEXT);
- if (!context) noContext();
- injected = context.named(name).apply(this, args);
+ if (!context) {
+ noContext();
+ }
+ injected = context.injector.named(name).apply(this, args);
return injected.apply(this, arguments);
};
injectCurrent.andReturn = andReturn;
@@ -172,7 +168,9 @@
inject.useCurrent = function(fn, ignoreNoContext) {
var context;
context = last(CONTEXT);
- if (!(context || ignoreNoContext)) noContext();
+ if (!(context || ignoreNoContext)) {
+ noContext();
+ }
if (context) {
return useInjector(context, fn);
} else {
@@ -232,54 +230,28 @@
return singleton;
};
- inject.setupControllerActions = function() {
- var action, funcName, _ref;
- _ref = getClass(this).actions;
- for (funcName in _ref) {
- action = _ref[funcName];
- this[funcName] = inject.useCurrent(this[funcName]);
- }
- return this._super.apply(this, arguments);
- };
-
- makeFactory = function(def) {
- var fn, fullName, name, params, _ref;
- _ref = factoryName.exec(def.name), fullName = _ref[0], name = _ref[1], params = _ref[2];
- fn = def.factory;
- return [
- name, function() {
- if (!fn) {
- throw new Error("" + fullName + " does not have a factory function so it cannot be injected into a function.");
- }
- if (arguments.length && !params) {
- throw new Error("" + fullName + " is not a parameterized factory, it cannot take arguments. If you want to pass it arguments, the name must end with '()'.");
- }
- return fn.apply(this, arguments);
- }
- ];
- };
-
- substitute = function(string, options) {
- return string.replace(/\{(.+?)\}/g, function(param, name) {
- return D.getObject(name, [options]);
- });
- };
-
- whenInjected = function(resolver) {
+ whenInjected = function(resolver, definition) {
var destroyed, injector, injectorFor;
destroyed = false;
injectorFor = function(name) {
var requires;
return requires = function() {
- var dependencies, fn, injected, _i;
+ var dependencies, fn, injectContext, injected, _i;
dependencies = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), fn = arguments[_i++];
- fn = useInjector(injector, fn);
- injected = useInjector(injector, function() {
+ injectContext = {
+ injector: injector,
+ name: name,
+ definition: definition
+ };
+ fn = useInjector(injectContext, fn);
+ injected = useInjector(injectContext, function() {
var args, d, deferreds, resolve, target;
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
- if (destroyed) return;
+ if (destroyed) {
+ return;
+ }
target = this;
- resolve = resolver(name || nameOf(target));
+ resolve = resolver(target);
try {
deferreds = (function() {
var _j, _len, _results;
@@ -295,7 +267,9 @@
throw e;
}
return D.when.apply(D, deferreds.concat(args)).pipe(function() {
- if (!destroyed) return fn.apply(target, arguments);
+ if (!destroyed) {
+ return fn.apply(target, arguments);
+ }
});
});
injected.andReturn = andReturn;
@@ -328,19 +302,9 @@
};
};
- nameOf = function(target) {
- if (target.element && getClass(target)) {
- return {
- controller: target
- };
- } else {
- return getName(target);
- }
- };
-
getName = function(target) {
- var _ref, _ref2, _ref3;
- return (target != null ? (_ref = target.options) != null ? (_ref2 = _ref.inject) != null ? _ref2.name : void 0 : void 0 : void 0) || ((_ref3 = getClass(target)) != null ? _ref3.fullName : void 0);
+ var _ref;
+ return (_ref = getClass(target)) != null ? _ref.fullName : void 0;
};
getClass = function(target) {
@@ -356,15 +320,19 @@
};
matchArgs = function(results, args, del) {
- var i, miss, result, _len;
- if (!results) return;
- for (i = 0, _len = results.length; i < _len; i++) {
+ var i, miss, result, _i, _len;
+ if (!results) {
+ return;
+ }
+ for (i = _i = 0, _len = results.length; _i < _len; i = ++_i) {
result = results[i];
miss = find(result.args || [], function(index, arg) {
return args[index] !== arg;
});
if (!miss) {
- if (del) delete result[i];
+ if (del) {
+ delete result[i];
+ }
return result;
}
}
@@ -372,6 +340,51 @@
exports.Inject = inject;
+ /*
+ Plugins can define 3 methods:
+
+ * init(pluginSupport) - passed the plugin support object, which has some helper functions.
+ * processDefinition(target,definitions) -
+ can return an additional definition object that will override the other definitions.
+ DO NOT call pluginSupport.definition inside this method.
+ * resolveFactory(target,name,targetDefinition) -
+ can return a factory function that will override the defined factory.
+ */
+
+
+ pluginSupport = {
+ /*
+ Helper for getting a copy of the definition used to inject the given object in the current context.
+
+ @param {Object|String} target the thing to be injected, or its name. Some plugins may
+ return a different definition for an instance than they would for the name.
+ @return {Object} a copy of the injection definition for target in the current injector.
+ */
+
+ definition: function(target) {
+ var context;
+ context = last(CONTEXT);
+ if (!context) {
+ noContext();
+ }
+ if (typeof target === 'string') {
+ target = {
+ Class: {
+ fullName: target
+ }
+ };
+ }
+ return context.definition(target);
+ }
+ };
+
+ inject.plugin = function(plugin) {
+ PLUGINS.push(plugin);
+ if (plugin.init) {
+ return plugin.init(pluginSupport);
+ }
+ };
+
groupBy = function(array, fn) {
var e, key, obj, prop, _i, _len;
prop = fn;
@@ -398,15 +411,17 @@
};
find = function(array, fn, context) {
- var index, value, _len;
+ var index, value, _i, _len;
if (fn == null) {
fn = function(it) {
return it;
};
}
- for (index = 0, _len = array.length; index < _len; index++) {
+ for (index = _i = 0, _len = array.length; _i < _len; index = ++_i) {
value = array[index];
- if (fn.call(context, value, index)) return value;
+ if (fn.call(context, value, index)) {
+ return value;
+ }
}
};
View
4 inject.coffee
@@ -1,4 +1,4 @@
if steal.plugins
- steal.plugins('jquery','jquery/lang')('inject-core.js')
+ steal.plugins('jquery')('//inject/inject-core.js')('//inject/controller.js')
else
- steal('jquery','jquery/lang','./inject-core.js')
+ steal('jquery','jquery/lang','./inject-core.js','./controller.js')
View
5 inject.js
@@ -1,9 +1,10 @@
+// Generated by CoffeeScript 1.3.3
(function() {
if (steal.plugins) {
- steal.plugins('jquery', 'jquery/lang')('inject-core.js');
+ steal.plugins('jquery', 'jquery/lang')('inject-core.js','controller.js');
} else {
- steal('jquery', 'jquery/lang', './inject-core.js');
+ steal('jquery', 'jquery/lang', './inject-core.js','./controller.js');
}
}).call(this);
View
53 test/qunit/inject_test.coffee
@@ -171,7 +171,7 @@ test "options substitution", ->
test "parameterized factories", ->
injector = Inject({
- name: 'foo()'
+ name: 'foo'
factory: (input) ->
def = $.Deferred()
setTimeout ->
@@ -473,3 +473,54 @@ test "andReturn", ->
equals(456,bar)
false
)(456)
+
+
+test 'injecting options', ->
+ expect 1
+ $.Controller('Foo',{
+ setup: Inject.setupController
+ },{
+ init: ->
+ equals(this.options.foo + this.options.bar,'Hello Bob!')
+ })
+
+ Inject({
+ name: 'bar',
+ factory: -> 'Hello '
+ },{
+ name: 'baz',
+ factory: -> 'World!'
+ },{
+ name: 'Foo',
+ options:
+ foo: 'bar',
+ bar: 'baz'
+ })(->
+ $('.testThing').foo({bar:'Bob!'})
+ ).call(this)
+
+
+test 'injecting attrs', ->
+ expect 1
+ $.Model('Foo',{
+ setup: Inject.setup
+ },{
+ init: -> @baz = @foo + @bar
+ })
+
+ Inject({
+ name: 'bar',
+ factory: -> 'Hello '
+ },{
+ name: 'baz',
+ factory: -> 'World!'
+ },{
+ name: 'Foo',
+ attrs: {
+ foo: 'bar',
+ bar: 'baz'
+ }
+ })(->
+ new Foo({bar:'Bob!'}).done (foo) ->
+ equals(foo.baz,'Hello Bob!')
+ ).call(this);
View
73 test/qunit/inject_test.js
@@ -1,3 +1,4 @@
+// Generated by CoffeeScript 1.3.3
(function() {
module("inject", {
@@ -204,7 +205,7 @@
test("parameterized factories", function() {
var injector;
injector = Inject({
- name: 'foo()',
+ name: 'foo',
factory: function(input) {
var def;
def = $.Deferred();
@@ -304,6 +305,7 @@
sharing context is as simple as using the shared context to inject
factories in another context
*/
+
contextA = Inject({
name: 'bar',
factory: shared('sharedFoo', function(foo) {
@@ -316,6 +318,7 @@
name: 'foo',
/* the shared context can be used as a factory in another context
*/
+
factory: shared('sharedFoo', function(foo) {
return foo;
})
@@ -340,6 +343,7 @@
however, Inject.require() must be on the outside if you want it to inject from the injector being defined
note the resulting order of the arguments
*/
+
factory: Inject.require('foo2', shared('sharedFoo', function(foo, foo2) {
return String(foo.qux) + String(foo2.qux);
}))
@@ -515,6 +519,7 @@
$.Controller('TestController4', {}, {
/* this is the important part
*/
+
setup: Inject.setupControllerActions,
".foo click": Inject.require('foo', function(foo) {
return equals(foo, expected);
@@ -569,4 +574,70 @@
})(456));
});
+ test('injecting options', function() {
+ expect(1);
+ $.Controller('Foo', {
+ setup: Inject.setupController
+ }, {
+ init: function() {
+ return equals(this.options.foo + this.options.bar, 'Hello Bob!');
+ }
+ });
+ return Inject({
+ name: 'bar',
+ factory: function() {
+ return 'Hello ';
+ }
+ }, {
+ name: 'baz',
+ factory: function() {
+ return 'World!';
+ }
+ }, {
+ name: 'Foo',
+ options: {
+ foo: 'bar',
+ bar: 'baz'
+ }
+ })(function() {
+ return $('.testThing').foo({
+ bar: 'Bob!'
+ });
+ }).call(this);
+ });
+
+ test('injecting attrs', function() {
+ expect(1);
+ $.Model('Foo', {
+ setup: Inject.setup
+ }, {
+ init: function() {
+ return this.baz = this.foo + this.bar;
+ }
+ });
+ return Inject({
+ name: 'bar',
+ factory: function() {
+ return 'Hello ';
+ }
+ }, {
+ name: 'baz',
+ factory: function() {
+ return 'World!';
+ }
+ }, {
+ name: 'Foo',
+ attrs: {
+ foo: 'bar',
+ bar: 'baz'
+ }
+ })(function() {
+ return new Foo({
+ bar: 'Bob!'
+ }).done(function(foo) {
+ return equals(foo.baz, 'Hello Bob!');
+ });
+ }).call(this);
+ });
+
}).call(this);
View
2  test/qunit/qunit.js
@@ -1,3 +1,3 @@
steal
- .plugins("funcunit/qunit", "inject",'jquery/controller')
+ .plugins("funcunit/qunit", "inject",'jquery/controller','jquery/model')
.then("inject_test");
Please sign in to comment.
Something went wrong with that request. Please try again.