Skip to content

Commit e8a122b

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 5fc9933 commit e8a122b

File tree

5 files changed

+321
-76
lines changed

5 files changed

+321
-76
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-61
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
/**
@@ -649,34 +653,34 @@ function createInjector(modulesToLoad, strictDi) {
649653
var INSTANTIATING = {},
650654
providerSuffix = 'Provider',
651655
path = [],
652-
loadedModules = new HashMap([], true),
653-
providerCache = {
656+
loadedModules = new HashMap(null, true),
657+
providerCache = new HashMap({
654658
$provide: {
655-
provider: supportObject(provider),
656-
factory: supportObject(factory),
657-
service: supportObject(service),
658-
value: supportObject(value),
659-
constant: supportObject(constant),
660-
decorator: decorator
661-
}
662-
},
663-
providerInjector = (providerCache.$injector =
664-
createInternalInjector(providerCache, function(serviceName, caller) {
665-
if (angular.isString(caller)) {
666-
path.push(caller);
667-
}
659+
provider: supportObject(provider),
660+
factory: supportObject(factory),
661+
service: supportObject(service),
662+
value: supportObject(value),
663+
constant: supportObject(constant),
664+
decorator: decorator
665+
}
666+
}),
667+
instanceCache = new HashMap(),
668+
providerInjector =
669+
createInternalInjector(providerCache, function(serviceName) {
668670
throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
669-
})),
670-
instanceCache = {},
671+
}, ' (provider)'),
671672
protoInstanceInjector =
672-
createInternalInjector(instanceCache, function(serviceName, caller) {
673-
var provider = providerInjector.get(serviceName + providerSuffix, caller);
674-
return instanceInjector.invoke(
675-
provider.$get, provider, undefined, serviceName);
673+
createInternalInjector(instanceCache, function(serviceName) {
674+
var providerId = !isString(serviceName) ? serviceName : serviceName + providerSuffix;
675+
var provider = providerInjector.get(providerId);
676+
677+
return instanceInjector.invoke(provider.$get, provider, undefined, serviceName);
676678
}),
677679
instanceInjector = protoInstanceInjector;
678680

679-
providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) };
681+
providerCache.put('$injector', providerInjector);
682+
providerCache.put('$injector' + providerSuffix, {$get: valueFn(protoInstanceInjector)});
683+
680684
var runBlocks = loadModules(modulesToLoad);
681685
instanceInjector = protoInstanceInjector.get('$injector');
682686
instanceInjector.strictDi = strictDi;
@@ -690,7 +694,7 @@ function createInjector(modulesToLoad, strictDi) {
690694

691695
function supportObject(delegate) {
692696
return function(key, value) {
693-
if (isObject(key)) {
697+
if ((arguments.length === 1) && isObject(key)) {
694698
forEach(key, reverseParams(delegate));
695699
} else {
696700
return delegate(key, value);
@@ -706,7 +710,11 @@ function createInjector(modulesToLoad, strictDi) {
706710
if (!provider_.$get) {
707711
throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
708712
}
709-
return (providerCache[name + providerSuffix] = provider_);
713+
714+
var providerId = !isString(name) ? name : name + providerSuffix;
715+
providerCache.put(providerId, provider_);
716+
717+
return provider_;
710718
}
711719

712720
function enforceReturnValue(name, factory) {
@@ -726,21 +734,24 @@ function createInjector(modulesToLoad, strictDi) {
726734
}
727735

728736
function service(name, constructor) {
729-
return factory(name, ['$injector', function($injector) {
730-
return $injector.instantiate(constructor);
731-
}]);
737+
return factory(name, function() {
738+
return instanceInjector.instantiate(constructor);
739+
});
732740
}
733741

734-
function value(name, val) { return factory(name, valueFn(val), false); }
742+
function value(name, val) {
743+
return factory(name, valueFn(val), false);
744+
}
735745

736746
function constant(name, value) {
737747
assertNotHasOwnProperty(name, 'constant');
738-
providerCache[name] = value;
739-
instanceCache[name] = value;
748+
providerCache.put(name, value);
749+
instanceCache.put(name, value);
740750
}
741751

742752
function decorator(serviceName, decorFn) {
743-
var origProvider = providerInjector.get(serviceName + providerSuffix),
753+
var providerId = !isString(serviceName) ? serviceName : serviceName + providerSuffix;
754+
var origProvider = providerInjector.get(providerId),
744755
orig$get = origProvider.$get;
745756

746757
origProvider.$get = function() {
@@ -775,9 +786,7 @@ function createInjector(modulesToLoad, strictDi) {
775786
runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
776787
runInvokeQueue(moduleFn._invokeQueue);
777788
runInvokeQueue(moduleFn._configBlocks);
778-
} else if (isFunction(module)) {
779-
runBlocks.push(providerInjector.invoke(module));
780-
} else if (isArray(module)) {
789+
} else if (isFunction(module) || isArray(module)) {
781790
runBlocks.push(providerInjector.invoke(module));
782791
} else {
783792
assertArgFn(module, 'module');
@@ -805,29 +814,44 @@ function createInjector(modulesToLoad, strictDi) {
805814
// internal Injector
806815
////////////////////////////////////
807816

808-
function createInternalInjector(cache, factory) {
817+
function createInternalInjector(cache, factory, suffix) {
818+
suffix = suffix || '';
809819

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

@@ -838,12 +862,13 @@ function createInjector(modulesToLoad, strictDi) {
838862

839863
for (var i = 0, length = $inject.length; i < length; i++) {
840864
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));
865+
// TODO(gkalpak): Remove this and the corresponding error (?)
866+
// if (typeof key !== 'string') {
867+
// throw $injectorMinErr('itkn',
868+
// 'Incorrect injection token! Expected service name as string, got {0}', key);
869+
// }
870+
var localsHasKey = locals && isString(key) && locals.hasOwnProperty(key);
871+
args.push(localsHasKey ? locals[key] : getService(key, serviceName));
847872
}
848873
return args;
849874
}
@@ -901,7 +926,8 @@ function createInjector(modulesToLoad, strictDi) {
901926
get: getService,
902927
annotate: createInjector.$$annotate,
903928
has: function(name) {
904-
return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
929+
return cache.has(name) ||
930+
providerCache.has(!isString(name) ? name : name + providerSuffix);
905931
}
906932
};
907933
}

src/ng/directive/select.js

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

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)