* closes #3086 * samples reworked to conform to our sample style, make more sense, and cover the points in prose. * copy edits to bring closer to Google docs standards.
This commit is contained in:
parent
09f79e3a19
commit
4253378e66
|
@ -1,56 +1,103 @@
|
|||
import { browser, element, by } from 'protractor';
|
||||
'use strict'; // necessary for es6 output in node
|
||||
|
||||
describe('Hierarchical dependency injection', function () {
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
beforeEach(function () {
|
||||
describe('Hierarchical dependency injection', () => {
|
||||
|
||||
beforeAll(() => {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
it('should open with a card view', function () {
|
||||
expect(element.all(by.cssContainingText('button', 'edit')).get(0).isDisplayed()).toBe(true,
|
||||
'edit button should be displayed');
|
||||
describe('Heroes Scenario', () => {
|
||||
let page = {
|
||||
heroName: '',
|
||||
income: '',
|
||||
|
||||
// queries
|
||||
heroEl: element.all(by.css('heroes-list li')).get(0), // first hero
|
||||
heroCardEl: element(by.css('heroes-list hero-tax-return')), // first hero tax-return
|
||||
taxReturnNameEl: element.all(by.css('heroes-list hero-tax-return #name')).get(0),
|
||||
incomeInputEl: element.all(by.css('heroes-list hero-tax-return input')).get(0),
|
||||
cancelButtonEl: element(by.cssContainingText('heroes-list hero-tax-return button', 'Cancel')),
|
||||
closeButtonEl: element(by.cssContainingText('heroes-list hero-tax-return button', 'Close')),
|
||||
saveButtonEl: element(by.cssContainingText('heroes-list hero-tax-return button', 'Save'))
|
||||
};
|
||||
|
||||
it('should list multiple heroes', () => {
|
||||
expect(element.all(by.css('heroes-list li')).count()).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('should show no hero tax-returns at the start', () => {
|
||||
expect(element.all(by.css('heroes-list li hero-tax-return')).count()).toBe(0);
|
||||
});
|
||||
|
||||
it('should open first hero in hero-tax-return view after click', () => {
|
||||
page.heroEl.getText()
|
||||
.then(val => {
|
||||
page.heroName = val;
|
||||
})
|
||||
.then(() => page.heroEl.click())
|
||||
.then(() => {
|
||||
expect(page.heroCardEl.isDisplayed()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('hero tax-return should have first hero\'s name', () => {
|
||||
// Not `page.tax-returnNameInputEl.getAttribute('value')` although later that is essential
|
||||
expect(page.taxReturnNameEl.getText()).toEqual(page.heroName);
|
||||
});
|
||||
|
||||
it('should be able to cancel change', () => {
|
||||
page.incomeInputEl.clear()
|
||||
.then(() => page.incomeInputEl.sendKeys('777'))
|
||||
.then(() => {
|
||||
expect(page.incomeInputEl.getAttribute('value')).toBe('777', 'income should be 777');
|
||||
return page.cancelButtonEl.click();
|
||||
})
|
||||
.then(() => {
|
||||
expect(page.incomeInputEl.getAttribute('value')).not.toBe('777', 'income should not be 777');
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to save change', () => {
|
||||
page.incomeInputEl.clear()
|
||||
.then(() => page.incomeInputEl.sendKeys('999'))
|
||||
.then(() => {
|
||||
expect(page.incomeInputEl.getAttribute('value')).toBe('999', 'income should be 999');
|
||||
return page.saveButtonEl.click();
|
||||
})
|
||||
.then(() => {
|
||||
expect(page.incomeInputEl.getAttribute('value')).toBe('999', 'income should still be 999');
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to close tax-return', () => {
|
||||
page.saveButtonEl.click()
|
||||
.then(() => {
|
||||
expect(element.all(by.css('heroes-list li hero-tax-return')).count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should have multiple heroes listed', function () {
|
||||
expect(element.all(by.css('heroes-list li')).count()).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('should change to editor view after selection', function () {
|
||||
let editButtonEle = element.all(by.cssContainingText('button', 'edit')).get(0);
|
||||
editButtonEle.click().then(function() {
|
||||
expect(editButtonEle.isDisplayed()).toBe(false, 'edit button should be hidden after selection');
|
||||
describe('Villains Scenario', () => {
|
||||
it('should list multiple villains', () => {
|
||||
expect(element.all(by.css('villains-list li')).count()).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to save editor change', function () {
|
||||
testEdit(true);
|
||||
});
|
||||
describe('Cars Scenario', () => {
|
||||
|
||||
it('should be able to cancel editor change', function () {
|
||||
testEdit(false);
|
||||
});
|
||||
|
||||
function testEdit(shouldSave: boolean) {
|
||||
// select 2nd ele
|
||||
let heroEle = element.all(by.css('heroes-list li')).get(1);
|
||||
// get the 2nd span which is the name of the hero
|
||||
let heroNameEle = heroEle.all(by.css('hero-card span')).get(1);
|
||||
let editButtonEle = heroEle.element(by.cssContainingText('button', 'edit'));
|
||||
editButtonEle.click().then(function() {
|
||||
let inputEle = heroEle.element(by.css('hero-editor input'));
|
||||
return inputEle.sendKeys('foo');
|
||||
}).then(function() {
|
||||
let buttonName = shouldSave ? 'save' : 'cancel';
|
||||
let buttonEle = heroEle.element(by.cssContainingText('button', buttonName));
|
||||
return buttonEle.click();
|
||||
}).then(function() {
|
||||
if (shouldSave) {
|
||||
expect(heroNameEle.getText()).toContain('foo');
|
||||
} else {
|
||||
expect(heroNameEle.getText()).not.toContain('foo');
|
||||
}
|
||||
it('A-component should use expected services', () => {
|
||||
expect(element(by.css('a-car')).getText()).toContain('C1-E1-T1');
|
||||
});
|
||||
}
|
||||
|
||||
it('B-component should use expected services', () => {
|
||||
expect(element(by.css('b-car')).getText()).toContain('C2-E2-T1');
|
||||
});
|
||||
|
||||
it('C-component should use expected services', () => {
|
||||
expect(element(by.css('c-car')).getText()).toContain('C3-E2-T1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-app',
|
||||
template: `
|
||||
<label><input type="checkbox" [checked]="showHeroes" (change)="showHeroes=!showHeroes">Heroes</label>
|
||||
<label><input type="checkbox" [checked]="showVillains" (change)="showVillains=!showVillains">Villains</label>
|
||||
<label><input type="checkbox" [checked]="showCars" (change)="showCars=!showCars">Cars</label>
|
||||
|
||||
<h1>Hierarchical Dependency Injection</h1>
|
||||
|
||||
<heroes-list *ngIf="showHeroes"></heroes-list>
|
||||
<villains-list *ngIf="showVillains"></villains-list>
|
||||
<my-cars *ngIf="showCars"></my-cars>
|
||||
`
|
||||
})
|
||||
export class AppComponent {
|
||||
showCars = true;
|
||||
showHeroes = true;
|
||||
showVillains = true;
|
||||
}
|
|
@ -1,43 +1,33 @@
|
|||
// #docregion
|
||||
import { NgModule } from '@angular/core';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { HeroesListComponent } from './heroes-list.component';
|
||||
import { HeroEditorComponent } from './hero-editor.component';
|
||||
import { HeroCardComponent } from './hero-card.component';
|
||||
import { HeroesService } from './heroes.service';
|
||||
import { AppComponent } from './app.component';
|
||||
import { HeroTaxReturnComponent } from './hero-tax-return.component';
|
||||
import { HeroesListComponent } from './heroes-list.component';
|
||||
import { HeroesService } from './heroes.service';
|
||||
import { VillainsListComponent } from './villains-list.component';
|
||||
|
||||
import { carComponents, carServices } from './car.components';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule
|
||||
],
|
||||
providers: [ HeroesService ],
|
||||
providers: [
|
||||
carServices,
|
||||
HeroesService
|
||||
],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
carComponents,
|
||||
HeroesListComponent,
|
||||
HeroCardComponent,
|
||||
HeroEditorComponent
|
||||
HeroTaxReturnComponent,
|
||||
VillainsListComponent
|
||||
],
|
||||
bootstrap: [ HeroesListComponent ]
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
||||
/* Documentation artifact below
|
||||
// #docregion bad-alternative
|
||||
// Don't do this!
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule
|
||||
],
|
||||
providers: [ HeroesService, RestoreService ],
|
||||
declarations: [ HeroesListComponent ],
|
||||
bootstrap: [
|
||||
HeroesListComponent,
|
||||
HeroCardComponent,
|
||||
HeroEditorComponent
|
||||
]
|
||||
})
|
||||
// #enddocregion bad-alternative
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
import {
|
||||
CarService, CarService2, CarService3,
|
||||
EngineService, EngineService2, TiresService
|
||||
} from './car.services';
|
||||
|
||||
////////// CCarComponent ////////////
|
||||
@Component({
|
||||
selector: 'c-car',
|
||||
template: `<div>C: {{description}}</div>`,
|
||||
providers: [
|
||||
{ provide: CarService, useClass: CarService3 }
|
||||
]
|
||||
})
|
||||
export class CCarComponent {
|
||||
description: string;
|
||||
constructor(carService: CarService) {
|
||||
this.description = `${carService.getCar().description} (${carService.name})`;
|
||||
}
|
||||
}
|
||||
|
||||
////////// BCarComponent ////////////
|
||||
@Component({
|
||||
selector: 'b-car',
|
||||
template: `
|
||||
<div>B: {{description}}</div>
|
||||
<c-car></c-car>
|
||||
`,
|
||||
providers: [
|
||||
{ provide: CarService, useClass: CarService2 },
|
||||
{ provide: EngineService, useClass: EngineService2 }
|
||||
]
|
||||
})
|
||||
export class BCarComponent {
|
||||
description: string;
|
||||
constructor(carService: CarService) {
|
||||
this.description = `${carService.getCar().description} (${carService.name})`;
|
||||
}
|
||||
}
|
||||
|
||||
////////// ACarComponent ////////////
|
||||
@Component({
|
||||
selector: 'a-car',
|
||||
template: `
|
||||
<div>A: {{description}}</div>
|
||||
<b-car></b-car>`
|
||||
})
|
||||
export class ACarComponent {
|
||||
description: string;
|
||||
constructor(carService: CarService) {
|
||||
this.description = `${carService.getCar().description} (${carService.name})`;
|
||||
}
|
||||
}
|
||||
////////// CarsComponent ////////////
|
||||
@Component({
|
||||
selector: 'my-cars',
|
||||
template: `
|
||||
<h3>Cars</h3>
|
||||
<a-car></a-car>`
|
||||
})
|
||||
export class CarsComponent { }
|
||||
|
||||
////////////////
|
||||
|
||||
export const carComponents = [
|
||||
CarsComponent,
|
||||
ACarComponent, BCarComponent, CCarComponent
|
||||
];
|
||||
|
||||
// generic car-related services
|
||||
export const carServices = [
|
||||
CarService, EngineService, TiresService
|
||||
];
|
|
@ -0,0 +1,95 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
/// Model ///
|
||||
export class Car {
|
||||
name = 'Avocado Motors';
|
||||
constructor(public engine: Engine, public tires: Tires) { }
|
||||
|
||||
get description() {
|
||||
return `${this.name} car with ` +
|
||||
`${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
|
||||
}
|
||||
}
|
||||
|
||||
export class Engine {
|
||||
cylinders = 4;
|
||||
}
|
||||
|
||||
export class Tires {
|
||||
make = 'Flintstone';
|
||||
model = 'Square';
|
||||
}
|
||||
|
||||
//// Engine services ///
|
||||
@Injectable()
|
||||
export class EngineService {
|
||||
id = 'E1';
|
||||
getEngine() { return new Engine(); }
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EngineService2 {
|
||||
id = 'E2';
|
||||
getEngine() {
|
||||
const eng = new Engine();
|
||||
eng.cylinders = 8;
|
||||
return eng;
|
||||
}
|
||||
}
|
||||
|
||||
//// Tire services ///
|
||||
@Injectable()
|
||||
export class TiresService {
|
||||
id = 'T1';
|
||||
getTires() { return new Tires(); }
|
||||
}
|
||||
|
||||
/// Car Services ///
|
||||
@Injectable()
|
||||
export class CarService {
|
||||
id = 'C1';
|
||||
constructor(
|
||||
protected engineService: EngineService,
|
||||
protected tiresService: TiresService) { }
|
||||
|
||||
getCar() {
|
||||
return new Car(
|
||||
this.engineService.getEngine(),
|
||||
this.tiresService.getTires());
|
||||
}
|
||||
|
||||
get name() {
|
||||
return `${this.id}-${this.engineService.id}-${this.tiresService.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CarService2 extends CarService {
|
||||
id = 'C2';
|
||||
constructor(
|
||||
protected engineService: EngineService,
|
||||
protected tiresService: TiresService) {
|
||||
super(engineService, tiresService);
|
||||
}
|
||||
getCar() {
|
||||
const car = super.getCar();
|
||||
car.name = 'BamBam Motors, BroVan 2000';
|
||||
return car;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CarService3 extends CarService2 {
|
||||
id = 'C3';
|
||||
constructor(
|
||||
protected engineService: EngineService,
|
||||
protected tiresService: TiresService) {
|
||||
super(engineService, tiresService);
|
||||
}
|
||||
getCar() {
|
||||
const car = super.getCar();
|
||||
car.name = 'Chizzamm Motors, Calico UltraMax Supreme';
|
||||
return car;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// #docregion
|
||||
export class EditItem<T> {
|
||||
editing: boolean;
|
||||
constructor (public item: T) {}
|
||||
}
|
||||
// #docregion
|
|
@ -1,17 +0,0 @@
|
|||
// #docregion
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
import { Hero } from './hero';
|
||||
|
||||
@Component({
|
||||
selector: 'hero-card',
|
||||
template: `
|
||||
<div>
|
||||
<span>Name:</span>
|
||||
<span>{{hero.name}}</span>
|
||||
</div>`
|
||||
})
|
||||
export class HeroCardComponent {
|
||||
@Input() hero: Hero;
|
||||
}
|
||||
// #docregion
|
|
@ -1,47 +0,0 @@
|
|||
// #docregion
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
import { RestoreService } from './restore.service';
|
||||
import { Hero } from './hero';
|
||||
|
||||
@Component({
|
||||
selector: 'hero-editor',
|
||||
// #docregion providers
|
||||
providers: [RestoreService],
|
||||
// #enddocregion providers
|
||||
template: `
|
||||
<div>
|
||||
<span>Name:</span>
|
||||
<input [(ngModel)]="hero.name"/>
|
||||
<div>
|
||||
<button (click)="onSaved()">save</button>
|
||||
<button (click)="onCanceled()">cancel</button>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
|
||||
export class HeroEditorComponent {
|
||||
@Output() canceled = new EventEmitter<Hero>();
|
||||
@Output() saved = new EventEmitter<Hero>();
|
||||
|
||||
constructor(private restoreService: RestoreService<Hero>) {}
|
||||
|
||||
@Input()
|
||||
set hero (hero: Hero) {
|
||||
this.restoreService.setItem(hero);
|
||||
}
|
||||
|
||||
get hero () {
|
||||
return this.restoreService.getItem();
|
||||
}
|
||||
|
||||
onSaved () {
|
||||
this.saved.emit(this.restoreService.getItem());
|
||||
}
|
||||
|
||||
onCanceled () {
|
||||
this.hero = this.restoreService.restoreItem();
|
||||
this.canceled.emit(this.hero);
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,22 @@
|
|||
.tax-return { border: thin dashed green; margin: 1em; padding: 1em; width: 18em; position: relative }
|
||||
#name { font-weight: bold;}
|
||||
#tid { float: right; }
|
||||
input { font-size: 100%; padding-left: 2px; width: 6em; }
|
||||
input.num { text-align: right; padding-left: 0; padding-right: 4px; width: 4em;}
|
||||
fieldset { border: 0 none;}
|
||||
|
||||
.msg {
|
||||
color: white;
|
||||
font-size: 150%;
|
||||
position: absolute;
|
||||
/*opacity: 0.3;*/
|
||||
left: 2px;
|
||||
top: 3em;
|
||||
width: 98%;
|
||||
background-color: green;
|
||||
text-align: center;
|
||||
}
|
||||
.msg.canceled {
|
||||
color: white;
|
||||
background-color: red;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<div class="tax-return">
|
||||
<div class="msg" [class.canceled]="message==='Canceled'">{{message}}</div>
|
||||
<fieldset>
|
||||
<span id=name>{{taxReturn.name}}</span>
|
||||
<label id=tid>TID: {{taxReturn.tid}}</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>
|
||||
Income: <input [(ngModel)]="taxReturn.income" class="num">
|
||||
</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label>Tax: {{taxReturn.tax}}</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<button (click)="onSaved()">Save</button>
|
||||
<button (click)="onCanceled()">Cancel</button>
|
||||
<button (click)="onClose()">Close</button>
|
||||
</fieldset>
|
||||
</div>
|
|
@ -0,0 +1,44 @@
|
|||
// #docregion
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { HeroTaxReturn } from './hero';
|
||||
import { HeroTaxReturnService } from './hero-tax-return.service';
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'hero-tax-return',
|
||||
templateUrl: './hero-tax-return.component.html',
|
||||
styleUrls: [ './hero-tax-return.component.css' ],
|
||||
// #docregion providers
|
||||
providers: [ HeroTaxReturnService ]
|
||||
// #enddocregion providers
|
||||
})
|
||||
export class HeroTaxReturnComponent {
|
||||
message = '';
|
||||
@Output() close = new EventEmitter<void>();
|
||||
@Input()
|
||||
get taxReturn(): HeroTaxReturn {
|
||||
return this.heroTaxReturnService.taxReturn;
|
||||
}
|
||||
set taxReturn (htr: HeroTaxReturn) {
|
||||
this.heroTaxReturnService.taxReturn = htr;
|
||||
}
|
||||
|
||||
constructor(private heroTaxReturnService: HeroTaxReturnService ) { }
|
||||
|
||||
onCanceled() {
|
||||
this.flashMessage('Canceled');
|
||||
this.heroTaxReturnService.restoreTaxReturn();
|
||||
};
|
||||
|
||||
onClose() { this.close.emit(); };
|
||||
|
||||
onSaved() {
|
||||
this.flashMessage('Saved');
|
||||
this.heroTaxReturnService.saveTaxReturn();
|
||||
}
|
||||
|
||||
flashMessage(msg: string) {
|
||||
this.message = msg;
|
||||
setTimeout(() => this.message = '', 500);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// #docregion
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HeroTaxReturn } from './hero';
|
||||
import { HeroesService } from './heroes.service';
|
||||
|
||||
@Injectable()
|
||||
export class HeroTaxReturnService {
|
||||
private currentTaxReturn: HeroTaxReturn;
|
||||
private originalTaxReturn: HeroTaxReturn;
|
||||
|
||||
constructor(private heroService: HeroesService) { }
|
||||
|
||||
set taxReturn (htr: HeroTaxReturn) {
|
||||
this.originalTaxReturn = htr;
|
||||
this.currentTaxReturn = htr.clone();
|
||||
}
|
||||
|
||||
get taxReturn (): HeroTaxReturn {
|
||||
return this.currentTaxReturn;
|
||||
}
|
||||
|
||||
restoreTaxReturn() {
|
||||
this.taxReturn = this.originalTaxReturn;
|
||||
}
|
||||
|
||||
saveTaxReturn() {
|
||||
this.taxReturn = this.currentTaxReturn;
|
||||
this.heroService.saveTaxReturn(this.currentTaxReturn).subscribe();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,31 @@
|
|||
// #docregion
|
||||
|
||||
export class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
power: string;
|
||||
tid: string; // tax id
|
||||
}
|
||||
|
||||
//// HeroTaxReturn ////
|
||||
let nextId = 100;
|
||||
|
||||
export class HeroTaxReturn {
|
||||
constructor(
|
||||
public id = nextId++,
|
||||
public hero: Hero,
|
||||
public income = 0 ) {
|
||||
if (id === 0) { id = nextId++; }
|
||||
}
|
||||
|
||||
get name() { return this.hero.name; }
|
||||
get tax() { return this.income ? .10 * this.income : 0; }
|
||||
get tid() { return this.hero.tid; }
|
||||
|
||||
toString() {
|
||||
return `${this.hero.name}`;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new HeroTaxReturn(this.id, this.hero, this.income);
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
||||
|
|
|
@ -1,51 +1,48 @@
|
|||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { EditItem } from './edit-item';
|
||||
import { HeroesService } from './heroes.service';
|
||||
import { Hero } from './hero';
|
||||
import { Hero, HeroTaxReturn } from './hero';
|
||||
import { HeroesService } from './heroes.service';
|
||||
|
||||
@Component({
|
||||
selector: 'heroes-list',
|
||||
template: `
|
||||
<div>
|
||||
<h3>Hero Tax Returns</h3>
|
||||
<ul>
|
||||
<li *ngFor="let editItem of heroes">
|
||||
<hero-card
|
||||
[hidden]="editItem.editing"
|
||||
[hero]="editItem.item">
|
||||
</hero-card>
|
||||
<button
|
||||
[hidden]="editItem.editing"
|
||||
(click)="editItem.editing = true">
|
||||
edit
|
||||
</button>
|
||||
<hero-editor
|
||||
(saved)="onSaved(editItem, $event)"
|
||||
(canceled)="onCanceled(editItem)"
|
||||
[hidden]="!editItem.editing"
|
||||
[hero]="editItem.item">
|
||||
</hero-editor>
|
||||
<li *ngFor="let hero of heroes | async"
|
||||
(click)="showTaxReturn(hero)">{{hero.name}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>`
|
||||
<hero-tax-return
|
||||
*ngFor="let selected of selectedTaxReturns; let i = index"
|
||||
[taxReturn]="selected"
|
||||
(close)="closeTaxReturn(i)">
|
||||
</hero-tax-return>
|
||||
</div>
|
||||
`,
|
||||
styles: [ 'li {cursor: pointer;}' ]
|
||||
})
|
||||
export class HeroesListComponent {
|
||||
heroes: Array<EditItem<Hero>>;
|
||||
constructor(heroesService: HeroesService) {
|
||||
this.heroes = heroesService.getHeroes()
|
||||
.map(item => new EditItem(item));
|
||||
heroes: Observable<Hero[]>;
|
||||
selectedTaxReturns: HeroTaxReturn[] = [];
|
||||
|
||||
constructor(private heroesService: HeroesService) {
|
||||
this.heroes = heroesService.getHeroes();
|
||||
}
|
||||
|
||||
onSaved (editItem: EditItem<Hero>, updatedHero: Hero) {
|
||||
editItem.item = updatedHero;
|
||||
editItem.editing = false;
|
||||
showTaxReturn(hero: Hero) {
|
||||
this.heroesService.getTaxReturn(hero)
|
||||
.subscribe(htr => {
|
||||
// show if not currently shown
|
||||
if (!this.selectedTaxReturns.find(tr => tr.id === htr.id)) {
|
||||
this.selectedTaxReturns.push(htr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCanceled (editItem: EditItem<Hero>) {
|
||||
editItem.editing = false;
|
||||
closeTaxReturn(ix: number) {
|
||||
this.selectedTaxReturns.splice(ix, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// #enddocregion
|
||||
|
|
|
@ -1,12 +1,47 @@
|
|||
import { Hero } from './hero';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscriber } from 'rxjs/Subscriber';
|
||||
|
||||
import { Hero, HeroTaxReturn } from './hero';
|
||||
|
||||
@Injectable()
|
||||
export class HeroesService {
|
||||
heroes: Array<Hero> = [
|
||||
{ name: 'RubberMan', power: 'flexibility'},
|
||||
{ name: 'Tornado', power: 'Weather changer'}
|
||||
heroes: Hero[] = [
|
||||
{ id: 1, name: 'RubberMan', tid: '082-27-5678'},
|
||||
{ id: 2, name: 'Tornado', tid: '099-42-4321'}
|
||||
];
|
||||
|
||||
getHeroes () {
|
||||
return this.heroes;
|
||||
heroTaxReturns: HeroTaxReturn[] = [
|
||||
new HeroTaxReturn(10, this.heroes[0], 35000),
|
||||
new HeroTaxReturn(20, this.heroes[1], 1250000)
|
||||
];
|
||||
|
||||
getHeroes(): Observable<Hero[]> {
|
||||
return new Observable<Hero[]>((subscriber: Subscriber<Hero[]>) => {
|
||||
subscriber.next(this.heroes);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getTaxReturn(hero: Hero): Observable<HeroTaxReturn> {
|
||||
return new Observable<HeroTaxReturn>((subscriber: Subscriber<HeroTaxReturn>) => {
|
||||
const htr = this.heroTaxReturns.find(t => t.hero.id === hero.id);
|
||||
subscriber.next(htr || new HeroTaxReturn(0, hero));
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
|
||||
saveTaxReturn(heroTaxReturn: HeroTaxReturn): Observable<HeroTaxReturn> {
|
||||
return new Observable<HeroTaxReturn>((subscriber: Subscriber<HeroTaxReturn>) => {
|
||||
const htr = this.heroTaxReturns.find(t => t.id === heroTaxReturn.id);
|
||||
if (htr) {
|
||||
heroTaxReturn = Object.assign(htr, heroTaxReturn); // demo: mutate
|
||||
} else {
|
||||
this.heroTaxReturns.push(heroTaxReturn);
|
||||
}
|
||||
subscriber.next(heroTaxReturn);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
// #docregion
|
||||
export class RestoreService<T> {
|
||||
originalItem: T;
|
||||
currentItem: T;
|
||||
|
||||
setItem (item: T) {
|
||||
this.originalItem = item;
|
||||
this.currentItem = this.clone(item);
|
||||
}
|
||||
|
||||
getItem (): T {
|
||||
return this.currentItem;
|
||||
}
|
||||
|
||||
restoreItem (): T {
|
||||
this.currentItem = this.originalItem;
|
||||
return this.getItem();
|
||||
}
|
||||
|
||||
clone (item: T): T {
|
||||
// super poor clone implementation
|
||||
return JSON.parse(JSON.stringify(item));
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,6 @@
|
|||
<div>
|
||||
<h3>Villains</h3>
|
||||
<ul>
|
||||
<li *ngFor="let villain of villaines | async">{{villain.name}}</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,21 @@
|
|||
// #docregion
|
||||
import { Component } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
import { Villain, VillainsService } from './villains.service';
|
||||
|
||||
// #docregion metadata
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'villains-list',
|
||||
templateUrl: './villains-list.component.html',
|
||||
providers: [ VillainsService ]
|
||||
})
|
||||
// #enddocregion metadata
|
||||
export class VillainsListComponent {
|
||||
villaines: Observable<Villain[]>;
|
||||
|
||||
constructor(private villainesService: VillainsService) {
|
||||
this.villaines = villainesService.getVillains();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { of } from 'rxjs/observable/of';
|
||||
|
||||
export interface Villain { id: number; name: string; }
|
||||
|
||||
@Injectable()
|
||||
export class VillainsService {
|
||||
villains: Villain[] = [
|
||||
{ id: 1, name: 'Dr. Evil'},
|
||||
{ id: 2, name: 'Moriarty'}
|
||||
];
|
||||
|
||||
getVillains() {
|
||||
return of(this.villains);
|
||||
}
|
||||
}
|
|
@ -19,9 +19,7 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<!-- #docregion heroes-list -->
|
||||
<heroes-list>loading...</heroes-list>
|
||||
<!-- #enddocregion heroes-list -->
|
||||
<my-app>loading...</my-app>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"description": "Hierachical Injectors",
|
||||
"files":["!**/*.d.ts", "!**/*.js"],
|
||||
"description": "Hierachical Dependency Injection",
|
||||
"files":[
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js"
|
||||
],
|
||||
"tags": ["dependency", "injection"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,18 @@ block includes
|
|||
The Angular documentation is a living document with continuous improvements.
|
||||
This log calls attention to recent significant changes.
|
||||
|
||||
## Hierarchical Dependency Injection: refreshed (2017-01-13)
|
||||
[Hierarchical Dependency Injection](hierarchical-dependency-injection.html) guide significantly revised.
|
||||
Closes issue #3086
|
||||
Revised samples are more clear and cover all topics discussed
|
||||
|
||||
## Miscellaneous (2017-01-05)
|
||||
* [Setup](setup.html) guide:
|
||||
added (optional) instructions on how to remove _non-essential_ files.
|
||||
* No longer consolidate RxJS operator imports in `rxjs-extensions` file; each file should import what it needs.
|
||||
* All samples prepend template/style URLS URLs w/ `./` ... and so should you.
|
||||
* [Style Guide](style-guide.html): copy edits and revised rules.
|
||||
|
||||
## Router: more detail (2016-12-21)
|
||||
Added more information to the [Router](router.html) guide
|
||||
including sections named outlets, wildcard routes, and preload strategies.
|
||||
|
|
|
@ -2,170 +2,200 @@ block includes
|
|||
include ../_util-fns
|
||||
|
||||
:marked
|
||||
We learned the basics of Angular Dependency injection in the
|
||||
[Dependency Injection](./dependency-injection.html) chapter.
|
||||
You learned the basics of Angular Dependency injection in the
|
||||
[Dependency Injection](./dependency-injection.html) guide.
|
||||
|
||||
Angular has a Hierarchical Dependency Injection system.
|
||||
There is actually a tree of injectors
|
||||
that parallel an application's component tree.
|
||||
We can reconfigure the injectors at any level of that component tree with
|
||||
interesting and useful results.
|
||||
Angular has a _Hierarchical Dependency Injection_ system.
|
||||
There is actually a tree of injectors that parallel an application's component tree.
|
||||
You can reconfigure the injectors at any level of that component tree.
|
||||
|
||||
In this chapter we explore these points and write some code.
|
||||
This guide explores this system and how to use it to your advantage.
|
||||
|
||||
Try the <live-example></live-example>.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## The Injector Tree
|
||||
## The injector tree
|
||||
|
||||
In the [Dependency Injection](./dependency-injection.html) chapter
|
||||
we learned how to configure a dependency injector and how to retrieve dependencies where we need them.
|
||||
|
||||
We oversimplified. In fact, there is no such thing as ***the*** injector!
|
||||
An application may have multiple injectors!
|
||||
In the [Dependency Injection](./dependency-injection.html) guide,
|
||||
you learned how to configure a dependency injector and how to retrieve dependencies where you need them.
|
||||
|
||||
In fact, there is no such thing as *the* injector.
|
||||
An application may have multiple injectors.
|
||||
An Angular application is a tree of components. Each component instance has its own injector!
|
||||
The tree of components parallels the tree of injectors.
|
||||
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Angular doesn't *literally* create a separate injector for each component.
|
||||
Every component doesn't need its own injector and it would be horribly inefficient to create
|
||||
masses of injectors for no good purpose.
|
||||
|
||||
But it is true that every component ***has an injector*** (even if it shares that injector with another component)
|
||||
and there may be many different injector instances operating at different levels of the component tree.
|
||||
|
||||
It is useful to pretend that every component has its own injector.
|
||||
:marked
|
||||
Consider a simple variation on the Tour of Heroes application consisting of three different components:
|
||||
`HeroesApp`, `HeroesListComponent` and `HeroesCardComponent`.
|
||||
The `HeroesApp` holds a single instance of `HeroesListComponent`.
|
||||
The new twist is that the `HeroesListComponent` may hold and manage multiple instances of the `HeroesCardComponent`.
|
||||
|
||||
The following diagram represents the state of the component tree when there are three instances of `HeroesCardComponent`
|
||||
Consider this guide's variation on the Tour of Heroes application.
|
||||
At the top is the `AppComponent` which has some sub-components.
|
||||
One of them is the `HeroesListComponent`.
|
||||
The `HeroesListComponent` holds and manages multiple instances of the `HeroTaxReturnComponent`.
|
||||
The following diagram represents the state of the this guide's three-level component tree when there are three instances of `HeroTaxReturnComponent`
|
||||
open simultaneously.
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/dependency-injection/component-hierarchy.png" alt="injector tree" width="500")
|
||||
img(src="/resources/images/devguide/dependency-injection/component-hierarchy.png" alt="injector tree" width="600")
|
||||
|
||||
:marked
|
||||
Each component instance gets its own injector and an injector at one level is a child injector of the injector above it in the tree.
|
||||
Angular doesn't actually _create_ a separate injector for each component.
|
||||
Every component doesn't need its own injector and it would be horribly inefficient to create
|
||||
masses of injectors for no good purpose.
|
||||
|
||||
When a component at the bottom requests a dependency, Angular tries to satisfy that dependency with a provider registered in that component's own injector.
|
||||
But every component _has an injector_, even if it shares that injector with another component or with the injector of the root `AppModule`.
|
||||
And there _may_ be multiple injector instances operating at different levels of the component tree
|
||||
depending upon how the developer registers providers, which is the subject of this guide.
|
||||
|
||||
### Injector bubbling
|
||||
|
||||
When a component requests a dependency, Angular tries to satisfy that dependency with a provider registered in that component's own injector.
|
||||
If the component's injector lacks the provider, it passes the request up to its parent component's injector.
|
||||
If that injector can't satisfy the request, it passes it along to *its* parent component's injector.
|
||||
The requests keep bubbling up until we find an injector that can handle the request or run out of component ancestors.
|
||||
If we run out of ancestors, Angular throws an error.
|
||||
If that injector can't satisfy the request, it passes it along to *its* parent injector.
|
||||
The requests keep bubbling up until Angular finds an injector that can handle the request or runs out of ancestor injectors.
|
||||
If it runs out of ancestors, Angular throws an error.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
There's a third possibility. An intermediate component can declare that it is the "host" component.
|
||||
The hunt for providers will climb no higher than the injector for this host component.
|
||||
We'll reserve discussion of this option for another day.
|
||||
:marked
|
||||
Such a proliferation of injectors makes little sense until we consider the possibility that injectors at different levels can be
|
||||
configured with different providers. We don't *have* to reconfigure providers at every level. But we *can*.
|
||||
You can cap the bubbling. An intermediate component can declare that it is the "host" component.
|
||||
The hunt for providers will climb no higher than the injector for that host component.
|
||||
This a topic for another day.
|
||||
|
||||
If we don't reconfigure, the tree of injectors appears to be flat. All requests bubble up to the root
|
||||
<span if-docs="ts">NgModule</span> injector that we configured with the `!{_bootstrapModule}` method.
|
||||
:marked
|
||||
### Re-providing a service at different levels
|
||||
You can re-register a provider for a particular dependency token at multiple levels of the injector tree.
|
||||
You don't *have* to re-register providers. You shouldn't do so unless you have a good reason.
|
||||
But you *can*.
|
||||
|
||||
As the resolution logic works upwards, the first provider encountered wins.
|
||||
Thus, a provider in an intermediate injector intercepts a request for a service from something lower in the tree.
|
||||
It effectively "reconfigures" and "shadows" a provider at a higher level in the tree.
|
||||
|
||||
If you only specify providers at the top level (typically the root `AppModule`), the tree of injectors appears to be flat.
|
||||
All requests bubble up to the root <span if-docs="ts"><code>NgModule</code></span> injector that you configured with the `!{_bootstrapModule}` method.
|
||||
|
||||
.l-main-section
|
||||
:marked
|
||||
## Component injectors
|
||||
|
||||
The ability to configure one or more providers at different levels opens up interesting and useful possibilities.
|
||||
|
||||
Let’s return to our Car example.
|
||||
Suppose we configured the root injector (marked as A) with providers for `Car`, `Engine` and `Tires`.
|
||||
We create a child component (B) that defines its own providers for `Car` and `Engine`
|
||||
This child is the parent of another component (C) that defines its own provider for `Car`.
|
||||
:marked
|
||||
### Scenario: service isolation
|
||||
|
||||
Behind the scenes each component sets up its own injector with one or more providers defined for that component itself.
|
||||
Architectural reasons may lead you to restrict access to a service to the application domain where it belongs.
|
||||
|
||||
When we resolve an instance of `Car` at the deepest component (C),
|
||||
The guide sample includes a `VillainsListComponent` that displays a list of villains.
|
||||
It gets those villains from a `VillainsService`.
|
||||
|
||||
While you could provide `VillainsService` in the root `AppModule` (that's where you'll find the `HeroesService`),
|
||||
that would make the `VillainsService` available everywhere in the application, including the _Hero_ workflows.
|
||||
|
||||
If you later modify the `VillainsService`, you could break something in a hero component somewhere.
|
||||
That's not supposed to happen but the way you've provided the service creates that risk.
|
||||
|
||||
Instead, provide the `VillainsService` in the `providers` metadata of the `VillainsListComponent` like this:
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/villains-list.component.ts', 'metadata','app/villains-list.component.ts (metadata)')(format='.')
|
||||
:marked
|
||||
By providing `VillainsService` in the `VillainsListComponent` metadata — and nowhere else —,
|
||||
the service becomes available only in the `VillainsListComponent` and its sub-component tree.
|
||||
It's still a singleton, but it's a singleton that exist solely in the _villain_ domain.
|
||||
|
||||
You are confident that a hero component can't access it. You've reduced your exposure to error.
|
||||
|
||||
### Scenario: multiple edit sessions
|
||||
|
||||
Many applications allow users to work on several open tasks at the same time.
|
||||
For example, in a tax preparation application, the preparer could be working several tax returns,
|
||||
switching from one to the other throughout the day.
|
||||
|
||||
This guide demonstrates that scenario with an example in the Tour of Heroes theme.
|
||||
Imagine an outer `HeroListComponent` that displays a list of super heroes.
|
||||
|
||||
To open a hero's tax return, the preparer clicks on a hero name, which opens a component for editing that return.
|
||||
Each selected hero tax return opens in its own component and multiple returns can be open at the same time.
|
||||
|
||||
Each tax return component
|
||||
* is its own tax return editing session.
|
||||
* can change a tax return without affecting a return in another component.
|
||||
* has the ability to save the changes to its tax return or cancel them.
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/dependency-injection/hid-heroes-anim.gif" width="400" alt="Heroes in action")
|
||||
:marked
|
||||
One might suppose that the `TaxReturnComponent` has logic to manage and restore changes.
|
||||
That would be a pretty easy task for a simple hero tax return.
|
||||
In the real world, with a rich tax return data model, the change management would be tricky.
|
||||
You might delegate that management to a helper service, as this example does.
|
||||
|
||||
Here is the `HeroTaxReturnService`.
|
||||
It caches a single `HeroTaxReturn`, tracks changes to that return, and can save or restore it.
|
||||
It also delegates to the application-wide, singleton `HeroService`, which it gets by injection.
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero-tax-return.service.ts', '', 'app/hero-tax-return.service.ts')
|
||||
|
||||
:marked
|
||||
Here is the `HeroTaxReturnComponent` that makes use of it.
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero-tax-return.component.ts', null, 'app/hero-tax-return.component.ts')
|
||||
|
||||
:marked
|
||||
The _tax-return-to-edit_ arrives via the input property which is implemented with getters and setters.
|
||||
The setter initializes the component's own instance of the `HeroTaxReturnService` with the incoming return.
|
||||
The getter always returns what that service says is the current state of the hero.
|
||||
The component also asks the service to save and restore this tax return.
|
||||
|
||||
There'd be big trouble if _this_ service were an application-wide singleton.
|
||||
Every component would share the same service instance.
|
||||
Each component would overwrite the tax return that belonged to another hero.
|
||||
What a mess!
|
||||
|
||||
Look closely at the metadata for the `HeroTaxReturnComponent`. Notice the `providers` property.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero-tax-return.component.ts', 'providers')
|
||||
:marked
|
||||
The `HeroTaxReturnComponent` has its own provider of the `HeroTaxReturnService`.
|
||||
Recall that every component _instance_ has its own injector.
|
||||
Providing the service at the component level ensures that _every_ instance of the component gets its own, private instance of the service.
|
||||
No tax return overwriting. No mess.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
The rest of the scenario code relies on other Angular features and techniques that you can learn about elsewhere in the documentation.
|
||||
You can review it and download it from the <live-example></live-example>
|
||||
|
||||
:marked
|
||||
### Scenario: specialized providers
|
||||
|
||||
Another reason to re-provide a service is to substitute a _more specialized_ implementation of that service,
|
||||
deeper in the component tree.
|
||||
|
||||
Consider again the Car example from the [Dependency Injection](./dependency-injection.html) guide.
|
||||
Suppose you configured the root injector (marked as A) with _generic_ providers for
|
||||
`CarService`, `EngineService` and `TiresService`.
|
||||
|
||||
You create a car component (A) that displays a car constructed from these three generic services.
|
||||
|
||||
Then you create a child component (B) that defines its own, _specialized_ providers for `CarService` and `EngineService`
|
||||
that have special capabilites suitable for whatever is going on in component (B).
|
||||
|
||||
Component (B) is the parent of another component (C) that defines its own, even _more specialized_ provider for `CarService`.
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/dependency-injection/car-components.png" alt="car components" width="220")
|
||||
|
||||
:marked
|
||||
Behind the scenes, each component sets up its own injector with zero, one, or more providers defined for that component itself.
|
||||
|
||||
When you resolve an instance of `Car` at the deepest component (C),
|
||||
its injector produces an instance of `Car` resolved by injector (C) with an `Engine` resolved by injector (B) and
|
||||
`Tires` resolved by the root injector (A).
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/devguide/dependency-injection/injector-tree.png" alt="injector tree" width="600")
|
||||
img(src="/resources/images/devguide/dependency-injection/injector-tree.png" alt="car injector tree" width="600")
|
||||
|
||||
.l-main-section
|
||||
.l-sub-section
|
||||
:marked
|
||||
The code for this _cars_ scenario is in the `car.components.ts` and `car.services.ts` files of the sample
|
||||
which you can review and download from the <live-example></live-example>
|
||||
:marked
|
||||
## Component Injectors
|
||||
|
||||
In the previous section, we talked about injectors and how they are organized like a tree. Lookups follow the injector tree upwards until they find the requested thing to inject. But when do we actually want to provide providers on the root injector and when do we want to provide them on a child injector?
|
||||
|
||||
Consider you are building a component to show a list of super heroes that displays each super hero in a card with its name and superpower. There should also be an edit button that opens up an editor to change the name and superpower of our hero.
|
||||
|
||||
One important aspect of the editing functionality is that we want to allow multiple heroes to be in edit mode at the same time and that one can always either commit or cancel the proposed changes.
|
||||
|
||||
Let’s take a look at the `HeroesListComponent` which is the root component for this example.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/heroes-list.component.ts', null, 'app/heroes-list.component.ts')
|
||||
|
||||
:marked
|
||||
Notice that it imports the `HeroService` that we’ve used before so we can skip its declaration. The only difference is that we’ve used a more formal approach for our `Hero`model and defined it upfront as such.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero.ts', null, 'app/hero.ts')(format=".")
|
||||
|
||||
:marked
|
||||
Our `HeroesListComponent` defines a template that creates a list of `HeroCardComponent`s and `HeroEditorComponent`s, each bound to an instance of hero that is returned from the `HeroService`. Ok, that’s not entirely true. It actually binds to an `EditItem<Hero>` which is a simple generic datatype that can wrap any type and indicate if the item being wrapped is currently being edited or not.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/edit-item.ts', null, 'app/edit-item.ts')(format=".")
|
||||
|
||||
:marked
|
||||
But how is `HeroCardComponent` implemented? Let’s take a look.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero-card.component.ts', null, 'app/hero-card.component.ts')
|
||||
|
||||
:marked
|
||||
The `HeroCardComponent` is basically a component that defines a template to render a hero. Nothing more.
|
||||
|
||||
Let’s get to the interesting part and take a look at the `HeroEditorComponent`
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero-editor.component.ts', null, 'app/hero-editor.component.ts')
|
||||
|
||||
:marked
|
||||
Now here it’s getting interesting. The `HeroEditorComponent`defines a template with an input to change the name of the hero and a `cancel` and a `save` button. Remember that we said we want to have the flexibility to cancel our editing and restore the old value? This means we need to maintain two copies of our `Hero` that we want to edit. Thinking ahead, this is a perfect use case to abstract it into its own generic service since we have probably more cases like this in our app.
|
||||
|
||||
And this is where the `RestoreService` enters the stage.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/restore.service.ts', null, 'app/restore.service.ts')
|
||||
|
||||
:marked
|
||||
All this tiny service does is define an API to set a value of any type which can be altered, retrieved or set back to its initial value. That’s exactly what we need to implement the desired functionality.
|
||||
|
||||
Our `HeroEditComponent` uses this services under the hood for its `hero` property. It intercepts the `get` and `set` method to delegate the actual work to our `RestoreService` which in turn makes sure that we won’t work on the original item but on a copy instead.
|
||||
|
||||
At this point we may be scratching our heads asking what this has to do with component injectors?
|
||||
Look closely at the metadata for our `HeroEditComponent`. Notice the `providers` property.
|
||||
|
||||
+makeExample('hierarchical-dependency-injection/ts/app/hero-editor.component.ts', 'providers')
|
||||
|
||||
- var _root_NgModule = _docsFor == 'dart' ? '<code>bootstrap</code> arguments' : 'root <code>NgModule</code>'
|
||||
:marked
|
||||
This adds a `RestoreService` provider to the injector of the `HeroEditComponent`.
|
||||
Couldn’t we simply alter our !{_root_NgModule} to include this provider?
|
||||
|
||||
+makeExcerpt(_appModuleTsVsMainTs, 'bad-alternative')
|
||||
|
||||
:marked
|
||||
Technically we could, but our component wouldn’t quite behave the way it is supposed to. Remember that each injector treats the services that it provides as singletons. However, in order to be able to have multiple instances of `HeroEditComponent` edit multiple heroes at the same time we need to have multiple instances of the `RestoreService`. More specifically, each instance of `HeroEditComponent` needs to be bound to its own instance of the `RestoreService`.
|
||||
|
||||
By configuring a provider for the `RestoreService` on the `HeroEditComponent`, we get exactly one new instance of the `RestoreService`per `HeroEditComponent`.
|
||||
|
||||
Does that mean that services aren’t singletons anymore in Angular? Yes and no.
|
||||
There can be only one instance of a service type in a particular injector.
|
||||
But we've learned that we can have multiple injectors operating at different levels of the application's component tree.
|
||||
Any of those injectors could have its own instance of the service.
|
||||
|
||||
If we defined a `RestoreService` provider only on the root component,
|
||||
we would have exactly one instance of that service and it would be shared across the entire application.
|
||||
|
||||
That’s clearly not what we want in this scenario. We want each component to have its own instance of the `RestoreService`.
|
||||
Defining (or redefining) a provider at the component level creates a new instance of the service for each new instance
|
||||
of that component. We've made the `RestoreService` a kind of "private" singleton for each `HeroEditComponent`,
|
||||
scoped to that component instance and its child components.
|
||||
|
||||
//- ## Advanced Dependency Injection in Angular
|
||||
//- Restrict Dependency Lookups
|
||||
//- [TODO] (@Host) This has been postponed for now until we come up with a decent use case
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
After Width: | Height: | Size: 127 KiB |
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
Loading…
Reference in New Issue