Skip to content

Commit

Permalink
feat($injector): add support for non-string IDs
Browse files Browse the repository at this point in the history
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()`).

Among other things, non-string identifiers enable:
- Private services (without relying on naming conventions).
- Collision avoidance (without relying on custom prefixes and namespacing).
- Better toolability (e.g. autocomplete support for service identifiers).
- Better compression (i.e. strings can't be minified, non-string values could).

Identifiers for directives and filters are still restricted to string values, since they need to be
referenced in HTML (text).

--
For services with string IDs, the corresponding provider ID is constructed by appending `Provider`
to the service ID. For services with non-string IDs, the corresponding provider has 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, function (barProvider) {
    // `bar` provider injected (because we are in config block)
    // (even though we used the same identifier (`bar`) that we will use in the run block)
  }]).
  run([bar, function (bar) {
    // `bar` service injected (because we are in run block)
  }]);
```

--
This change is backwards compatible (afaict).

Fixes angular#10347
  • Loading branch information
gkalpak committed Aug 6, 2016
1 parent a9863bf commit 32bde2d
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 72 deletions.
26 changes: 0 additions & 26 deletions docs/content/error/$injector/itkn.ngdoc

This file was deleted.

113 changes: 67 additions & 46 deletions src/auto/injector.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
* Implicit module which gets automatically added to each {@link auto.$injector $injector}.
*/

var PROVIDER_ID_SUFFIX = 'Provider';
var ARROW_ARG = /^([^\(]+?)=>/;
var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
Expand Down Expand Up @@ -129,6 +130,14 @@ function annotate(fn, strictDi, name) {
return $inject;
}

function getProviderId(id) {
return !isString(id) ? id : id + PROVIDER_ID_SUFFIX;
}

function stringifyIdForError(id, suffix) {
return isString(id) ? id : id + suffix;
}

///////////////////////////////////////

/**
Expand Down Expand Up @@ -647,10 +656,9 @@ function annotate(fn, strictDi, name) {
function createInjector(modulesToLoad, strictDi) {
strictDi = (strictDi === true);
var INSTANTIATING = {},
providerSuffix = 'Provider',
path = [],
loadedModules = new HashMap(null, true),
providerCache = {
providerCache = new HashMap({
$provide: {
provider: supportObject(provider),
factory: supportObject(factory),
Expand All @@ -659,24 +667,22 @@ function createInjector(modulesToLoad, strictDi) {
constant: supportObject(constant),
decorator: decorator
}
},
providerInjector = (providerCache.$injector =
createInternalInjector(providerCache, function(serviceName, caller) {
if (angular.isString(caller)) {
path.push(caller);
}
}),
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 provider = providerInjector.get(getProviderId(serviceName));
return instanceInjector.invoke(provider.$get, provider);
}),
instanceInjector = protoInstanceInjector;

providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) };
providerCache.put('$injector', providerInjector);
providerCache.put(getProviderId('$injector'), {$get: valueFn(protoInstanceInjector)});

var runBlocks = loadModules(modulesToLoad);
instanceInjector = protoInstanceInjector.get('$injector');
instanceInjector.strictDi = strictDi;
Expand All @@ -690,7 +696,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);
Expand All @@ -706,7 +712,10 @@ function createInjector(modulesToLoad, strictDi) {
if (!provider_.$get) {
throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
}
return (providerCache[name + providerSuffix] = provider_);

providerCache.put(getProviderId(name), provider_);

return provider_;
}

function enforceReturnValue(name, factory) {
Expand Down Expand Up @@ -737,12 +746,12 @@ function createInjector(modulesToLoad, strictDi) {

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 origProvider = providerInjector.get(getProviderId(serviceName)),
orig$get = origProvider.$get;

origProvider.$get = function() {
Expand Down Expand Up @@ -805,29 +814,45 @@ function createInjector(modulesToLoad, strictDi) {
// internal Injector
////////////////////////////////////

function createInternalInjector(cache, factory) {
function createInternalInjector(cache, factory, displayNameSuffix) {
if (!isString(displayNameSuffix)) {
displayNameSuffix = '';
}

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 hasCaller = isDefined(caller);
var hadInstance = cache.has(serviceName);
var instance;

if (hasCaller) {
path.unshift(stringifyIdForError(caller, displayNameSuffix));
}
path.unshift(stringifyIdForError(serviceName, displayNameSuffix));

try {
if (hadInstance) {
instance = cache.get(serviceName);

if (instance === INSTANTIATING) {
throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- '));
}
throw err;
} finally {
path.shift();

return instance;
} else {
cache.put(serviceName, INSTANTIATING);

instance = factory(serviceName);
cache.put(serviceName, instance);

return instance;
}
} finally {
if (!hadInstance && (cache.get(serviceName) === INSTANTIATING)) {
cache.remove(serviceName);
}

path.shift();
if (hasCaller) path.shift();
}
}

Expand All @@ -838,12 +863,8 @@ 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));
var localsHasKey = locals && isString(key) && locals.hasOwnProperty(key);
args.push(localsHasKey ? locals[key] : getService(key, serviceName));
}
return args;
}
Expand Down Expand Up @@ -901,7 +922,7 @@ 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(getProviderId(name));
}
};
}
Expand Down

0 comments on commit 32bde2d

Please sign in to comment.