Skip to content

Commit b241160

Browse files
committed
feat($injector): add support for non-string IDs
Previously, only strings could be used as identifiers for Angular services. This commit adds support for using any value as identifier for an Angular service (e.g. used with `.provider()`, `.factory()`, `.service()`, `.value()`, `.constant()`, `.decorator()`). Identifiers for directives and filters are still restricted to string values, since they need to be references in HTML (text). -- As usual, if a service has a string ID, its provider's ID is constructed by appending `Provider` to the service ID. For services with non-string IDs, their providers have the exact same ID (it is the context that determines if a service or a provider should be injected). E.g.: ```js var bar = {}; angular. module('myModule', []). provider('foo' /* string ID */, {$get: function () { return 'FOO'; }}). provider( bar /* non-string ID */, {$get: function () { return 'BAR'; }}). config(['fooProvider', function (fooProvider) { // `foo` provider injected (because we are in config block) }]). run(['foo', function (foo) { // `foo` service injected (because we are in run block) }]). config([bar /* same ID */, function (barProvider) { // `bar` provider injected (because we are in config block) }]). run([bar /* same ID */, function (bar) { // `bar` service injected (because we are in run block) }]); ``` -- This change is backwards compatible (afaict). Fixes angular#10347
1 parent 1660ddd commit b241160

File tree

5 files changed

+259
-75
lines changed

5 files changed

+259
-75
lines changed

