docs(di-cookbook): InjectionToken->OpaqueToken; clarify class-interface

This commit is contained in:
Ward Bell 2017-04-01 00:08:46 -07:00
parent f38620d882
commit ebd8b48da6
11 changed files with 170 additions and 127 deletions

View File

@ -4,29 +4,10 @@ import { Injectable } from '@angular/core';
import { LoggerService } from './logger.service';
// #docregion minimal-logger
// class used as a restricting interface (hides other public members)
export abstract class MinimalLogger {
logInfo: (msg: string) => void;
logs: string[];
}
// #enddocregion minimal-logger
/*
// Transpiles to:
// #docregion minimal-logger-transpiled
var MinimalLogger = (function () {
function MinimalLogger() {}
return MinimalLogger;
}());
exports("MinimalLogger", MinimalLogger);
// #enddocregion minimal-logger-transpiled
*/
// #docregion date-logger-service
@Injectable()
// #docregion date-logger-service-signature
export class DateLoggerService extends LoggerService implements MinimalLogger
export class DateLoggerService extends LoggerService
// #enddocregion date-logger-service-signature
{
logInfo(msg: any) { super.logInfo(stamp(msg)); }

View File

@ -0,0 +1,26 @@
// Illustrative (not used), mini-version of the actual HeroOfTheMonthComponent
// Injecting with the MinimalLogger "interface-class"
import { Component, NgModule } from '@angular/core';
import { LoggerService } from './logger.service';
import { MinimalLogger } from './minimal-logger.service';
// #docregion
@Component({
selector: 'hero-of-the-month',
templateUrl: './hero-of-the-month.component.html',
// Todo: move this aliasing, `useExisting` provider to the AppModule
providers: [{ provide: MinimalLogger, useExisting: LoggerService }]
})
export class HeroOfTheMonthComponent {
logs: string[] = [];
constructor(logger: MinimalLogger) {
logger.logInfo('starting up');
}
}
// #enddocregion
// This NgModule exists only to avoid the Angular language service's "undeclared component" error
@NgModule({
declarations: [ HeroOfTheMonthComponent ]
})
class NoopModule {}

View File

@ -0,0 +1,9 @@
<h3>{{title}}</h3>
<div>Winner: <strong>{{heroOfTheMonth.name}}</strong></div>
<div>Reason for award: <strong>{{heroOfTheMonth.description}}</strong></div>
<div>Runners-up: <strong id="rups1">{{runnersUp}}</strong></div>
<p>Logs:</p>
<div id="logs">
<div *ngFor="let log of logs">{{log}}</div>
</div>

View File

@ -1,19 +1,19 @@
/* tslint:disable:one-line:check-open-brace*/
// #docplaster
// #docregion opaque-token
import { OpaqueToken } from '@angular/core';
// #docregion injection-token
import { InjectionToken } from '@angular/core';
export const TITLE = new OpaqueToken('title');
// #enddocregion opaque-token
export const TITLE = new InjectionToken<string>('title');
// #enddocregion injection-token
// #docregion hero-of-the-month
import { Component, Inject } from '@angular/core';
import { DateLoggerService,
MinimalLogger } from './date-logger.service';
import { DateLoggerService } from './date-logger.service';
import { Hero } from './hero';
import { HeroService } from './hero.service';
import { LoggerService } from './logger.service';
import { MinimalLogger } from './minimal-logger.service';
import { RUNNERS_UP,
runnersUpFactory } from './runners-up';
@ -22,28 +22,16 @@ import { RUNNERS_UP,
const someHero = new Hero(42, 'Magma', 'Had a great month!', '555-555-5555');
// #enddocregion some-hero
const template = `
<h3>{{title}}</h3>
<div>Winner: <strong>{{heroOfTheMonth.name}}</strong></div>
<div>Reason for award: <strong>{{heroOfTheMonth.description}}</strong></div>
<div>Runners-up: <strong id="rups1">{{runnersUp}}</strong></div>
<p>Logs:</p>
<div id="logs">
<div *ngFor="let log of logs">{{log}}</div>
</div>
`;
// #docregion hero-of-the-month
@Component({
selector: 'hero-of-the-month',
template: template,
templateUrl: './hero-of-the-month.component.html',
providers: [
// #docregion use-value
{ provide: Hero, useValue: someHero },
// #docregion provide-opaque-token
// #docregion provide-injection-token
{ provide: TITLE, useValue: 'Hero of the Month' },
// #enddocregion provide-opaque-token
// #enddocregion provide-injection-token
// #enddocregion use-value
// #docregion use-class
{ provide: HeroService, useClass: HeroService },
@ -52,9 +40,9 @@ const template = `
// #docregion use-existing
{ provide: MinimalLogger, useExisting: LoggerService },
// #enddocregion use-existing
// #docregion provide-opaque-token, use-factory
// #docregion provide-injection-token, use-factory
{ provide: RUNNERS_UP, useFactory: runnersUpFactory(2), deps: [Hero, HeroService] }
// #enddocregion provide-opaque-token, use-factory
// #enddocregion provide-injection-token, use-factory
]
})
export class HeroOfTheMonthComponent {

View File

@ -0,0 +1,22 @@
// #docregion
// Class used as a "narrowing" interface that exposes a minimal logger
// Other members of the actual implementation are invisible
export abstract class MinimalLogger {
logs: string[];
logInfo: (msg: string) => void;
}
// #enddocregion
/*
// Transpiles to:
// #docregion minimal-logger-transpiled
var MinimalLogger = (function () {
function MinimalLogger() {}
return MinimalLogger;
}());
exports("MinimalLogger", MinimalLogger);
// #enddocregion minimal-logger-transpiled
*/
// See http://stackoverflow.com/questions/43154832/unexpected-token-export-in-angular-app-with-systemjs-and-typescript/
export const _ = 0;

View File

@ -1,12 +1,12 @@
// #docplaster
// #docregion
import { OpaqueToken } from '@angular/core';
import { InjectionToken } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';
// #docregion runners-up
export const RUNNERS_UP = new OpaqueToken('RunnersUp');
export const RUNNERS_UP = new InjectionToken<string>('RunnersUp');
// #enddocregion runners-up
// #docregion factory-synopsis

View File

@ -1,7 +1,7 @@
// #docregion token
import { OpaqueToken } from '@angular/core';
import { InjectionToken } from '@angular/core';
export let APP_CONFIG = new OpaqueToken('app.config');
export let APP_CONFIG = new InjectionToken<AppConfig>('app.config');
// #enddocregion token
// #docregion config

View File

@ -8,34 +8,44 @@ include ../_util-fns
:marked
# Contents
- [Application-wide dependencies](#app-wide-dependencies)
- [External module configuration](#external-module-configuration)
- [`@Injectable()` and nested service dependencies](#nested-dependencies)
- [`@Injectable()`](#injectable-1)
- [Limit service scope to a component subtree](#service-scope)
- [Multiple service instances (sandboxing)](#multiple-service-instances)
- [Qualify dependency lookup with `@Optional()` and `@Host()`](#qualify-dependency-lookup)
- [Demonstration](#demonstration)
- [Inject the component's DOM element](#component-element)
- [Define dependencies with providers](#providers)
- [Defining providers](#defining-providers)
- [The *provide* object literal](#provide)
- [`useValue`&mdash;the *value provider*](#usevalue)
- [`useClass`&mdash;the *class provider*](#useclass)
- [`useExisting`&mdash;the *alias provider*](#useexisting)
- [`useFactory`&mdash;the *factory provider*](#usefactory)
- [Provider token alternatives: the class-interface and `OpaqueToken`](#tokens)
- [class-interface](#class-interface)
- [`OpaqueToken`](#opaque-token)
- [Inject into a derived class](#di-inheritance)
- [Find a parent component by injection](#find-parent)
- [Find parent with a known component type](#known-parent)
- [Cannot find a parent by its base class](#base-parent)
- [Find a parent by its class-interface](#class-interface-parent)
- [Find a parent in a tree of parents with `@SkipSelf()`](#parent-tree)
- [The `Parent` class-interface](#parent-token)
- [A `provideParent()` helper function](#provideparent)
- [Break circularities with a forward class reference (*forwardRef*)](#forwardref)
* [Application-wide dependencies](#app-wide-dependencies)
* [External module configuration](#external-module-configuration)
* [`@Injectable()` and nested service dependencies](#nested-dependencies)
* [`@Injectable()`](#injectable-1)
* [Limit service scope to a component subtree](#service-scope)
* [Multiple service instances (sandboxing)](#multiple-service-instances)
* [Qualify dependency lookup with `@Optional()` and `@Host()`](#qualify-dependency-lookup)
* [Demonstration](#demonstration)
* [Inject the component's DOM element](#component-element)
* [Define dependencies with providers](#providers)
* [Defining providers](#defining-providers)
* [The *provide* object literal](#provide)
* [`useValue`&mdash;the *value provider*](#usevalue)
* [`useClass`&mdash;the *class provider*](#useclass)
* [`useExisting`&mdash;the *alias provider*](#useexisting)
* [`useFactory`&mdash;the *factory provider*](#usefactory)
* [Provider token alternatives: the class-interface and `InjectionToken`](#tokens)
* [class-interface](#class-interface)
* [`InjectionToken`](#injection-token)
* [Inject into a derived class](#di-inheritance)
* [Find a parent component by injection](#find-parent)
* [Find parent with a known component type](#known-parent)
* [Cannot find a parent by its base class](#base-parent)
* [Find a parent by its class-interface](#class-interface-parent)
* [Find a parent in a tree of parents with `@SkipSelf()`](#parent-tree)
* [The `Parent` class-interface](#parent-token)
* [A `provideParent()` helper function](#provideparent)
* [Break circularities with a forward class reference (*forwardRef*)](#forwardref)
:marked
See the <live-example name="cb-dependency-injection"></live-example>
@ -385,11 +395,11 @@ a#defining-providers
You need other ways to deliver dependency values and that means you need other ways to specify a provider.
The `HeroOfTheMonthComponent` example demonstrates many of the alternatives and why you need them.
It's visually simple: a few properties and the logs produced by a logger.
figure.image-display
img(src="/resources/images/cookbooks/dependency-injection/hero-of-month.png" alt="Hero of the month" width="300px")
:marked
It's visually simple: a few properties and the output of a logger. The code behind it gives you plenty to think about.
The code behind it gives you plenty to think about.
+makeExample('cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts','hero-of-the-month','hero-of-the-month.component.ts')
.l-main-section
@ -400,15 +410,14 @@ a(id='provide')
The `provide` object literal takes a *token* and a *definition object*.
The *token* is usually a class but [it doesn't have to be](#tokens).
The *definition* object has one main property, `useValue`, that indicates how the provider
should create or return the provided value.
The *definition* object has a required property that specifies how to create the singleton instance of the service. In this case, the property.
.l-main-section
a(id='usevalue')
:marked
#### useValue&mdash;the *value provider*
Set the `useValue` property to a ***fixed value*** that the provider can return as the dependency object.
Set the `useValue` property to a ***fixed value*** that the provider can return as the service instance (AKA, the "dependency object").
Use this technique to provide *runtime configuration constants* such as website base addresses and feature flags.
You can use a *value provider* in a unit test to replace a production service with a fake or mock.
@ -422,8 +431,9 @@ a(id='usevalue')
and the consumer of the injected hero would want the type information.
The `TITLE` provider token is *not a class*.
It's a special kind of provider lookup key called an [OpaqueToken](#opaquetoken).
You can use an `OpaqueToken` when the dependency is a simple value like a string, a number, or a function.
It's a special kind of provider lookup key called an [InjectionToken](#injection-token).
You can use an `InjectionToken` for any kind of provider but it's particular
helpful when the dependency is a simple value like a string, a number, or a function.
The value of a *value provider* must be defined *now*. You can't create the value later.
Obviously the title string literal is immediately available.
@ -447,7 +457,7 @@ a(id='useclass')
+makeExample('cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts','use-class')(format='.')
:marked
The first provider is the *de-sugared*, expanded form of the most typical case in which the
class to be created (`HeroService`) is also the provider's injection token.
class to be created (`HeroService`) is also the provider's dependency injection token.
It's in this long form to de-mystify the preferred short form.
The second provider substitutes the `DateLoggerService` for the `LoggerService`.
@ -472,19 +482,25 @@ a(id='useexisting')
+makeExample('cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts','use-existing')
:marked
Narrowing an API through an aliasing interface is _one_ important use case for this technique.
This example shows aliasing for that very purpose here.
Imagine that the `LoggerService` had a large API; it's actually only three methods and a property.
You'd want to shrink that API surface to just the two members exposed by the `MinimalLogger` [*class-interface*](#class-interface):
The following example shows aliasing for that purpose.
+makeExample('cb-dependency-injection/ts/src/app/date-logger.service.ts','minimal-logger','src/app/date-logger.service.ts (MinimalLogger)')(format='.')
Imagine that the `LoggerService` had a large API, much larger than the actual three methods and a property.
You might want to shrink that API surface to just the members you actually need.
Here the `MinimalLogger` [*class-interface*](#class-interface) reduces the API to two members:
+makeExample('cb-dependency-injection/ts/src/app/minimal-logger.service.ts', null,'src/app/minimal-logger.service.ts')(format='.')
:marked
The constructor's `logger` parameter is typed as `MinimalLogger` so only its two members are visible in TypeScript:
Now put it to use in a simplified version of the `HeroOfTheMonthComponent`.
+makeExample('cb-dependency-injection/ts/src/app/hero-of-the-month.component.1.ts', null,'src/app/hero-of-the-month.component.ts (minimal version)')(format='.')
:marked
The `HeroOfTheMonthComponent` constructor's `logger` parameter is typed as `MinimalLogger` so only the `logs` and `logInfo` members are visible in a TypeScript-aware editor:
figure.image-display
img(src="/resources/images/cookbooks/dependency-injection/minimal-logger-intellisense.png" alt="MinimalLogger restricted API")
:marked
Angular actually sets the `logger` parameter to the injector's full version of the `LoggerService`
which happens to be the `DateLoggerService`. This is because of the override provider
registered previously via `useClass`.
Behind the scenes, Angular actually sets the `logger` parameter to the full service registered under the `LoggingService` token which happens to be the `DateLoggerService` that was [provided above](#useclass).
.l-sub-section
:marked
The following image, which displays the logging date, confirms the point:
figure.image-display
img(src="/resources/images/cookbooks/dependency-injection/date-logger-entry.png" alt="DateLoggerService entry" width="300px")
@ -533,7 +549,7 @@ a(id='usefactory')
a(id="tokens")
.l-main-section
:marked
## Provider token alternatives: the *class-interface* and *OpaqueToken*
## Provider token alternatives: the *class-interface* and *InjectionToken*
Angular dependency injection is easiest when the provider *token* is a class
that is also the type of the returned dependency object, or what you usually call the *service*.
@ -550,29 +566,26 @@ a(id="tokens")
+makeExample('cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts','use-existing')
:marked
The `MinimalLogger` is an abstract class.
+makeExample('cb-dependency-injection/ts/src/app/date-logger.service.ts','minimal-logger')(format='.')
+makeExample('cb-dependency-injection/ts/src/app/minimal-logger.service.ts')(format='.')
:marked
You usually inherit from an abstract class.
But `LoggerService` doesn't inherit from `MinimalLogger`. *No class* inherits from it.
Instead, you use it like an interface.
But *no class* in this application inherits from `MinimalLogger`.
Look again at the declaration for `DateLoggerService`:
+makeExample('cb-dependency-injection/ts/src/app/date-logger.service.ts','date-logger-service-signature')(format='.')
:marked
`DateLoggerService` inherits (extends) from `LoggerService`, not `MinimalLogger`.
The `DateLoggerService` *implements* `MinimalLogger` as if `MinimalLogger` were an *interface*.
The `LoggerService` and the `DateLoggerService` _could_ have inherited from `MinimalLogger`.
They could have _implemented_ it instead in the manner of an interface.
But they did neither.
The `MinimalLogger` is used exclusively as a dependency injection token.
When you use a class this way, it's called a ***class-interface***.
The key benefit of a *class-interface* is that you can get the strong-typing of an interface
and you can ***use it as a provider token*** in the same manner as a normal class.
and you can ***use it as a provider token*** in the way you would a normal class.
A ***class-interface*** should define *only* the members that its consumers are allowed to call.
Such a narrowing interface helps decouple the concrete class from its consumers.
The `MinimalLogger` defines just two of the `LoggerClass` members.
.l-sub-section
:marked
#### Why *MinimalLogger* is a class and not an interface
#### Why *MinimalLogger* is a class and not a TypeScript interface
You can't use an interface as a provider token because
interfaces are not JavaScript objects.
They exist only in the TypeScript design space.
@ -581,17 +594,17 @@ a(id="tokens")
A provider token must be a real JavaScript object of some kind:
such as a function, an object, a string, or a class.
Using a class as an interface gives you the characteristics of an interface in a JavaScript object.
Using a class as an interface gives you the characteristics of an interface in a real JavaScript object.
To minimize memory cost, the class should have *no implementation*.
The `MinimalLogger` transpiles to this unoptimized, pre-minified JavaScript:
+makeExample('cb-dependency-injection/ts/src/app/date-logger.service.ts','minimal-logger-transpiled')(format='.')
Of course a real object occupies memory. To minimize memory cost, the class should have *no implementation*.
The `MinimalLogger` transpiles to this unoptimized, pre-minified JavaScript for a constructor function:
+makeExample('cb-dependency-injection/ts/src/app/minimal-logger.service.ts','minimal-logger-transpiled')(format='.')
:marked
It never grows larger no matter how many members you add *as long as they are typed but not implemented*.
Notice that it doesn't have a single member. It never grows no matter how many members you add to the class *as long as those members are typed but not implemented*. Look again at the TypeScript `MinimalLogger` class to confirm that it has no implementation.
a(id='opaque-token')
a(id='injection-token')
:marked
### _OpaqueToken_
### _InjectionToken_
Dependency objects can be simple values like dates, numbers and strings, or
shapeless objects like arrays and functions.
@ -601,14 +614,16 @@ a(id='opaque-token')
a JavaScript object that has a friendly name but won't conflict with
another token that happens to have the same name.
The `OpaqueToken` has these characteristics.
The `InjectionToken` has these characteristics.
You encountered them twice in the *Hero of the Month* example,
in the *title* value provider and in the *runnersUp* factory provider.
+makeExample('cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts','provide-opaque-token')(format='.')
+makeExample('cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts','provide-injection-token')(format='.')
:marked
You created the `TITLE` token like this:
+makeExample('cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts','opaque-token')(format='.')
+makeExample('cb-dependency-injection/ts/src/app/hero-of-the-month.component.ts','injection-token')(format='.')
:marked
The type parameter, while optional, conveys the dependency's type to developers and tooling.
The token description is another developer aid.
a(id="di-inheritance")
.l-main-section

View File

@ -251,7 +251,7 @@ a#decoration
At the core, an [`injector`](#injector) returns dependency values on request.
The expression `injector.get(token)` returns the value associated with the given token.
A token is an Angular type (`OpaqueToken`). You rarely need to work with tokens directly; most
A token is an Angular type (`InjectionToken`). You rarely need to work with tokens directly; most
methods accept a class name (`Foo`) or a string ("foo") and Angular converts it
to a token. When you write `injector.get(Foo)`, the injector returns
the value associated with the token for the `Foo` class, typically an instance of `Foo` itself.

View File

@ -39,7 +39,7 @@ block includes
* [Dependency injection tokens](#dependency-injection-tokens)
* [Non-class dependencies](#non-class-dependencies)
* [`OpaqueToken`](#opaquetoken)
* [`InjectionToken`](#injection-token)
* [Optional dependencies](#optional)
* [Summary](#summary)
@ -645,7 +645,7 @@ a#value-provider
:marked
See more `useValue` examples in the
[Non-class dependencies](#non-class-dependencies) and
[OpaqueToken](#opaquetoken) sections.
[InjectionToken](#injection-token) sections.
#factory-provider
:marked
@ -784,18 +784,20 @@ p
The TypeScript interface disappears from the generated JavaScript.
There is no interface type information left for Angular to find at runtime.
a#opaquetoken
a#injection-token
:marked
### _OpaqueToken_
### _InjectionToken_
One solution to choosing a provider token for non-class dependencies is
to define and use an <a href="../api/core/index/OpaqueToken-class.html"><b>OpaqueToken</b></a>.
The definition looks like this:
to define and use an <a href="../api/core/index/InjectionToken-class.html"><b>InjectionToken</b></a>.
The definition of such a token looks like this:
+makeExample('dependency-injection/ts/src/app/app.config.ts','token')(format='.')
:marked
Register the dependency provider using the `OpaqueToken` object:
The type parameter, while optional, conveys the dependency's type to developers and tooling.
The token description is another developer aid.
Register the dependency provider using the `InjectionToken` object:
+makeExample('dependency-injection/ts/src/app/providers.component.ts','providers-9')(format=".")