diff --git a/src/apis.js b/src/apis.js index d9bd34f90de5..43273c204f99 100644 --- a/src/apis.js +++ b/src/apis.js @@ -36,14 +36,19 @@ function hashKey(obj, nextUidFn) { /** * HashMap which can use objects as keys */ -function HashMap(array, isolatedUid) { +function HashMap(seedData, isolatedUid) { if (isolatedUid) { var uid = 0; this.nextUid = function() { return ++uid; }; } - forEach(array, this.put, this); + + if (seedData) { + var putFn = isArray(seedData) ? + this.put : reverseParams(this.put.bind(this)); + forEach(seedData, putFn, this); + } } HashMap.prototype = { /** @@ -63,6 +68,14 @@ HashMap.prototype = { return this[hashKey(key, this.nextUid)]; }, + /** + * @param key + * @returns {boolean} whether a value is stored under the specified key + */ + has: function(key) { + return this.hasOwnProperty(hashKey(key, this.nextUid)); + }, + /** * Remove the key/value pair * @param key diff --git a/src/auto/injector.js b/src/auto/injector.js index 352561afa4d9..3c9a862dc7e6 100644 --- a/src/auto/injector.js +++ b/src/auto/injector.js @@ -74,7 +74,7 @@ function stringifyFn(fn) { // Support: Chrome 50-51 only // Creating a new string by adding `' '` at the end, to hack around some bug in Chrome v50/51 // (See https://github.com/angular/angular.js/issues/14487.) - // TODO (gkalpak): Remove workaround when Chrome v52 is released + // TODO(gkalpak): Remove workaround when Chrome v52 is released return Function.prototype.toString.call(fn) + ' '; } @@ -129,6 +129,10 @@ function annotate(fn, strictDi, name) { return $inject; } +function stringifyServiceId(id, suffix) { + return (isUndefined(id) || isString(id)) ? id : id + suffix; +} + /////////////////////////////////////// /** @@ -649,34 +653,34 @@ function createInjector(modulesToLoad, strictDi) { var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], - loadedModules = new HashMap([], true), - providerCache = { + loadedModules = new HashMap(null, true), + providerCache = new HashMap({ $provide: { - provider: supportObject(provider), - factory: supportObject(factory), - service: supportObject(service), - value: supportObject(value), - constant: supportObject(constant), - decorator: decorator - } - }, - providerInjector = (providerCache.$injector = - createInternalInjector(providerCache, function(serviceName, caller) { - if (angular.isString(caller)) { - path.push(caller); - } + provider: supportObject(provider), + factory: supportObject(factory), + service: supportObject(service), + value: supportObject(value), + constant: supportObject(constant), + decorator: decorator + } + }), + instanceCache = new HashMap(), + providerInjector = + createInternalInjector(providerCache, function(serviceName) { throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); - })), - instanceCache = {}, + }, ' (provider)'), protoInstanceInjector = - createInternalInjector(instanceCache, function(serviceName, caller) { - var provider = providerInjector.get(serviceName + providerSuffix, caller); - return instanceInjector.invoke( - provider.$get, provider, undefined, serviceName); + createInternalInjector(instanceCache, function(serviceName) { + var providerId = !isString(serviceName) ? serviceName : serviceName + providerSuffix; + var provider = providerInjector.get(providerId); + + return instanceInjector.invoke(provider.$get, provider, undefined, serviceName); }), instanceInjector = protoInstanceInjector; - providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) }; + providerCache.put('$injector', providerInjector); + providerCache.put('$injector' + providerSuffix, {$get: valueFn(protoInstanceInjector)}); + var runBlocks = loadModules(modulesToLoad); instanceInjector = protoInstanceInjector.get('$injector'); instanceInjector.strictDi = strictDi; @@ -690,7 +694,7 @@ function createInjector(modulesToLoad, strictDi) { function supportObject(delegate) { return function(key, value) { - if (isObject(key)) { + if ((arguments.length === 1) && isObject(key)) { forEach(key, reverseParams(delegate)); } else { return delegate(key, value); @@ -706,7 +710,11 @@ function createInjector(modulesToLoad, strictDi) { if (!provider_.$get) { throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); } - return (providerCache[name + providerSuffix] = provider_); + + var providerId = !isString(name) ? name : name + providerSuffix; + providerCache.put(providerId, provider_); + + return provider_; } function enforceReturnValue(name, factory) { @@ -726,21 +734,24 @@ function createInjector(modulesToLoad, strictDi) { } function service(name, constructor) { - return factory(name, ['$injector', function($injector) { - return $injector.instantiate(constructor); - }]); + return factory(name, function() { + return instanceInjector.instantiate(constructor); + }); } - function value(name, val) { return factory(name, valueFn(val), false); } + function value(name, val) { + return factory(name, valueFn(val), false); + } function constant(name, value) { assertNotHasOwnProperty(name, 'constant'); - providerCache[name] = value; - instanceCache[name] = value; + providerCache.put(name, value); + instanceCache.put(name, value); } function decorator(serviceName, decorFn) { - var origProvider = providerInjector.get(serviceName + providerSuffix), + var providerId = !isString(serviceName) ? serviceName : serviceName + providerSuffix; + var origProvider = providerInjector.get(providerId), orig$get = origProvider.$get; origProvider.$get = function() { @@ -775,9 +786,7 @@ function createInjector(modulesToLoad, strictDi) { runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); runInvokeQueue(moduleFn._invokeQueue); runInvokeQueue(moduleFn._configBlocks); - } else if (isFunction(module)) { - runBlocks.push(providerInjector.invoke(module)); - } else if (isArray(module)) { + } else if (isFunction(module) || isArray(module)) { runBlocks.push(providerInjector.invoke(module)); } else { assertArgFn(module, 'module'); @@ -805,29 +814,44 @@ function createInjector(modulesToLoad, strictDi) { // internal Injector //////////////////////////////////// - function createInternalInjector(cache, factory) { + function createInternalInjector(cache, factory, suffix) { + suffix = suffix || ''; function getService(serviceName, caller) { - if (cache.hasOwnProperty(serviceName)) { - if (cache[serviceName] === INSTANTIATING) { - throw $injectorMinErr('cdep', 'Circular dependency found: {0}', - serviceName + ' <- ' + path.join(' <- ')); - } - return cache[serviceName]; - } else { - try { - path.unshift(serviceName); - cache[serviceName] = INSTANTIATING; - cache[serviceName] = factory(serviceName, caller); - return cache[serviceName]; - } catch (err) { - if (cache[serviceName] === INSTANTIATING) { - delete cache[serviceName]; + var callerStr = stringifyServiceId(caller, suffix); + var hasCaller = callerStr && (path[0] !== callerStr); + var instance; + + if (hasCaller) path.unshift(callerStr); + path.unshift(stringifyServiceId(serviceName, suffix)); + + try { + if (cache.has(serviceName)) { + instance = cache.get(serviceName); + + if (instance === INSTANTIATING) { + throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); + } + + return instance; + } else { + try { + cache.put(serviceName, INSTANTIATING); + + instance = factory(serviceName); + cache.put(serviceName, instance); + + return instance; + } catch (err) { + if (cache.get(serviceName) === INSTANTIATING) { + cache.remove(serviceName); + } + throw err; } - throw err; - } finally { - path.shift(); } + } finally { + path.shift(); + if (hasCaller) path.shift(); } } @@ -838,12 +862,13 @@ function createInjector(modulesToLoad, strictDi) { for (var i = 0, length = $inject.length; i < length; i++) { var key = $inject[i]; - if (typeof key !== 'string') { - throw $injectorMinErr('itkn', - 'Incorrect injection token! Expected service name as string, got {0}', key); - } - args.push(locals && locals.hasOwnProperty(key) ? locals[key] : - getService(key, serviceName)); + // TODO(gkalpak): Remove this and the corresponding error (?) + // if (typeof key !== 'string') { + // throw $injectorMinErr('itkn', + // 'Incorrect injection token! Expected service name as string, got {0}', key); + // } + var localsHasKey = locals && isString(key) && locals.hasOwnProperty(key); + args.push(localsHasKey ? locals[key] : getService(key, serviceName)); } return args; } @@ -901,7 +926,8 @@ function createInjector(modulesToLoad, strictDi) { get: getService, annotate: createInjector.$$annotate, has: function(name) { - return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); + return cache.has(name) || + providerCache.has(!isString(name) ? name : name + providerSuffix); } }; } diff --git a/src/ng/directive/select.js b/src/ng/directive/select.js index dc0621de60b0..6a4150806f65 100644 --- a/src/ng/directive/select.js +++ b/src/ng/directive/select.js @@ -563,8 +563,11 @@ var selectDirective = function() { // Write value now needs to set the selected property of each matching option selectCtrl.writeValue = function writeMultipleValue(value) { var items = new HashMap(value); + var selectValueMap = selectCtrl.selectValueMap; + forEach(element.find('option'), function(option) { - option.selected = isDefined(items.get(option.value)) || isDefined(items.get(selectCtrl.selectValueMap[option.value])); + var value = option.value; + option.selected = items.has(value) || items.has(selectValueMap[value]); }); }; diff --git a/test/ApiSpecs.js b/test/ApiSpecs.js index 349dfe17e662..0fb237acc9e1 100644 --- a/test/ApiSpecs.js +++ b/test/ApiSpecs.js @@ -8,11 +8,16 @@ describe('api', function() { var key = {}; var value1 = {}; var value2 = {}; + map.put(key, value1); map.put(key, value2); + + expect(map.has(key)).toBe(true); + expect(map.has({})).toBe(false); expect(map.get(key)).toBe(value2); expect(map.get({})).toBeUndefined(); expect(map.remove(key)).toBe(value2); + expect(map.has(key)).toBe(false); expect(map.get(key)).toBeUndefined(); }); @@ -23,6 +28,13 @@ describe('api', function() { expect(map.get('c')).toBeUndefined(); }); + it('should init from an object', function() { + var map = new HashMap({a: 'foo', b: 'bar'}); + expect(map.get('a')).toBe('foo'); + expect(map.get('b')).toBe('bar'); + expect(map.get('c')).toBeUndefined(); + }); + it('should maintain hashKey for object keys', function() { var map = new HashMap(); var key = {}; diff --git a/test/auto/injectorSpec.js b/test/auto/injectorSpec.js index c0760aecb687..978f0b666df9 100644 --- a/test/auto/injectorSpec.js +++ b/test/auto/injectorSpec.js @@ -46,13 +46,13 @@ describe('injector', function() { it('should resolve dependency graph and instantiate all services just once', function() { var log = []; -// s1 -// / | \ -// / s2 \ -// / / | \ \ -// /s3 < s4 > s5 -// // -// s6 + // ____ s1 _ + // / | \ + // / __ s2 __ \ + // / / | \ \ + // | s3 <- s4 --> s5 + // | / + // s6 providers('s1', function() { log.push('s1'); return {}; }, {$inject: ['s2', 's5', 's6']}); @@ -287,7 +287,7 @@ describe('injector', function() { }); // Support: Chrome 50-51 only - // TODO (gkalpak): Remove when Chrome v52 is released. + // TODO(gkalpak): Remove when Chrome v52 is released. // it('should be able to inject fat-arrow function', function() { // inject(($injector) => { // expect($injector).toBeDefined(); @@ -327,7 +327,7 @@ describe('injector', function() { } // Support: Chrome 50-51 only - // TODO (gkalpak): Remove when Chrome v52 is released. + // TODO(gkalpak): Remove when Chrome v52 is released. // it('should be able to invoke classes', function() { // class Test { // constructor($injector) { @@ -884,7 +884,6 @@ describe('injector', function() { var $injector = createInjectorWithValue('instance', instance); expect($injector.invoke(function(instance) { return instance; })).toBe(instance); }); - }); @@ -1039,7 +1038,7 @@ describe('injector', function() { describe('protection modes', function() { it('should prevent provider lookup in app', function() { - var $injector = createInjector([function($provide) { + var $injector = createInjector([function($provide) { $provide.value('name', 'angular'); }]); expect(function() { @@ -1049,7 +1048,7 @@ describe('injector', function() { it('should prevent provider configuration in app', function() { - var $injector = createInjector([]); + var $injector = createInjector([]); expect(function() { $injector.get('$provide').value('a', 'b'); }).toThrowMinErr("$injector", "unpr", "Unknown provider: $provideProvider <- $provide"); @@ -1162,3 +1161,195 @@ describe('strict-di injector', function() { expect($injector.strictDi).toBe(true); })); }); + +describe('injector with non-string IDs', function() { + it('should support non-string service identifiers', function() { + var ids = [ + undefined, + null, + true, + 1, + {}, + [], + noop, + /./ + ]; + + module(function($provide) { + ids.forEach(function(id, idx) { $provide.value(id, idx); }); + $provide.factory('allDeps', ids.concat(function() { return sliceArgs(arguments); })); + }); + + inject(function($injector) { + var allDeps = $injector.get('allDeps'); + + expect(allDeps.length).toBe(ids.length); + expect(allDeps.every(function(dep, idx) { return dep === idx; })).toBe(true); + }); + }); + + + it('should support configuring services with non-string identifiers', function() { + /* eslint-disable no-new-wrappers */ + var id1 = new String('same string, no problem'); + var id2 = new String('same string, no problem'); + /* eslint-enable */ + + angular. + module('test', []). + factory(id2, [id1, identity]). + provider(id1, function Id1Provider() { + var value = 'foo'; + this.setValue = function setValue(newValue) { value = newValue; }; + this.$get = function $get() { return value; }; + }). + config([id1, function config(id1Provider) { + id1Provider.setValue('bar'); + }]); + + module('test'); + inject([id2, function(dep2) { + expect(dep2).toBe('bar'); + }]); + }); + + + it('should support decorating services with non-string identifiers', function() { + /* eslint-disable no-new-wrappers */ + var id1 = new String('same string, no problem'); + var id2 = new String('same string, no problem'); + /* eslint-enable */ + + angular. + module('test', []). + factory(id2, [id1, identity]). + value(id1, 'foo'). + decorator(id1, function decorator($delegate) { + expect($delegate).toBe('foo'); + return 'bar'; + }); + + module('test'); + inject([id2, function(dep2) { + expect(dep2).toBe('bar'); + }]); + }); + + + it('should still allow passing multiple providers as object', function() { + var obj = { + foo: 'foo', + bar: 'bar' + }; + + module(function($provide) { + $provide.value(obj); + $provide.value(obj, 'foo&bar'); + }); + + inject(function($injector) { + expect($injector.get('foo')).toBe('foo'); + expect($injector.get('bar')).toBe('bar'); + expect($injector.get(obj)).toBe('foo&bar'); + }); + }); + + + describe('should stringify non-string identifiers for error messages', function() { + var foo, bar, baz; + + beforeEach(function() { + foo = {toString: valueFn('fooThingy')}; + bar = {toString: valueFn('barThingy')}; + baz = {toString: valueFn('bazThingy')}; + }); + + + it('(Unknown provider)', function() { + var executed = false; + + module(function($provide) { + expect(function() { + $provide.provider('foo', ['barProvider', noop]); + }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: barProvider\n'); + + expect(function() { + $provide.provider('foo', [{}, noop]); + }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: [object Object] (provider)\n'); + + expect(function() { + $provide.provider('foo', [bar, noop]); + }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: barThingy (provider)\n'); + + executed = true; + }); + + inject(); + + expect(executed).toBe(true); + }); + + + it('(Unknown service)', function() { + var executed = false; + + module(function($provide) { + $provide.provider('bar', valueFn({$get: [baz, noop]})); + $provide.provider(bar, valueFn({$get: ['baz', noop]})); + + $provide.provider('foo', valueFn({$get: [bar, noop]})); + $provide.provider(foo, valueFn({$get: ['bar', noop]})); + }); + + inject(function($injector) { + var specs = [ + ['bar', 'bazThingy (provider) <- bazThingy <- bar'], + [bar, 'bazProvider <- baz <- barThingy'], + ['foo', 'bazProvider <- baz <- barThingy <- foo'], + [foo, 'bazThingy (provider) <- bazThingy <- bar <- fooThingy'] + ]; + + forEach(specs, function(spec) { + var serviceId = spec[0]; + var errorPath = spec[1]; + + expect(function() { + $injector.get(serviceId); + }).toThrowMinErr('$injector', 'unpr', 'Unknown provider: ' + errorPath + '\n'); + }); + + executed = true; + }); + + expect(executed).toBe(true); + }); + + + it('(Circular dependency)', function() { + var executed = false; + + module(function($provide) { + $provide.provider('baz', valueFn({$get: [foo, noop]})); + $provide.provider(baz, valueFn({$get: ['foo', noop]})); + + $provide.provider('bar', valueFn({$get: [baz, noop]})); + $provide.provider(bar, valueFn({$get: ['baz', noop]})); + + $provide.provider('foo', valueFn({$get: [bar, noop]})); + $provide.provider(foo, valueFn({$get: ['bar', noop]})); + }); + + inject(function($injector) { + var errorPath = 'fooThingy <- baz <- barThingy <- foo <- bazThingy <- bar <- fooThingy'; + + expect(function() { + $injector.get(foo); + }).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: ' + errorPath + '\n'); + + executed = true; + }); + + expect(executed).toBe(true); + }); + }); +});