src/apis.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,19 @@ function hashKey(obj, nextUidFn) {
3636
/**
3737
* HashMap which can use objects as keys
3838
*/
39-
function HashMap(array, isolatedUid) {
39+
function HashMap(seedData, isolatedUid) {
4040
if (isolatedUid) {
4141
var uid = 0;
4242
this.nextUid = function() {
4343
return ++uid;
4444
};
4545
}
46-
forEach(array, this.put, this);
46+
47+
if (seedData) {
48+
var putFn = isArray(seedData) ?
49+
this.put : reverseParams(this.put.bind(this));
50+
forEach(seedData, putFn, this);
51+
}
4752
}
4853
HashMap.prototype = {
4954
/**
@@ -63,6 +68,14 @@ HashMap.prototype = {
6368
return this[hashKey(key, this.nextUid)];
6469
},
6570

71+
/**
72+
* @param key
73+
* @returns {boolean} whether a value is stored under the specified key
74+
*/
75+
has: function(key) {
76+
return this.hasOwnProperty(hashKey(key, this.nextUid));
77+
},
78+
6679
/**
6780
* Remove the key/value pair
6881
* @param key

src/auto/injector.js

+87-60
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ function stringifyFn(fn) {
7474
// Support: Chrome 50-51 only
7575
// Creating a new string by adding `' '` at the end, to hack around some bug in Chrome v50/51
7676
// (See https://github.com/angular/angular.js/issues/14487.)
77-
// TODO (gkalpak): Remove workaround when Chrome v52 is released
77+
// TODO(gkalpak): Remove workaround when Chrome v52 is released
7878
return Function.prototype.toString.call(fn) + ' ';
7979
}
8080

@@ -129,6 +129,10 @@ function annotate(fn, strictDi, name) {
129129
return $inject;
130130
}
131131

132+
function stringifyServiceId(id, suffix) {
133+
return (isUndefined(id) || isString(id)) ? id : id + suffix;
134+
}
135+
132136
///////////////////////////////////////
133137

134138
/**
@@ -650,34 +654,34 @@ function createInjector(modulesToLoad, strictDi) {
650654
var INSTANTIATING = {},
651655
providerSuffix = 'Provider',
652656
path = [],
653-
loadedModules = new HashMap([], true),
654-
providerCache = {
657+
loadedModules = new HashMap(null, true),
658+
providerCache = new HashMap({
655659
$provide: {
656-
provider: supportObject(provider),
657-
factory: supportObject(factory),
658-
service: supportObject(service),
659-
value: supportObject(value),
660-
constant: supportObject(constant),
661-
decorator: decorator
662-
}
663-
},
664-
providerInjector = (providerCache.$injector =
665-
createInternalInjector(providerCache, function(serviceName, caller) {
666-
if (angular.isString(caller)) {
667-
path.push(caller);
668-
}
660+
provider: supportObject(provider),
661+
factory: supportObject(factory),
662+
service: supportObject(service),
663+
value: supportObject(value),
664+
constant: supportObject(constant),
665+
decorator: decorator
666+
}
667+
}, true),
668+
instanceCache = new HashMap(null, true),
669+
providerInjector =
670+
createInternalInjector(providerCache, function(serviceName) {
669671
throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
670-
})),
671-
instanceCache = {},
672+
}, ' (provider)'),
672673
protoInstanceInjector =
673-
createInternalInjector(instanceCache, function(serviceName, caller) {
674-
var provider = providerInjector.get(serviceName + providerSuffix, caller);
675-
return instanceInjector.invoke(
676-
provider.$get, provider, undefined, serviceName);
674+
createInternalInjector(instanceCache, function(serviceName) {
675+
var providerId = !isString(serviceName) ? serviceName : serviceName + providerSuffix;
676+
var provider = providerInjector.get(providerId);
677+
678+
return instanceInjector.invoke(provider.$get, provider, undefined, serviceName);
677679
}),
678680
instanceInjector = protoInstanceInjector;
679681

680-
providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) };
682+
providerCache.put('$injector', providerInjector);
683+
providerCache.put('$injector' + providerSuffix, {$get: valueFn(protoInstanceInjector)});
684+
681685
var runBlocks = loadModules(modulesToLoad);
682686
instanceInjector = protoInstanceInjector.get('$injector');
683687
instanceInjector.strictDi = strictDi;
@@ -691,7 +695,7 @@ function createInjector(modulesToLoad, strictDi) {
691695

692696
function supportObject(delegate) {
693697
return function(key, value) {
694-
if (isObject(key)) {
698+
if ((arguments.length === 1) && isObject(key)) {
695699
forEach(key, reverseParams(delegate));
696700
} else {
697701
return delegate(key, value);
@@ -707,7 +711,11 @@ function createInjector(modulesToLoad, strictDi) {
707711
if (!provider_.$get) {
708712
throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
709713
}
710-
return providerCache[name + providerSuffix] = provider_;
714+
715+
var providerId = !isString(name) ? name : name + providerSuffix;
716+
providerCache.put(providerId, provider_);
717+
718+
return provider_;
711719
}
712720

713721
function enforceReturnValue(name, factory) {
@@ -727,21 +735,24 @@ function createInjector(modulesToLoad, strictDi) {
727735
}
728736

729737
function service(name, constructor) {
730-
return factory(name, ['$injector', function($injector) {
731-
return $injector.instantiate(constructor);
732-
}]);
738+
return factory(name, function() {
739+
return instanceInjector.instantiate(constructor);
740+
});
733741
}
734742

735-
function value(name, val) { return factory(name, valueFn(val), false); }
743+
function value(name, val) {
744+
return factory(name, valueFn(val), false);
745+
}
736746

737747
function constant(name, value) {
738748
assertNotHasOwnProperty(name, 'constant');
739-
providerCache[name] = value;
740-
instanceCache[name] = value;
749+
providerCache.put(name, value);
750+
instanceCache.put(name, value);
741751
}
742752

743753
function decorator(serviceName, decorFn) {
744-
var origProvider = providerInjector.get(serviceName + providerSuffix),
754+
var providerId = !isString(serviceName) ? serviceName : serviceName + providerSuffix;
755+
var origProvider = providerInjector.get(providerId),
745756
orig$get = origProvider.$get;
746757

747758
origProvider.$get = function() {
@@ -776,9 +787,7 @@ function createInjector(modulesToLoad, strictDi) {
776787
runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
777788
runInvokeQueue(moduleFn._invokeQueue);
778789
runInvokeQueue(moduleFn._configBlocks);
779-
} else if (isFunction(module)) {
780-
runBlocks.push(providerInjector.invoke(module));
781-
} else if (isArray(module)) {
790+
} else if (isFunction(module) || isArray(module)) {
782791
runBlocks.push(providerInjector.invoke(module));
783792
} else {
784793
assertArgFn(module, 'module');
@@ -806,28 +815,44 @@ function createInjector(modulesToLoad, strictDi) {
806815
// internal Injector
807816
////////////////////////////////////
808817

809-
function createInternalInjector(cache, factory) {
818+
function createInternalInjector(cache, factory, suffix) {
819+
suffix = suffix || '';
810820

811821
function getService(serviceName, caller) {
812-
if (cache.hasOwnProperty(serviceName)) {
813-
if (cache[serviceName] === INSTANTIATING) {
814-
throw $injectorMinErr('cdep', 'Circular dependency found: {0}',
815-
serviceName + ' <- ' + path.join(' <- '));
816-
}
817-
return cache[serviceName];
818-
} else {
819-
try {
820-
path.unshift(serviceName);
821-
cache[serviceName] = INSTANTIATING;
822-
return cache[serviceName] = factory(serviceName, caller);
823-
} catch (err) {
824-
if (cache[serviceName] === INSTANTIATING) {
825-
delete cache[serviceName];
822+
var instance;
823+
var callerStr = stringifyServiceId(caller, suffix);
824+
var hasCaller = callerStr && (path[0] !== callerStr);
825+
826+
if (hasCaller) path.unshift(callerStr);
827+
path.unshift(stringifyServiceId(serviceName, suffix));
828+
829+
try {
830+
if (cache.has(serviceName)) {
831+
instance = cache.get(serviceName);
832+
833+
if (instance === INSTANTIATING) {
834+
throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- '));
835+
}
836+
837+
return instance;
838+
} else {
839+
try {
840+
cache.put(serviceName, INSTANTIATING);
841+
842+
instance = factory(serviceName);
843+
cache.put(serviceName, instance);
844+
845+
return instance;
846+
} catch (err) {
847+
if (cache.get(serviceName) === INSTANTIATING) {
848+
cache.remove(serviceName);
849+
}
850+
throw err;
826851
}
827-
throw err;
828-
} finally {
829-
path.shift();
830852
}
853+
} finally {
854+
path.shift();
855+
if (hasCaller) path.shift();
831856
}
832857
}
833858

@@ -838,12 +863,13 @@ function createInjector(modulesToLoad, strictDi) {
838863

839864
for (var i = 0, length = $inject.length; i < length; i++) {
840865
var key = $inject[i];
841-
if (typeof key !== 'string') {
842-
throw $injectorMinErr('itkn',
843-
'Incorrect injection token! Expected service name as string, got {0}', key);
844-
}
845-
args.push(locals && locals.hasOwnProperty(key) ? locals[key] :
846-
getService(key, serviceName));
866+
// TODO(gkalpak): Remove this and the corresponding error (?)
867+
// if (typeof key !== 'string') {
868+
// throw $injectorMinErr('itkn',
869+
// 'Incorrect injection token! Expected service name as string, got {0}', key);
870+
// }
871+
var localsHasKey = locals && isString(key) && locals.hasOwnProperty(key);
872+
args.push(localsHasKey ? locals[key] : getService(key, serviceName));
847873
}
848874
return args;
849875
}
@@ -901,7 +927,8 @@ function createInjector(modulesToLoad, strictDi) {
901927
get: getService,
902928
annotate: createInjector.$$annotate,
903929
has: function(name) {
904-
return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
930+
return cache.has(name) ||
931+
providerCache.has(!isString(name) ? name : name + providerSuffix);
905932
}
906933
};
907934
}

src/ng/directive/select.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -564,8 +564,11 @@ var selectDirective = function() {
564564
// Write value now needs to set the selected property of each matching option
565565
selectCtrl.writeValue = function writeMultipleValue(value) {
566566
var items = new HashMap(value);
567+
var selectValueMap = selectCtrl.selectValueMap;
568+
567569
forEach(element.find('option'), function(option) {
568-
option.selected = isDefined(items.get(option.value)) || isDefined(items.get(selectCtrl.selectValueMap[option.value]));
570+
var value = option.value;
571+
option.selected = items.has(value) || items.has(selectValueMap[value]);
569572
});
570573
};
571574

test/ApiSpecs.js

+12
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ describe('api', function() {
88
var key = {};
99
var value1 = {};
1010
var value2 = {};
11+
1112
map.put(key, value1);
1213
map.put(key, value2);
14+
15+
expect(map.has(key)).toBe(true);
16+
expect(map.has({})).toBe(false);
1317
expect(map.get(key)).toBe(value2);
1418
expect(map.get({})).toBeUndefined();
1519
expect(map.remove(key)).toBe(value2);
20+
expect(map.has(key)).toBe(false);
1621
expect(map.get(key)).toBeUndefined();
1722
});
1823

@@ -23,6 +28,13 @@ describe('api', function() {
2328
expect(map.get('c')).toBeUndefined();
2429
});
2530

31+
it('should init from an object', function() {
32+
var map = new HashMap({a: 'foo', b: 'bar'});
33+
expect(map.get('a')).toBe('foo');
34+
expect(map.get('b')).toBe('bar');
35+
expect(map.get('c')).toBeUndefined();
36+
});
37+
2638
it('should maintain hashKey for object keys', function() {
2739
var map = new HashMap();
2840
var key = {};

0 commit comments

Comments
 (0)