From ed36b9da3be338fe9eb36f3eeea901d6f51cd768 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Tue, 1 Nov 2011 21:09:54 -0700 Subject: refactor(injector): switch to injector 2.0 introduce modules --- example/personalLog/test/personalLogSpec.js | 4 +- src/Angular.js | 36 +++++-- src/Injector.js | 162 ++++++---------------------- src/scenario/Runner.js | 2 +- src/scenario/dsl.js | 8 +- src/service/compiler.js | 9 +- src/widget/form.js | 4 +- src/widget/input.js | 14 +-- src/widget/select.js | 4 +- src/widgets.js | 12 +-- test/AngularSpec.js | 29 ++--- test/BinderSpec.js | 16 +-- test/InjectorSpec.js | 120 ++++++++++++--------- test/ResourceSpec.js | 6 +- test/markupSpec.js | 2 +- test/scenario/dslSpec.js | 2 +- test/service/compilerSpec.js | 10 +- test/service/cookiesSpec.js | 4 +- test/service/deferSpec.js | 4 +- test/service/exceptionHandlerSpec.js | 6 +- test/service/locationSpec.js | 18 ++-- test/service/logSpec.js | 8 +- test/service/scopeSpec.js | 18 ++-- test/service/xhr.bulkSpec.js | 10 +- test/service/xhr.cacheSpec.js | 12 +-- test/service/xhr.errorSpec.js | 8 +- test/service/xhrSpec.js | 8 +- test/testabilityPatch.js | 28 +---- test/widget/inputSpec.js | 6 +- test/widgetsSpec.js | 2 +- 30 files changed, 239 insertions(+), 333 deletions(-) diff --git a/example/personalLog/test/personalLogSpec.js b/example/personalLog/test/personalLogSpec.js index cf80a420..2e93925c 100644 --- a/example/personalLog/test/personalLogSpec.js +++ b/example/personalLog/test/personalLogSpec.js @@ -2,7 +2,7 @@ describe('example.personalLog.LogCtrl', function() { var logCtrl; function createNotesCtrl() { - var injector = angular.injector(); + var injector = angular.injector('NG'); var scope = injector('$rootScope'); scope.$cookies = injector('$cookies'); return scope.$new(example.personalLog.LogCtrl); @@ -121,4 +121,4 @@ describe('example.personalLog.LogCtrl', function() { expect(logCtrl.$cookies.logs).not.toBeDefined(); }); }); -}); \ No newline at end of file +}); diff --git a/src/Angular.js b/src/Angular.js index bbd43d3b..4344425a 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -100,6 +100,7 @@ var _undefined = undefined, /** @name angular */ angular = window[$angular] || (window[$angular] = {}), + angularModules = angular.modules || (angular.modules = {}), /** @name angular.markup */ angularTextMarkup = extensionMap(angular, 'markup'), /** @name angular.attrMarkup */ @@ -274,6 +275,7 @@ function inherit(parent, extra) { */ function noop() {} +noop.$inject = []; /** @@ -292,6 +294,7 @@ function noop() {} */ function identity($) {return $;} +identity.$inject = []; function valueFn(value) {return function() {return value;};} @@ -945,14 +948,20 @@ function encodeUriQuery(val, pctEncodeSpaces) { */ function angularInit(config, document){ var autobind = config.autobind; - + if (autobind) { - var element = isString(autobind) ? document.getElementById(autobind) : document, - injector = createInjector(angularService), - scope = injector('$rootScope'); - - injector('$compile')(element)(scope); - scope.$apply(); + var modules = [ngModule]; + forEach((config.modules || '').split(','), function(module){ + module = trim(module); + if (module) { + modules.push(module); + } + }); + createInjector(modules, angularModules)(['$rootScope', '$compile', function(scope, compile){ + scope.$apply(function(){ + compile(isString(autobind) ? document.getElementById(autobind) : document)(scope); + }); + }]); } } @@ -1017,13 +1026,11 @@ function assertArgFn(arg, name) { function publishExternalAPI(angular){ extend(angular, { - // disabled for now until we agree on public name - //'annotate': annotate, 'copy': copy, 'extend': extend, 'equals': equals, 'forEach': forEach, - 'injector': createInjector, + 'injector': function(){ return createInjector(arguments, angularModules); }, 'noop':noop, 'bind':bind, 'toJson': toJson, @@ -1041,6 +1048,15 @@ function publishExternalAPI(angular){ 'lowercase': lowercase, 'uppercase': uppercase }); + + angularModules.NG = ngModule; +} + +ngModule.$inject = ['$provide']; +function ngModule($provide) { + forEach(angularService, function(factory, name){ + $provide.factory(name, factory); + }); } diff --git a/src/Injector.js b/src/Injector.js index fe0cc5e9..12c2ffa6 100644 --- a/src/Injector.js +++ b/src/Injector.js @@ -22,108 +22,16 @@ * The injector function also has these properties: * * * An `invoke` property which can be used to invoke methods with dependency-injected arguments. - * `injector.invoke(self, fn, curryArgs)` + * `injector.invoke(self, fn, locals)` * * `self` - The "`this`" to be used when invoking the function. * * `fn` - The function to be invoked. The function may have the `$inject` property that * lists the set of arguments which should be auto-injected. * (see {@link guide/dev_guide.di dependency injection}). - * * `curryArgs(array)` - Optional array of arguments to pass to the function - * invocation after the injection arguments (also known as curry arguments or currying). + * * `locals(array)` - Optional array of arguments to pass to the function + * invocation after the injection arguments. * * An `eager` property which is used to initialize the eager services. * `injector.eager()` */ -function createInjector(factories) { - var instanceCache = { - $injector: injector - }; - factories = factories || angularService; - - injector.invoke = invoke; - - forEach(factories, function(factory, name){ - if (factory.$eager) - injector(name); - }); - return instanceCache.$injector; - - function injector(serviceId, path){ - if (typeof serviceId == 'string') { - if (!(serviceId in instanceCache)) { - var factory = factories[serviceId]; - path = path || []; - path.unshift(serviceId); - if (!factory) throw Error("Unknown provider for '" + path.join("' <- '") + "'."); - inferInjectionArgs(factory); - instanceCache[serviceId] = invoke(null, factory, [], path); - path.shift(); - } - return instanceCache[serviceId]; - } else { - return invoke(null, serviceId, path); - } - } - - function invoke(self, fn, args, path){ - args = args || []; - var injectNames; - var i; - if (typeof fn == 'function') { - injectNames = fn.$inject || []; - i = injectNames.length; - } else if (fn instanceof Array) { - injectNames = fn; - i = injectNames.length; - fn = injectNames[--i]; - } - assertArgFn(fn, 'fn'); - while(i--) { - args.unshift(injector(injectNames[i], path)); - } - return fn.apply(self, args); - } -} - -/** - * THIS IS NOT PUBLIC DOC YET! - * - * @name angular.annotate - * @function - * - * @description - * Annotate the function with injection arguments. This is equivalent to setting the `$inject` - * property as described in {@link guide.di dependency injection}. - * - *
- * var MyController = angular.annotate('$location', function($location){ ... });
- *
- *
- * is the same as
- *
- *
- * var MyController = function($location){ ... };
- * MyController.$inject = ['$location'];
- *
- *
- * @param {String|Array} serviceName... zero or more service names to inject into the
- * `annotatedFunction`.
- * @param {function} annotatedFunction function to annotate with `$inject`
- * functions.
- * @returns {function} `annotatedFunction`
- */
-function annotate(services, fn) {
- if (services instanceof Array) {
- fn.$inject = services;
- return fn;
- } else {
- var i = 0,
- length = arguments.length - 1, // last one is the destination function
- $inject = arguments[length].$inject = [];
- for (; i < length; i++) {
- $inject.push(arguments[i]);
- }
- return arguments[length]; // return the last one
- }
-}
function angularServiceInject(name, fn, inject, eager) {
angularService(name, fn, {$inject:inject, $eager:eager});
@@ -156,22 +64,16 @@ function inferInjectionArgs(fn) {
}
///////////////////////////////////////
-function createInjector2(modulesToLoad, moduleRegistry) {
+function createInjector(modulesToLoad, moduleRegistry) {
var cache = {},
$injector = internalInjector(cache),
providerSuffix = 'Provider',
providerSuffixLength = providerSuffix.length;
- function $provide(name) {
- var provider = cache['#' + name + providerSuffix];
- if (provider) {
- return provider;
- } else {
- throw Error("No provider for: " + name);
- }
- }
+ value('$injector', $injector);
+ value('$provide', {service: service, factory: factory, value: value});
- $provide.service = function(name, provider) {
+ function service(name, provider) {
if (isFunction(provider)){
provider = $injector.instantiate(provider);
}
@@ -180,11 +82,8 @@ function createInjector2(modulesToLoad, moduleRegistry) {
}
cache['#' + name + providerSuffix] = provider;
};
- $provide.factory = function(name, factoryFn) { $provide.service(name, { $get:factoryFn }); };
- $provide.value = function(name, value) { $provide.factory(name, valueFn(value)); };
-
- $provide.value('$injector', $injector);
- $provide.value('$provide', $provide);
+ function factory(name, factoryFn) { service(name, { $get:factoryFn }); };
+ function value(name, value) { factory(name, valueFn(value)); };
function internalInjector(cache) {
var path = [];
@@ -194,9 +93,10 @@ function createInjector2(modulesToLoad, moduleRegistry) {
case 'function':
return invoke(null, value);
case 'string':
- var instanceKey = '#' + value;
- if (cache[instanceKey]) {
- return cache[instanceKey];
+ var instanceKey = '#' + value,
+ instance = cache[instanceKey];
+ if (instance !== undefined || cache.hasOwnProperty(instanceKey)) {
+ return instance;
}
try {
path.unshift(value);
@@ -219,28 +119,32 @@ function createInjector2(modulesToLoad, moduleRegistry) {
}
}
- function invoke(self, fn){
+ function invoke(self, fn, locals){
var args = [],
$inject,
- length;
- switch(typeof fn){
- case 'function':
- $inject = inferInjectionArgs(fn);
+ length,
+ key;
+
+ if (fn instanceof Function) {
+ $inject = inferInjectionArgs(fn);
+ length = $inject.length;
+ } else {
+ if (fn instanceof Array) {
+ $inject = fn;
length = $inject.length;
- break;
- case 'object':
- if (typeof fn.length == 'number') {
- $inject = fn;
- length = $inject.length;
- fn = $inject[--length];
- }
- default:
- assertArgFn(fn, 'fn');
- };
+ fn = $inject[--length];
+ }
+ assertArgFn(fn, 'fn');
+ }
while(length--) {
- args.unshift(injector($inject[length], path));
+ key = $inject[length];
+ args.unshift(
+ locals && locals.hasOwnProperty(key)
+ ? locals[key]
+ : injector($inject[length], path)
+ );
}
switch (self ? -1 : args.length) {
diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js
index d7e1a82b..fba076a8 100644
--- a/src/scenario/Runner.js
+++ b/src/scenario/Runner.js
@@ -163,7 +163,7 @@ angular.scenario.Runner.prototype.createSpecRunner_ = function(scope) {
*/
angular.scenario.Runner.prototype.run = function(application) {
var self = this;
- var $root = angular.injector()('$rootScope');
+ var $root = angular.injector('NG')('$rootScope');
angular.extend($root, this);
angular.forEach(angular.scenario.Runner.prototype, function(fn, name) {
$root[name] = angular.bind(self, fn);
diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js
index 13ae8b8f..97749787 100644
--- a/src/scenario/dsl.js
+++ b/src/scenario/dsl.js
@@ -103,25 +103,25 @@ angular.scenario.dsl('browser', function() {
api.url = function() {
return this.addFutureAction('$location.url()', function($window, $document, done) {
- done(null, $window.angular.injector()('$location').url());
+ done(null, $window.angular.injector('NG')('$location').url());
});
};
api.path = function() {
return this.addFutureAction('$location.path()', function($window, $document, done) {
- done(null, $window.angular.injector()('$location').path());
+ done(null, $window.angular.injector('NG')('$location').path());
});
};
api.search = function() {
return this.addFutureAction('$location.search()', function($window, $document, done) {
- done(null, $window.angular.injector()('$location').search());
+ done(null, $window.angular.injector('NG')('$location').search());
});
};
api.hash = function() {
return this.addFutureAction('$location.hash()', function($window, $document, done) {
- done(null, $window.angular.injector()('$location').hash());
+ done(null, $window.angular.injector('NG')('$location').hash());
});
};
diff --git a/src/service/compiler.js b/src/service/compiler.js
index c1728f7d..0bd3e54d 100644
--- a/src/service/compiler.js
+++ b/src/service/compiler.js
@@ -22,14 +22,15 @@ angularServiceInject('$compile', function($injector, $exceptionHandler, $textMar
Template.prototype = {
link: function(element, scope) {
- var childScope = scope;
+ var childScope = scope,
+ locals = {$element: element};
if (this.newScope) {
childScope = isFunction(this.newScope) ? scope.$new(this.newScope(scope)) : scope.$new();
element.data($$scope, childScope);
}
forEach(this.linkFns, function(fn) {
try {
- childScope.$service.invoke(childScope, fn, [element]);
+ $injector.invoke(childScope, fn, locals);
} catch (e) {
$exceptionHandler(e);
}
@@ -54,6 +55,10 @@ angularServiceInject('$compile', function($injector, $exceptionHandler, $textMar
addLinkFn:function(linkingFn) {
if (linkingFn) {
+ //TODO(misko): temporary hack.
+ if (isFunction(linkingFn) && !linkingFn.$inject) {
+ linkingFn.$inject = ['$element'];
+ }
this.linkFns.push(linkingFn);
}
},
diff --git a/src/widget/form.js b/src/widget/form.js
index 065ee74d..4608efc1 100644
--- a/src/widget/form.js
+++ b/src/widget/form.js
@@ -56,7 +56,7 @@
angularWidget('form', function(form){
this.descend(true);
this.directives(true);
- return annotate('$formFactory', function($formFactory, formElement) {
+ return ['$formFactory', '$element', function($formFactory, formElement) {
var name = formElement.attr('name'),
parentForm = $formFactory.forElement(formElement),
form = $formFactory(parentForm);
@@ -74,7 +74,7 @@ angularWidget('form', function(form){
formElement[value ? 'addClass' : 'removeClass']('ng-' + name);
});
}
- });
+ }];
});
angularWidget('ng:form', angularWidget('form'));
diff --git a/src/widget/input.js b/src/widget/input.js
index cf29d0f1..5108a4e4 100644
--- a/src/widget/input.js
+++ b/src/widget/input.js
@@ -569,7 +569,7 @@ angularInputType('radio', function(inputElement) {
function numericRegexpInputType(regexp, error) {
- return function(inputElement) {
+ return ['$element', function(inputElement) {
var widget = this,
min = 1 * (inputElement.attr('min') || Number.MIN_VALUE),
max = 1 * (inputElement.attr('max') || Number.MAX_VALUE);
@@ -598,7 +598,7 @@ function numericRegexpInputType(regexp, error) {
widget.$viewValue = '' + widget.$modelValue;
}
};
- };
+ }];
}
@@ -713,7 +713,7 @@ angularWidget('input', function(inputElement){
this.descend(true);
var modelExp = inputElement.attr('ng:model');
return modelExp &&
- annotate('$defer', '$formFactory', function($defer, $formFactory, inputElement){
+ ['$defer', '$formFactory', '$element', function($defer, $formFactory, inputElement){
var form = $formFactory.forElement(inputElement),
// We have to use .getAttribute, since jQuery tries to be smart and use the
// type property. Trouble is some browser change unknown to text.
@@ -761,14 +761,16 @@ angularWidget('input', function(inputElement){
}
}
- !TypeController.$inject && (TypeController.$inject = []);
+ //TODO(misko): setting $inject is a hack
+ !TypeController.$inject && (TypeController.$inject = ['$element']);
widget = form.$createWidget({
scope: modelScope,
model: modelExp,
onChange: inputElement.attr('ng:change'),
alias: inputElement.attr('name'),
controller: TypeController,
- controllerArgs: [inputElement]});
+ controllerArgs: {$element: inputElement}
+ });
watchElementProperty(this, widget, 'value', inputElement);
watchElementProperty(this, widget, 'required', inputElement);
@@ -830,7 +832,7 @@ angularWidget('input', function(inputElement){
}
});
}
- });
+ }];
});
diff --git a/src/widget/select.js b/src/widget/select.js
index 2e328b26..c70b5ded 100644
--- a/src/widget/select.js
+++ b/src/widget/select.js
@@ -131,7 +131,7 @@ angularWidget('select', function(element){
this.directives(true);
this.descend(true);
return element.attr('ng:model') &&
- annotate('$formFactory', '$compile', function($formFactory, $compile, selectElement){
+ ['$formFactory', '$compile', '$element', function($formFactory, $compile, selectElement){
var modelScope = this,
match,
form = $formFactory.forElement(selectElement),
@@ -433,5 +433,5 @@ angularWidget('select', function(element){
}
};
}
- });
+ }];
});
diff --git a/src/widgets.js b/src/widgets.js
index 11d9a2f0..6ecd27a1 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -90,7 +90,7 @@ angularWidget('ng:include', function(element){
this.directives(true);
} else {
element[0]['ng:compiled'] = true;
- return extend(function(xhr, element){
+ return ['$xhr.cache', '$element', function(xhr, element){
var scope = this,
changeCounter = 0,
releaseScopes = [],
@@ -129,7 +129,7 @@ angularWidget('ng:include', function(element){
element.html('');
}
});
- }, {$inject:['$xhr.cache']});
+ }];
}
});
@@ -555,7 +555,7 @@ angularWidget('ng:view', function(element) {
if (!element[0]['ng:compiled']) {
element[0]['ng:compiled'] = true;
- return annotate('$xhr.cache', '$route', function($xhr, $route, element){
+ return ['$xhr.cache', '$route', '$element', function($xhr, $route, element){
var template;
var changeCounter = 0;
@@ -578,7 +578,7 @@ angularWidget('ng:view', function(element) {
element.html('');
}
});
- });
+ }];
} else {
compiler.descend(true);
compiler.directives(true);
@@ -759,7 +759,7 @@ angularWidget('ng:pluralize', function(element) {
whenExp = element.attr('when'),
offset = element.attr('offset') || 0;
- return annotate('$locale', function($locale, element) {
+ return ['$locale', '$element', function($locale, element) {
var scope = this,
whens = scope.$eval(whenExp),
whensExpFns = {};
@@ -783,5 +783,5 @@ angularWidget('ng:pluralize', function(element) {
}, function(scope, newVal) {
element.text(newVal);
});
- });
+ }];
});
diff --git a/test/AngularSpec.js b/test/AngularSpec.js
index a957fcf7..8902c2a9 100644
--- a/test/AngularSpec.js
+++ b/test/AngularSpec.js
@@ -375,9 +375,9 @@ describe('angular', function() {
describe('angular service', function() {
- it('should override services', inject(function(service){
- service('fake', function() { return 'old'; });
- service('fake', function() { return 'new'; });
+ it('should override services', inject(function($provide){
+ $provide.value('fake', 'old');
+ $provide.value('fake', 'new');
}, function(fake) {
expect(fake).toEqual('new');
}));
@@ -401,22 +401,23 @@ describe('angular', function() {
expect(result.third).toBeTruthy();
});
- it('should inject dependencies specified by $inject', function() {
- angular.service('svc1', function() { return 'svc1'; });
- angular.service('svc2', function(svc1) { return 'svc2-' + svc1; }, {$inject: ['svc1']});
- expect(createInjector()('svc2')).toEqual('svc2-svc1');
- });
-
it('should inject dependencies specified by $inject and ignore function argument name', function() {
- angular.service('svc1', function() { return 'svc1'; });
- angular.service('svc2', function(foo) { return 'svc2-' + foo; }, {$inject: ['svc1']});
- expect(createInjector()('svc2')).toEqual('svc2-svc1');
+ expect(angular.injector(function($provide){
+ $provide.factory('svc1', function() { return 'svc1'; });
+ $provide.factory('svc2', ['svc1', function(s) { return 'svc2-' + s; }]);
+ })('svc2')).toEqual('svc2-svc1');
});
it('should eagerly instantiate a service if $eager is true', function() {
var log = [];
- angular.service('svc1', function() { log.push('svc1'); }, {$eager: true});
- createInjector(angularService);
+ angular.injector(function($provide){
+ $provide.service('svc1', function() {
+ this.$get = function(){
+ log.push('svc1');
+ }
+ this.$eager = true;
+ });
+ });
expect(log).toEqual(['svc1']);
});
});
diff --git a/test/BinderSpec.js b/test/BinderSpec.js
index 6a459074..495d98a8 100644
--- a/test/BinderSpec.js
+++ b/test/BinderSpec.js
@@ -222,8 +222,8 @@ describe('Binder', function() {
}));
it('IfTextBindingThrowsErrorDecorateTheSpan', inject(
- function(service){
- service('$exceptionHandler', $exceptionHandlerMockFactory);
+ function($provide){
+ $provide.factory('$exceptionHandler', $exceptionHandlerMockFactory);
},
function($rootScope, $exceptionHandler, $compile) {
$compile('