diff options
| author | Misko Hevery | 2011-10-31 15:33:52 -0700 |
|---|---|---|
| committer | Misko Hevery | 2011-11-14 16:39:32 -0800 |
| commit | c925f8a6578e05c8136c206f2fd98eeaaf1c0f16 (patch) | |
| tree | e0211ce36aabe753fd633874ea5338612ab9f9fb | |
| parent | 4c10d33eb4340d6df30e298d63f4d37c25aab65f (diff) | |
| download | angular.js-c925f8a6578e05c8136c206f2fd98eeaaf1c0f16.tar.bz2 | |
new(injector): new injector v2.0
- not connected to keep the CL small
| -rw-r--r-- | src/Angular.js | 2 | ||||
| -rw-r--r-- | src/Injector.js | 139 | ||||
| -rw-r--r-- | test/AngularSpec.js | 2 | ||||
| -rw-r--r-- | test/InjectorSpec.js | 281 | ||||
| -rw-r--r-- | test/testabilityPatch.js | 2 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 2 |
6 files changed, 412 insertions, 16 deletions
diff --git a/src/Angular.js b/src/Angular.js index 29199f9b..bbd43d3b 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -948,7 +948,7 @@ function angularInit(config, document){ if (autobind) { var element = isString(autobind) ? document.getElementById(autobind) : document, - injector = createInjector(), + injector = createInjector(angularService), scope = injector('$rootScope'); injector('$compile')(element)(scope); diff --git a/src/Injector.js b/src/Injector.js index 624246af..fe0cc5e9 100644 --- a/src/Injector.js +++ b/src/Injector.js @@ -154,3 +154,142 @@ function inferInjectionArgs(fn) { } return fn.$inject; } + +/////////////////////////////////////// +function createInjector2(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); + } + } + + $provide.service = function(name, provider) { + if (isFunction(provider)){ + provider = $injector.instantiate(provider); + } + if (!provider.$get) { + throw Error('Providers must define $get factory method.'); + } + 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 internalInjector(cache) { + var path = []; + + function injector(value) { + switch(typeof value) { + case 'function': + return invoke(null, value); + case 'string': + var instanceKey = '#' + value; + if (cache[instanceKey]) { + return cache[instanceKey]; + } + try { + path.unshift(value); + var providerKey = instanceKey + providerSuffix, + provider = cache[providerKey]; + if (provider) { + return cache[instanceKey] = invoke(provider, provider.$get); + } else { + throw Error("Unknown provider for '" + path.join("' <- '") + "'."); + } + } finally { + path.shift(); + } + case 'object': + if (value instanceof Array) { + return invoke(null, value); + } + default: + throw Error('Injector expects name or function.'); + } + } + + function invoke(self, fn){ + var args = [], + $inject, + length; + + switch(typeof fn){ + case 'function': + $inject = inferInjectionArgs(fn); + length = $inject.length; + break; + case 'object': + if (typeof fn.length == 'number') { + $inject = fn; + length = $inject.length; + fn = $inject[--length]; + } + default: + assertArgFn(fn, 'fn'); + }; + + while(length--) { + args.unshift(injector($inject[length], path)); + } + + switch (self ? -1 : args.length) { + case 0: return fn(); + case 1: return fn(args[0]); + case 2: return fn(args[0], args[1]); + case 3: return fn(args[0], args[1], args[2]); + case 4: return fn(args[0], args[1], args[2], args[3]); + case 5: return fn(args[0], args[1], args[2], args[3], args[4]); + case 6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]); + case 7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); + case 8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); + case 9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); + case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); + default: return fn.apply(self, args); + } + }; + + injector.invoke = invoke; + injector.instantiate = function(Type, locals){ + var Constructor = function(){}, + instance; + Constructor.prototype = Type.prototype; + instance = new Constructor(); + return invoke(instance, Type, locals) || instance; + }; + return injector; + } + + + forEach(modulesToLoad, function(module){ + if (isString(module)) { + module = moduleRegistry[module]; + } + if (isFunction(module) || isArray(module)) { + $injector(module); + } else { + assertArgFn(module, 'module'); + } + }); + + // instantiate $eager providers + // for perf we can't do forEach + for(var name in cache) { + var index = name.indexOf(providerSuffix); + if (index == name.length - providerSuffixLength && cache[name].$eager) { + $injector(name.substring(1, index)); + } + } + + return $injector; +} diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 7e6f35fa..a957fcf7 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -416,7 +416,7 @@ describe('angular', function() { it('should eagerly instantiate a service if $eager is true', function() { var log = []; angular.service('svc1', function() { log.push('svc1'); }, {$eager: true}); - createInjector(); + createInjector(angularService); expect(log).toEqual(['svc1']); }); }); diff --git a/test/InjectorSpec.js b/test/InjectorSpec.js index 6ac17876..8c36677d 100644 --- a/test/InjectorSpec.js +++ b/test/InjectorSpec.js @@ -21,7 +21,7 @@ describe('injector', function() { it('should inject providers', function() { providers('a', function() {return 'Mi';}); - providers('b', function(mi){return mi+'sko';}, {$inject:['a']}); + providers('b', function(mi) {return mi+'sko';}, {$inject:['a']}); expect(injector('b')).toEqual('Misko'); }); @@ -57,7 +57,7 @@ describe('injector', function() { }).toThrow("Unknown provider for 'idontexist'."); }); - it('should proved path to the missing provider', function(){ + it('should proved path to the missing provider', function() { expect(function() { injector('idontexist', ['a', 'b']); }).toThrow("Unknown provider for 'idontexist' <- 'a' <- 'b'."); @@ -71,10 +71,10 @@ describe('injector', function() { expect(injector('eager')).toBe('foo'); }); - describe('invoke', function(){ + describe('invoke', function() { var args; - beforeEach(function(){ + beforeEach(function() { args = null; providers('a', function() {return 1;}); providers('b', function() {return 2;}); @@ -94,7 +94,7 @@ describe('injector', function() { }); - it('should treat array as annotations', function(){ + it('should treat array as annotations', function() { injector.invoke({name:"this"}, ['a', 'b', fn], [3, 4]); expect(args).toEqual([{name:'this'}, 1, 2, 3, 4]); }); @@ -105,11 +105,11 @@ describe('injector', function() { }); - it('should fail with errors if not function or array', function(){ - expect(function(){ + it('should fail with errors if not function or array', function() { + expect(function() { injector.invoke({}, {}); }).toThrow("Argument 'fn' is not a function, got Object"); - expect(function(){ + expect(function() { injector.invoke({}, ['a', 123]); }).toThrow("Argument 'fn' is not a function, got number"); }); @@ -133,10 +133,10 @@ describe('injector', function() { $a, // x, <-- looks like an arg but it is a comment b_, /* z, <-- looks like an arg but it is a multi-line comment - function (a, b){} + function (a, b) {} */ _c, - /* {some type} */ d){ extraParans();} + /* {some type} */ d) { extraParans();} expect(inferInjectionArgs($f_n0)).toEqual(['$a', 'b_', '_c', 'd']); expect($f_n0.$inject).toEqual(['$a', 'b_', '_c', 'd']); }); @@ -148,7 +148,7 @@ describe('injector', function() { }); it('should handle args with both $ and _', function() { - function $f_n0($a_){} + function $f_n0($a_) {} expect(inferInjectionArgs($f_n0)).toEqual(['$a_']); expect($f_n0.$inject).toEqual(['$a_']); }); @@ -162,7 +162,7 @@ describe('injector', function() { it('should infer injection on services', function() { var $injector = createInjector({ a: function() { return 'a';}, - b: function(a){ return a + 'b';} + b: function(a) { return a + 'b';} }); expect($injector('b')).toEqual('ab'); }); @@ -180,3 +180,260 @@ describe('injector', function() { }); }); }); + +describe('injector2', function() { + + it('should have $injector', function() { + var $injector = createInjector2(); + expect($injector('$injector')).toBe($injector); + }); + + it('should define module', function() { + var log = ''; + var injector = createInjector2([function($provide) { + $provide.value('value', 'value;'); + $provide.factory('fn', valueFn('function;')); + $provide.service('service', function() { + this.$get = valueFn('service;'); + }); + }, function(valueProvider, fnProvider, serviceProvider) { + log += valueProvider.$get() + fnProvider.$get() + serviceProvider.$get(); + }])(function(value, fn, service) { + log += '->' + value + fn + service; + }); + expect(log).toEqual('value;function;service;->value;function;service;'); + }); + + + describe('module', function() { + it('should provide $injector and $provide even when no module is requested', function() { + var $provide, + $injector = createInjector2([ + angular.extend(function(p) { $provide = p; }, {$inject: ['$provide']}) + ]); + expect($injector('$injector')).toBe($injector); + expect($injector('$provide')).toBe($provide); + }); + + + it('should load multiple function modules and infer inject them', function() { + var a = 'junk'; + var $injector = createInjector2([ + function() { + a = 'A'; // reset to prove we ran + }, + function($provide) { + $provide.value('a', a); + }, + angular.extend(function(p, serviceA) { + p.value('b', serviceA.$get() + 'B' ); + }, {$inject:['$provide', 'aProvider']}), + ['$provide', 'bProvider', function(p, serviceB) { + p.value('c', serviceB.$get() + 'C'); + }] + ]); + expect($injector('a')).toEqual('A'); + expect($injector('b')).toEqual('AB'); + expect($injector('c')).toEqual('ABC'); + }); + + + it('should run symbolic modules', function() { + var $injector = createInjector2(['myModule'], { + myModule: ['$provide', function(provide) { + provide.value('a', 'abc'); + }] + }); + expect($injector('a')).toEqual('abc'); + }); + + + describe('$provide', function() { + describe('value', function(){ + it('should configure $provide values', function() { + expect(createInjector2([function($provide) { + $provide.value('value', 'abc'); + }])('value')).toEqual('abc'); + }); + }); + + + describe('factory', function(){ + it('should configure $provide factory function', function() { + expect(createInjector2([function($provide) { + $provide.factory('value', valueFn('abc')); + }])('value')).toEqual('abc'); + }); + }); + + + describe('service', function(){ + it('should configure $provide service object', function() { + expect(createInjector2([function($provide) { + $provide.service('value', { + $get: valueFn('abc') + }); + }])('value')).toEqual('abc'); + }); + + + it('should configure $provide service type', function() { + function Type() {}; + Type.prototype.$get = function() { + expect(this instanceof Type).toBe(true); + return 'abc'; + }; + expect(createInjector2([function($provide) { + $provide.service('value', Type); + }])('value')).toEqual('abc'); + }); + }); + }); + + + describe('error handling', function() { + it('should handle wrong argument type', function() { + expect(function() { + createInjector2([ + {} + ], {}); + }).toThrow("Argument 'module' is not a function, got Object"); + }); + + + it('should handle exceptions', function() { + expect(function() { + createInjector2([function() { + throw 'MyError'; + }], {}); + }).toThrow('MyError'); + }); + + + it('should handle no module alias', function() { + expect(function() { + createInjector2([function(dontExist) { + }], {}); + }).toThrow("Unknown provider for 'dontExist'."); + }); + }); + }); + + + describe('retrieval', function() { + var instance, + $injector, + $provide; + + beforeEach(function() { + $injector = createInjector2([ ['$provide', function(provide) { + ($provide = provide).value('instance', instance = {name:'angular'}); + }]]); + }); + + + it('should retrieve by name and cache instance', function() { + expect(instance).toEqual({name: 'angular'}); + expect($injector('instance')).toBe(instance); + expect($injector('instance')).toBe(instance); + }); + + + it('should call functions and infer arguments', function() { + expect($injector(function(instance) { return instance; })).toBe(instance); + expect($injector(function(instance) { return instance; })).toBe(instance); + }); + }); + + + describe('method invoking', function() { + var $injector; + + beforeEach(function() { + $injector = createInjector2([ function($provide) { + $provide.value('book', 'moby'); + $provide.value('author', 'melville'); + }]); + }); + + + it('should invoke method', function() { + expect($injector(function(book, author) { return author + ':' + book;})).toEqual('melville:moby'); + expect($injector.invoke($injector, function(book, author) { + expect(this).toEqual($injector); + return author + ':' + book;})).toEqual('melville:moby'); + }); + + + it('should invoke method which is annotated', function() { + expect($injector(extend(function(b, a) { return a + ':' + b}, {$inject:['book', 'author']}))). + toEqual('melville:moby'); + expect($injector.invoke($injector, extend(function(b, a) { + expect(this).toEqual($injector); + return a + ':' + b; + }, {$inject:['book', 'author']}))).toEqual('melville:moby'); + }); + + + it('should invoke method which is an array of annotation', function() { + expect($injector(function(book, author) { return author + ':' + book;})).toEqual('melville:moby'); + expect($injector.invoke($injector, function(book, author) { + expect(this).toEqual($injector); + return author + ':' + book; + })).toEqual('melville:moby'); + }); + + + it('should throw usefull error on wrong argument type]', function() { + expect(function(){ + $injector.invoke(null, {}); + }).toThrow("Argument 'fn' is not a function, got Object"); + }); + }); + + describe('service instantiation', function() { + var $injector; + + beforeEach(function() { + $injector = createInjector2([ function($provide) { + $provide.value('book', 'moby'); + $provide.value('author', 'melville'); + }]); + }); + + + function Type(book, author) { + this.book = book; + this.author = author; + } + Type.prototype.title = function() { + return this.author + ': ' + this.book; + }; + + + it('should instantiate object and preserve constructor property and be instanceof', function() { + var t = $injector.instantiate(Type); + expect(t.book).toEqual('moby'); + expect(t.author).toEqual('melville'); + expect(t.title()).toEqual('melville: moby'); + expect(t instanceof Type).toBe(true); + }); + + + it('should allow constructor to return different object', function() { + var t = $injector.instantiate(function() { return 'ABC'; }); + expect(t).toBe('ABC'); + }); + + + it('should handle constructor exception', function() { + expect(function() { + $injector.instantiate(function() { throw 'MyError'; }); + }).toThrow('MyError'); + }); + }); + + describe('injector chaining', function() { + + }); +}); diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index b5949623..3e68fbed 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -111,7 +111,7 @@ function inject(){ fn.call(spec, spec.$service); } else { if (!spec.$injector) { - spec.$injector = angular.injector(spec.$service); + spec.$injector = angular.injector(spec.$service || angular.service); } spec.$injector.invoke(spec, fn); } diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index d2204540..b7ef16a4 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -487,7 +487,7 @@ describe("widget", function() { })); it('should be possible to nest ng:view in ng:include', inject(function() { - var injector = createInjector(); + var injector = createInjector(angularService); var myApp = injector('$rootScope'); var $browser = myApp.$service('$browser'); $browser.xhr.expectGET('includePartial.html').respond('view: <ng:view></ng:view>'); |
