docs(cookbook/component-communication): first of the cookbook series
closes #824
|
@ -0,0 +1,210 @@
|
|||
describe('Component Communication Cookbook Tests', function () {
|
||||
|
||||
|
||||
beforeAll(function () {
|
||||
browser.get('');
|
||||
});
|
||||
|
||||
describe('Parent-to-child communication', function() {
|
||||
// #docregion parent-to-child
|
||||
// ...
|
||||
var _heroNames = ['Mr. IQ', 'Magneta', 'Bombasto'];
|
||||
var _masterName = 'Master';
|
||||
|
||||
it('should pass properties to children properly', function () {
|
||||
var parent = element.all(by.tagName('hero-parent')).get(0);
|
||||
var heroes = parent.all(by.tagName('hero-child'));
|
||||
|
||||
for (var i = 0; i < _heroNames.length; i++) {
|
||||
var childTitle = heroes.get(i).element(by.tagName('h3')).getText();
|
||||
var childDetail = heroes.get(i).element(by.tagName('p')).getText();
|
||||
expect(childTitle).toEqual(_heroNames[i] + ' says:')
|
||||
expect(childDetail).toContain(_masterName)
|
||||
}
|
||||
});
|
||||
// ...
|
||||
// #enddocregion parent-to-child
|
||||
});
|
||||
|
||||
describe('Parent-to-child communication with setter', function() {
|
||||
// #docregion parent-to-child-setter
|
||||
// ...
|
||||
it('should display trimmed, non-empty names', function () {
|
||||
var _nonEmptyNameIndex = 0;
|
||||
var _nonEmptyName = '"Mr. IQ"';
|
||||
var parent = element.all(by.tagName('name-parent')).get(0);
|
||||
var hero = parent.all(by.tagName('name-child')).get(_nonEmptyNameIndex);
|
||||
|
||||
var displayName = hero.element(by.tagName('h3')).getText();
|
||||
expect(displayName).toEqual(_nonEmptyName)
|
||||
});
|
||||
|
||||
it('should replace empty name with default name', function () {
|
||||
var _emptyNameIndex = 1;
|
||||
var _defaultName = '"<no name set>"';
|
||||
var parent = element.all(by.tagName('name-parent')).get(0);
|
||||
var hero = parent.all(by.tagName('name-child')).get(_emptyNameIndex);
|
||||
|
||||
var displayName = hero.element(by.tagName('h3')).getText();
|
||||
expect(displayName).toEqual(_defaultName)
|
||||
});
|
||||
// ...
|
||||
// #enddocregion parent-to-child-setter
|
||||
});
|
||||
|
||||
describe('Parent-to-child communication with ngOnChanges', function() {
|
||||
// #docregion parent-to-child-onchanges
|
||||
// ...
|
||||
// Test must all execute in this exact order
|
||||
it('should set expected initial values', function () {
|
||||
var actual = getActual();
|
||||
|
||||
var initialLabel = "Version 1.23";
|
||||
var initialLog = 'major changed from {} to 1, minor changed from {} to 23';
|
||||
|
||||
expect(actual.label).toBe(initialLabel);
|
||||
expect(actual.count).toBe(1);
|
||||
expect(actual.logs.get(0).getText()).toBe(initialLog);
|
||||
});
|
||||
|
||||
it('should set expected values after clicking "Minor" twice', function () {
|
||||
var repoTag = element(by.tagName('version-parent'));
|
||||
var newMinorButton = repoTag.all(by.tagName('button')).get(0);
|
||||
|
||||
newMinorButton.click().then(function() {
|
||||
newMinorButton.click().then(function() {
|
||||
var actual = getActual();
|
||||
|
||||
var labelAfter2Minor = "Version 1.25";
|
||||
var logAfter2Minor = 'minor changed from 24 to 25';
|
||||
|
||||
expect(actual.label).toBe(labelAfter2Minor);
|
||||
expect(actual.count).toBe(3);
|
||||
expect(actual.logs.get(2).getText()).toBe(logAfter2Minor);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
it('should set expected values after clicking "Major" once', function () {
|
||||
var repoTag = element(by.tagName('version-parent'));
|
||||
var newMajorButton = repoTag.all(by.tagName('button')).get(1);
|
||||
|
||||
newMajorButton.click().then(function() {
|
||||
var actual = getActual();
|
||||
|
||||
var labelAfterMajor = "Version 2.0";
|
||||
var logAfterMajor = 'major changed from 1 to 2, minor changed from 25 to 0';
|
||||
|
||||
expect(actual.label).toBe(labelAfterMajor);
|
||||
expect(actual.count).toBe(4);
|
||||
expect(actual.logs.get(3).getText()).toBe(logAfterMajor);
|
||||
});
|
||||
});
|
||||
|
||||
function getActual() {
|
||||
var versionTag = element(by.tagName('version-child'));
|
||||
var label = versionTag.element(by.tagName('h3')).getText();
|
||||
var ul = versionTag.element((by.tagName('ul')));
|
||||
var logs = ul.all(by.tagName('li'));
|
||||
|
||||
return {
|
||||
label: label,
|
||||
logs: logs,
|
||||
count: logs.count()
|
||||
};
|
||||
}
|
||||
// ...
|
||||
// #enddocregion parent-to-child-onchanges
|
||||
|
||||
});
|
||||
|
||||
describe('Child-to-parent communication', function() {
|
||||
// #docregion child-to-parent
|
||||
// ...
|
||||
it('should not emit the event initially', function () {
|
||||
var voteLabel = element(by.tagName('vote-taker'))
|
||||
.element(by.tagName('h3')).getText();
|
||||
expect(voteLabel).toBe("Agree: 0, Disagree: 0");
|
||||
});
|
||||
|
||||
it('should process Agree vote', function () {
|
||||
var agreeButton1 = element.all(by.tagName('my-voter')).get(0)
|
||||
.all(by.tagName('button')).get(0);
|
||||
agreeButton1.click().then(function() {
|
||||
var voteLabel = element(by.tagName('vote-taker'))
|
||||
.element(by.tagName('h3')).getText();
|
||||
expect(voteLabel).toBe("Agree: 1, Disagree: 0");
|
||||
});
|
||||
});
|
||||
|
||||
it('should process Disagree vote', function () {
|
||||
var agreeButton1 = element.all(by.tagName('my-voter')).get(1)
|
||||
.all(by.tagName('button')).get(1);
|
||||
agreeButton1.click().then(function() {
|
||||
var voteLabel = element(by.tagName('vote-taker'))
|
||||
.element(by.tagName('h3')).getText();
|
||||
expect(voteLabel).toBe("Agree: 1, Disagree: 1");
|
||||
});
|
||||
});
|
||||
// ...
|
||||
// #enddocregion child-to-parent
|
||||
});
|
||||
|
||||
describe('Parent calls ViewChild', function() {
|
||||
// #docregion parent-to-view-child
|
||||
// ...
|
||||
it('should stop the countdown', function () {
|
||||
var stopButton = element
|
||||
.all(by.tagName('countdown-parent')).get(0)
|
||||
.all(by.tagName('button')).get(1);
|
||||
|
||||
stopButton.click().then(function() {
|
||||
var message = element(by.tagName('countdown-timer'))
|
||||
.element(by.tagName('p')).getText();
|
||||
expect(message).toContain('Holding');
|
||||
});
|
||||
});
|
||||
// ...
|
||||
// #enddocregion parent-to-view-child
|
||||
});
|
||||
|
||||
describe('Parent and children communicate via a service', function() {
|
||||
// #docregion bidirectional-service
|
||||
// ...
|
||||
it('should announce a mission', function () {
|
||||
var missionControl = element(by.tagName('mission-control'));
|
||||
var announceButton = missionControl.all(by.tagName('button')).get(0);
|
||||
announceButton.click().then(function () {
|
||||
var history = missionControl.all(by.tagName('li'));
|
||||
expect(history.count()).toBe(1);
|
||||
expect(history.get(0).getText()).toMatch(/Mission.* announced/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should confirm the mission by Lovell', function () {
|
||||
testConfirmMission(1, 2, 'Lovell');
|
||||
});
|
||||
|
||||
it('should confirm the mission by Haise', function () {
|
||||
testConfirmMission(3, 3, 'Haise');
|
||||
});
|
||||
|
||||
it('should confirm the mission by Swigert', function () {
|
||||
testConfirmMission(2, 4, 'Swigert');
|
||||
});
|
||||
|
||||
function testConfirmMission(buttonIndex, expectedLogCount, astronaut) {
|
||||
var _confirmedLog = ' confirmed the mission';
|
||||
var missionControl = element(by.tagName('mission-control'));
|
||||
var confirmButton = missionControl.all(by.tagName('button')).get(buttonIndex);
|
||||
confirmButton.click().then(function () {
|
||||
var history = missionControl.all(by.tagName('li'));
|
||||
expect(history.count()).toBe(expectedLogCount);
|
||||
expect(history.get(expectedLogCount-1).getText()).toBe(astronaut + _confirmedLog);
|
||||
});
|
||||
}
|
||||
// ...
|
||||
// #enddocregion bidirectional-service
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
**/*.js
|
|
@ -0,0 +1,44 @@
|
|||
<h1 id="top">Component Communication Cookbook</h1>
|
||||
|
||||
<a href="#parent-to-child">Pass data from parent to child with input binding</a><br/>
|
||||
<a href="#parent-to-child-setter">Intercept input property changes with a setter</a><br/>
|
||||
<a href="#parent-to-child-on-changes">Intercept input property changes with <i>ngOnChanges</i></a><br/>
|
||||
<a href="#child-to-parent">Parent listens for child event</a><br/>
|
||||
<a href="#parent-to-view-child">Parent calls <i>ViewChild</i></a><br/>
|
||||
<a href="#bidirectional-service">Parent and children communicate via a service</a><br/>
|
||||
|
||||
<div id="parent-to-child">
|
||||
<hero-parent></hero-parent>
|
||||
</div>
|
||||
<a href="#top" class="to-top">Back to Top</a>
|
||||
|
||||
<hr>
|
||||
<div id="parent-to-child-setter">
|
||||
<name-parent></name-parent>
|
||||
</div>
|
||||
<a href="#top" class="to-top">Back to Top</a>
|
||||
|
||||
<hr>
|
||||
<div id="parent-to-child-on-changes">
|
||||
<version-parent></version-parent>
|
||||
</div>
|
||||
<a href="#top" class="to-top">Back to Top</a>
|
||||
|
||||
<hr>
|
||||
<div id="child-to-parent">
|
||||
<vote-taker></vote-taker>
|
||||
</div>
|
||||
<a href="#top" class="to-top">Back to Top</a>
|
||||
<hr>
|
||||
|
||||
<div id="parent-to-view-child">
|
||||
<countdown-parent></countdown-parent>
|
||||
</div>
|
||||
<a href="#top" class="to-top">Back to Top</a>
|
||||
<hr>
|
||||
|
||||
<div id="bidirectional-service">
|
||||
<mission-control></mission-control>
|
||||
</div>
|
||||
<a href="#top" class="to-top">Back to Top</a>
|
||||
<hr>
|
|
@ -0,0 +1,21 @@
|
|||
import {Component} from 'angular2/core';
|
||||
import {HeroParentComponent} from './hero-parent.component';
|
||||
import {NameParentComponent} from './name-parent.component';
|
||||
import {VersionParentComponent} from './version-parent.component';
|
||||
import {VoteTakerComponent} from './votetaker.component';
|
||||
import {CountdownParentComponent} from './countdown-parent.component';
|
||||
import {MissionControlComponent} from './missioncontrol.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
templateUrl: 'app/app.component.html',
|
||||
directives: [
|
||||
HeroParentComponent,
|
||||
NameParentComponent,
|
||||
VersionParentComponent,
|
||||
VoteTakerComponent,
|
||||
CountdownParentComponent,
|
||||
MissionControlComponent
|
||||
]
|
||||
})
|
||||
export class AppComponent { }
|
|
@ -0,0 +1,45 @@
|
|||
// #docregion
|
||||
import {Component, Input, OnDestroy} from 'angular2/core';
|
||||
import {MissionService} from './mission.service';
|
||||
import {Subscription} from 'rxjs/Subscription';
|
||||
|
||||
@Component({
|
||||
selector: 'my-astronaut',
|
||||
template: `
|
||||
<p>
|
||||
{{astronaut}}: <strong>{{mission}}</strong>
|
||||
<button
|
||||
(click)="confirm()"
|
||||
[disabled]="!announced || confirmed">
|
||||
Confirm
|
||||
</button>
|
||||
</p>
|
||||
`
|
||||
})
|
||||
export class AstronautComponent implements OnDestroy{
|
||||
@Input() astronaut: string;
|
||||
mission = "<no mission announced>";
|
||||
confirmed = false;
|
||||
announced = false;
|
||||
subscription:Subscription;
|
||||
|
||||
constructor(private missionService: MissionService) {
|
||||
this.subscription = missionService.missionAnnounced$.subscribe(
|
||||
mission => {
|
||||
this.mission = mission;
|
||||
this.announced = true;
|
||||
this.confirmed = false;
|
||||
})
|
||||
}
|
||||
|
||||
confirm() {
|
||||
this.confirmed = true;
|
||||
this.missionService.confirmMission(this.astronaut);
|
||||
}
|
||||
|
||||
ngOnDestroy(){
|
||||
// prevent memory leak when component destroyed
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,22 @@
|
|||
// #docregion
|
||||
import {Component, ViewChild} from 'angular2/core';
|
||||
import {CountdownTimerComponent} from './countdown-timer.component';
|
||||
|
||||
@Component({
|
||||
selector:'countdown-parent',
|
||||
template: `
|
||||
<h3>Countdown to Liftoff</h3>
|
||||
<button (click)="start()">Start</button>
|
||||
<button (click)="stop()">Stop</button>
|
||||
<countdown-timer></countdown-timer>
|
||||
`,
|
||||
directives: [CountdownTimerComponent]
|
||||
})
|
||||
export class CountdownParentComponent {
|
||||
|
||||
@ViewChild(CountdownTimerComponent)
|
||||
private _timerComponent:CountdownTimerComponent;
|
||||
|
||||
start(){ this._timerComponent.start(); }
|
||||
stop() { this._timerComponent.stop(); }
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// #docregion
|
||||
import {Component, EventEmitter, OnInit, Output} from 'angular2/core';
|
||||
|
||||
@Component({
|
||||
selector:'countdown-timer',
|
||||
template: '<p>{{message}}</p>'
|
||||
})
|
||||
export class CountdownTimerComponent implements OnInit {
|
||||
|
||||
intervalId = 0;
|
||||
message = '';
|
||||
seconds = 11;
|
||||
|
||||
private _countDown() {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = setInterval(()=>{
|
||||
this.seconds -= 1;
|
||||
if (this.seconds == 0) {
|
||||
this.message = "Blast off!";
|
||||
this.seconds = 11; // reset
|
||||
} else {
|
||||
this.message = `T-${this.seconds} seconds and counting`;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
ngOnInit() { this.start(); }
|
||||
start() { this._countDown(); }
|
||||
stop() {
|
||||
clearInterval(this.intervalId);
|
||||
this.message = `Holding at T-${this.seconds} seconds`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// #docregion
|
||||
import {Component, Input} from 'angular2/core';
|
||||
import {Hero} from './hero';
|
||||
|
||||
@Component({
|
||||
selector: 'hero-child',
|
||||
template: `
|
||||
<h3>{{hero.name}} says:</h3>
|
||||
<p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
|
||||
`
|
||||
})
|
||||
export class HeroChildComponent {
|
||||
@Input() hero: Hero;
|
||||
@Input('master') masterName: string;
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,21 @@
|
|||
// #docregion
|
||||
import {Component} from 'angular2/core';
|
||||
import {HeroChildComponent} from './hero-child.component';
|
||||
import {HEROES} from './hero';
|
||||
|
||||
@Component({
|
||||
selector: 'hero-parent',
|
||||
template: `
|
||||
<h2>{{master}} controls {{heroes.length}} heroes</h2>
|
||||
<hero-child *ngFor="#hero of heroes"
|
||||
[hero]="hero"
|
||||
[master]="master">
|
||||
</hero-child>
|
||||
`,
|
||||
directives: [HeroChildComponent]
|
||||
})
|
||||
export class HeroParentComponent {
|
||||
heroes = HEROES;
|
||||
master: string = 'Master';
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,9 @@
|
|||
export interface Hero {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const HEROES = [
|
||||
{name: 'Mr. IQ'},
|
||||
{name: 'Magneta'},
|
||||
{name: 'Bombasto'}
|
||||
];
|
|
@ -0,0 +1,4 @@
|
|||
import {bootstrap} from 'angular2/platform/browser';
|
||||
import {AppComponent} from './app.component';
|
||||
|
||||
bootstrap(AppComponent);
|
|
@ -0,0 +1,25 @@
|
|||
// #docregion
|
||||
import {Injectable} from 'angular2/core'
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
@Injectable()
|
||||
export class MissionService {
|
||||
|
||||
// Observable string sources
|
||||
private _missionAnnouncedSource = new Subject<string>();
|
||||
private _missionConfirmedSource = new Subject<string>();
|
||||
|
||||
// Observable string streams
|
||||
missionAnnounced$ = this._missionAnnouncedSource.asObservable();
|
||||
missionConfirmed$ = this._missionConfirmedSource.asObservable();
|
||||
|
||||
// Service message commands
|
||||
announceMission(mission: string) {
|
||||
this._missionAnnouncedSource.next(mission)
|
||||
}
|
||||
|
||||
confirmMission(astronaut: string) {
|
||||
this._missionConfirmedSource.next(astronaut);
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,44 @@
|
|||
// #docregion
|
||||
import {Component} from 'angular2/core';
|
||||
import {AstronautComponent} from './astronaut.component';
|
||||
import {MissionService} from './mission.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mission-control',
|
||||
template: `
|
||||
<h2>Mission Control</h2>
|
||||
<button (click)="announce()">Announce mission</button>
|
||||
<my-astronaut *ngFor="#astronaut of astronauts"
|
||||
[astronaut]="astronaut">
|
||||
</my-astronaut>
|
||||
<h2>History</h2>
|
||||
<ul>
|
||||
<li *ngFor="#event of history">{{event}}</li>
|
||||
</ul>
|
||||
`,
|
||||
directives: [AstronautComponent],
|
||||
providers: [MissionService]
|
||||
})
|
||||
export class MissionControlComponent {
|
||||
astronauts = ['Lovell', 'Swigert', 'Haise']
|
||||
history: string[] = [];
|
||||
missions = ['Fly to the moon!',
|
||||
'Fly to mars!',
|
||||
'Fly to Vegas!'];
|
||||
nextMission = 0;
|
||||
|
||||
constructor(private missionService: MissionService) {
|
||||
missionService.missionConfirmed$.subscribe(
|
||||
astronaut => {
|
||||
this.history.push(`${astronaut} confirmed the mission`);
|
||||
})
|
||||
}
|
||||
|
||||
announce() {
|
||||
let mission = this.missions[this.nextMission++];
|
||||
this.missionService.announceMission(mission);
|
||||
this.history.push(`Mission "${mission}" announced`);
|
||||
if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,20 @@
|
|||
// #docregion
|
||||
import {Component, Input} from 'angular2/core';
|
||||
|
||||
@Component({
|
||||
selector: 'name-child',
|
||||
template: `
|
||||
<h3>"{{name}}"</h3>
|
||||
`
|
||||
})
|
||||
export class NameChildComponent {
|
||||
_name: string = '<no name set>';
|
||||
|
||||
@Input()
|
||||
set name(name: string) {
|
||||
this._name = (name && name.trim()) || '<no name set>';
|
||||
}
|
||||
|
||||
get name() { return this._name; }
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,19 @@
|
|||
// #docregion
|
||||
import {Component} from 'angular2/core';
|
||||
import {NameChildComponent} from './name-child.component';
|
||||
|
||||
@Component({
|
||||
selector: 'name-parent',
|
||||
template: `
|
||||
<h2>Master controls {{names.length}} names</h2>
|
||||
<name-child *ngFor="#name of names"
|
||||
[name]="name">
|
||||
</name-child>
|
||||
`,
|
||||
directives: [NameChildComponent]
|
||||
})
|
||||
export class NameParentComponent {
|
||||
// Displays 'Mr. IQ', '<no name set>', 'Bombasto'
|
||||
names = ['Mr. IQ', ' ', ' Bombasto '];
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,30 @@
|
|||
// #docregion
|
||||
import {Component, Input, OnChanges, SimpleChange} from 'angular2/core';
|
||||
|
||||
@Component({
|
||||
selector: 'version-child',
|
||||
template: `
|
||||
<h3>Version {{major}}.{{minor}}</h3>
|
||||
<h4>Change log:</h4>
|
||||
<ul>
|
||||
<li *ngFor="#change of changeLog">{{change}}</li>
|
||||
</ul>
|
||||
`
|
||||
})
|
||||
export class VersionChildComponent implements OnChanges {
|
||||
@Input() major: number;
|
||||
@Input() minor: number;
|
||||
changeLog: string[] = [];
|
||||
|
||||
ngOnChanges(changes: {[propKey:string]: SimpleChange}){
|
||||
let log: string[] = [];
|
||||
for (let propName in changes) {
|
||||
let changedProp = changes[propName];
|
||||
let from = JSON.stringify(changedProp.previousValue);
|
||||
let to = JSON.stringify(changedProp.currentValue);
|
||||
log.push( `${propName} changed from ${from} to ${to}`);
|
||||
}
|
||||
this.changeLog.push(log.join(', '));
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,28 @@
|
|||
// #docregion
|
||||
import {Component} from 'angular2/core';
|
||||
import {VersionChildComponent} from './version-child.component';
|
||||
|
||||
@Component({
|
||||
selector: 'version-parent',
|
||||
template: `
|
||||
<h2>Source code version</h2>
|
||||
<button (click)="newMinor()">New minor version</button>
|
||||
<button (click)="newMajor()">New major version</button>
|
||||
<version-child [major]="major" [minor]="minor"></version-child>
|
||||
`,
|
||||
directives: [VersionChildComponent]
|
||||
})
|
||||
export class VersionParentComponent {
|
||||
major: number = 1;
|
||||
minor: number = 23;
|
||||
|
||||
newMinor() {
|
||||
this.minor++;
|
||||
}
|
||||
|
||||
newMajor() {
|
||||
this.major++;
|
||||
this.minor = 0;
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,22 @@
|
|||
// #docregion
|
||||
import {Component, EventEmitter, Input, Output} from 'angular2/core';
|
||||
|
||||
@Component({
|
||||
selector: 'my-voter',
|
||||
template: `
|
||||
<h4>{{name}}</h4>
|
||||
<button (click)="vote(true)" [disabled]="voted">Agree</button>
|
||||
<button (click)="vote(false)" [disabled]="voted">Disagree</button>
|
||||
`
|
||||
})
|
||||
export class VoterComponent {
|
||||
@Input() name: string;
|
||||
@Output() onVoted = new EventEmitter<boolean>();
|
||||
voted = false;
|
||||
|
||||
vote(agreed:boolean){
|
||||
this.onVoted.emit(agreed);
|
||||
this.voted = true;
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,26 @@
|
|||
// #docregion
|
||||
import {Component} from 'angular2/core';
|
||||
import {VoterComponent} from './voter.component';
|
||||
|
||||
@Component({
|
||||
selector: 'vote-taker',
|
||||
template: `
|
||||
<h2>Should mankind colonize the Universe?</h2>
|
||||
<h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
|
||||
<my-voter *ngFor="#voter of voters"
|
||||
[name]="voter"
|
||||
(onVoted)="onVoted($event)">
|
||||
</my-voter>
|
||||
`,
|
||||
directives: [VoterComponent]
|
||||
})
|
||||
export class VoteTakerComponent {
|
||||
agreed = 0;
|
||||
disagreed = 0;
|
||||
voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto']
|
||||
|
||||
onVoted(agreed: boolean) {
|
||||
agreed ? this.agreed++ : this.disagreed++;
|
||||
}
|
||||
}
|
||||
// #enddocregion
|
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Passing information from parent to child</title>
|
||||
<style>
|
||||
.to-top {margin-top: 8px; display: block;}
|
||||
</style>
|
||||
|
||||
<!-- IE required polyfills, in this exact order -->
|
||||
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
|
||||
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
|
||||
|
||||
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
|
||||
<script src="node_modules/systemjs/dist/system.src.js"></script>
|
||||
<script src="node_modules/rxjs/bundles/Rx.js"></script>
|
||||
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
|
||||
<script>
|
||||
System.config({
|
||||
packages: {
|
||||
app: {
|
||||
format: 'register',
|
||||
defaultExtension: 'js'
|
||||
}
|
||||
}
|
||||
});
|
||||
System.import('app/main')
|
||||
.then(null, console.error.bind(console));
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app>loading...</app>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"description": "Component Communication Cookbook samples",
|
||||
"files":[
|
||||
"!**/*.d.ts",
|
||||
"!**/*.js"
|
||||
],
|
||||
"tags":["cookbook", "component"]
|
||||
}
|
|
@ -3,7 +3,8 @@
|
|||
if secondaryPath
|
||||
- var data = secondaryPath._data
|
||||
- var listType = data._listtype
|
||||
- var ordered = listType == "ordered" ? "is-ordered" : ""
|
||||
- var isOrdered = listType == "ordered" || listType == "alpha"
|
||||
- var ordered = isOrdered ? "is-ordered" : ""
|
||||
- var items = listType == 'api' ? secondaryPath : data
|
||||
- var number = 1
|
||||
|
||||
|
@ -26,7 +27,7 @@ if secondaryPath
|
|||
- var path = "/docs/" + current.path[1] + "/" + current.path[2] + "/" + current.path[3] + "/" + slug
|
||||
|
||||
// ORDERED LIST VALUES
|
||||
if listType == 'ordered'
|
||||
if isOrdered
|
||||
- var num = number++
|
||||
- var name = (listType == "ordered") ? num + '. ' + page.title : page.title;
|
||||
|
||||
|
|
|
@ -23,6 +23,12 @@
|
|||
"banner": "Angular 2 is currently in Beta."
|
||||
},
|
||||
|
||||
"cookbooks": {
|
||||
"icon": "list",
|
||||
"title": "Cookbook Recipes",
|
||||
"banner": "How to solve common implementation challenges."
|
||||
},
|
||||
|
||||
"testing": {
|
||||
"icon": "list",
|
||||
"title": "Testing Guides",
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"_listtype": "alpha",
|
||||
|
||||
"index": {
|
||||
"title": "Cookbooks"
|
||||
},
|
||||
|
||||
"component-communication": {
|
||||
"title": "Component Communication"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
include ../../../../_includes/_util-fns
|
||||
|
||||
<a id="top"></a>
|
||||
:marked
|
||||
This cookbook contains recipes for common component communication scenarios
|
||||
in which two or more components share information.
|
||||
|
||||
//
|
||||
.l-sub-section
|
||||
:marked
|
||||
For an in-depth look at each fundamental concepts in component communication, we can find detailed description and
|
||||
samples in the [Component Communication]() document.
|
||||
|
||||
<a id="toc"></a>
|
||||
:marked
|
||||
## Table of contents
|
||||
|
||||
[Pass data from parent to child with input binding](#parent-to-child)
|
||||
|
||||
[Intercept input property changes with a setter](#parent-to-child-setter)
|
||||
|
||||
[Intercept input property changes with *ngOnChanges*](#parent-to-child-on-changes)
|
||||
|
||||
[Parent listens for child event](#child-to-parent)
|
||||
|
||||
[Parent calls a *ViewChild*](#parent-to-view-child)
|
||||
|
||||
[Parent and children communicate via a service](#bidirectional-service)
|
||||
|
||||
:marked
|
||||
**See the [live example](/resources/live-examples/cb-component-communication/ts/plnkr.html)**.
|
||||
|
||||
.l-main-section
|
||||
<a id="parent-to-child"></a>
|
||||
:marked
|
||||
## Pass data from parent to child with input binding
|
||||
|
||||
`HeroChildComponent` has two ***input properties***,
|
||||
typically adorned with [@Input decorations](docs/ts/latest/guide/template-syntax.html#inputs-outputs).
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/hero-child.component.ts')
|
||||
:marked
|
||||
The second `@Input` aliases the child component property name `masterName` as `'master'`.
|
||||
|
||||
The `HeroParentComponent` nests the child `HeroChildComponent` inside an `*ngFor` repeater,
|
||||
binding its `master` string property to the child's `master` alias
|
||||
and each iteration's `hero` instance to the child's `hero` property.
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/hero-parent.component.ts')
|
||||
:marked
|
||||
The running application displays three heroes:
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/cookbooks/component-communication/parent-to-child.png" alt="Parent-to-child")
|
||||
|
||||
:marked
|
||||
### Test it
|
||||
|
||||
E2E test that all children were instantiated and displayed as expected:
|
||||
|
||||
+makeExample('cb-component-communication/e2e-spec.js', 'parent-to-child')
|
||||
|
||||
:marked
|
||||
[Back to top](#top)
|
||||
|
||||
.l-main-section
|
||||
<a id="parent-to-child-setter"></a>
|
||||
:marked
|
||||
## Intercept input property changes with a setter
|
||||
|
||||
Use an input property setter to intercept and act upon a value from the parent.
|
||||
|
||||
The setter of the `name` input property in the child `NameChildComponent`
|
||||
trims the whitespace from a name and replaces an empty value with default text.
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/name-child.component.ts')
|
||||
|
||||
:marked
|
||||
Here's the `NameParentComponent` demonstrating name variations including a name with all spaces:
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/name-parent.component.ts')
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/cookbooks/component-communication/setter.png" alt="Parent-to-child-setter")
|
||||
|
||||
:marked
|
||||
### Test it
|
||||
|
||||
E2E tests of input property setter with empty and non-empty names:
|
||||
|
||||
+makeExample('cb-component-communication/e2e-spec.js', 'parent-to-child-setter')
|
||||
|
||||
:marked
|
||||
[Back to top](#top)
|
||||
|
||||
.l-main-section
|
||||
<a id="parent-to-child-on-changes"></a>
|
||||
:marked
|
||||
## Intercept input property changes with *ngOnChanges*
|
||||
|
||||
Detect and act upon changes to input property values with the `ngOnChanges` method of the `OnChanges` lifecycle hook interface.
|
||||
.l-sub-section
|
||||
:marked
|
||||
May prefer this approach to the property setter when watching multiple, interacting input properties.
|
||||
|
||||
Learn about `ngOnChanges` in the [LifeCycle Hooks](../guide/lifecycle-hooks.html) chapter.
|
||||
:marked
|
||||
This `VersionChildComponent` detects changes to the `major` and `minor` input properties and composes a log message reporting these changes:
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/version-child.component.ts')
|
||||
|
||||
:marked
|
||||
The `VersionParentComponent` supplies the `minor` and `major` values and binds buttons to methods that change them.
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/version-parent.component.ts')
|
||||
|
||||
:marked
|
||||
Here's the output of a button-pushing sequence:
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/cookbooks/component-communication/parent-to-child-on-changes.gif" alt="Parent-to-child-onchanges")
|
||||
|
||||
:marked
|
||||
### Test it
|
||||
|
||||
Test that ***both*** input properties are set initially and that button clicks trigger
|
||||
the expected `ngOnChanges` calls and values:
|
||||
|
||||
+makeExample('cb-component-communication/e2e-spec.js', 'parent-to-child-onchanges')
|
||||
|
||||
:marked
|
||||
[Back to top](#top)
|
||||
|
||||
.l-main-section
|
||||
<a id="child-to-parent"></a>
|
||||
:marked
|
||||
## Parent listens for child event
|
||||
|
||||
The child component exposes an `EventEmitter` property with which it `emits`events when something happens.
|
||||
The parent binds to that event property and reacts to those events.
|
||||
|
||||
The child's `EventEmitter` property is an ***output property***,
|
||||
typically adorned with an [@Output decoration](docs/ts/latest/guide/template-syntax.html#inputs-outputs)
|
||||
as seen in this `VoterComponent`:
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/voter.component.ts')
|
||||
|
||||
:marked
|
||||
Clicking a button triggers emission of a `true` or `false` (the boolean *payload*).
|
||||
|
||||
The parent `VoteTakerComponent` binds an event handler (`onVoted`) that responds to the child event
|
||||
payload (`$event`) and updates a counter.
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/votetaker.component.ts')
|
||||
|
||||
:marked
|
||||
The framework passes the event argument — represented by `$event` — to the handler method,
|
||||
and the method processes it:
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/cookbooks/component-communication/child-to-parent.gif" alt="Child-to-parent")
|
||||
|
||||
:marked
|
||||
### Test it
|
||||
|
||||
Test that clicking the *Agree* and *Disagree* buttons update the appropriate counters:
|
||||
|
||||
+makeExample('cb-component-communication/e2e-spec.js', 'child-to-parent')
|
||||
|
||||
:marked
|
||||
[Back to top](#top)
|
||||
|
||||
.l-main-section
|
||||
<a id="parent-to-view-child"></a>
|
||||
:marked
|
||||
## Parent calls a *ViewChild*
|
||||
A parent can call a child component once it has been located by a property adorned with a `@ViewChild` decorator property.
|
||||
|
||||
This `CountdownTimerComponent` keeps counting down to zero and launching rockets.
|
||||
It has `start` and `stop` methods that control the countdown.
|
||||
+makeExample('cb-component-communication/ts/app/countdown-timer.component.ts')
|
||||
:marked
|
||||
The parent `CountdownParentComponent` cannot bind to the child's `start` and `stop` methods.
|
||||
But it can obtain a reference to the child component by applying a `@ViewChild` decorator
|
||||
to a receiver property (`timerComponent`) after giving that decorator the type of component to find.
|
||||
Once it has that reference, it can access *any property or method* of the child component.
|
||||
|
||||
Here it wires its own buttons to the child's start` and `stop`.
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/countdown-parent.component.ts')
|
||||
:marked
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/cookbooks/component-communication/countdown-timer-anim.gif" alt="countdown timer")
|
||||
:marked
|
||||
### Test it
|
||||
|
||||
Test that clicking the *Stop* button pauses the countdown timer:
|
||||
|
||||
+makeExample('cb-component-communication/e2e-spec.js', 'parent-to-view-child')
|
||||
|
||||
:marked
|
||||
[Back to top](#top)
|
||||
|
||||
.l-main-section
|
||||
<a id="bidirectional-service"></a>
|
||||
:marked
|
||||
## Parent and children communicate via a service
|
||||
|
||||
A parent component and its children share a service whose interface enables bi-directional communication
|
||||
*within the family*.
|
||||
|
||||
The scope of the service instance is the parent component and its children.
|
||||
Components outside this component subtree have no access to the service or their communications.
|
||||
|
||||
This `MissionService` connects the `MissionControlComponent` to multiple `AstronautComponent` children.
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/mission.service.ts')
|
||||
:marked
|
||||
The `MissionControlComponent` both provides the instance of the service that it shares with its children
|
||||
(through the `providers` metadata array) and injects that instance into itself through its constructor:
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/missioncontrol.component.ts')
|
||||
|
||||
:marked
|
||||
The `AstronoutComponent` also injects the service in its constructor.
|
||||
Each `AstronoutComponent` is a child of the `MissionControlComponent` and therefore receives its parent's service instance:
|
||||
|
||||
+makeExample('cb-component-communication/ts/app/astronaut.component.ts')
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
Notice that we capture the `subscription` and unsubscribe when the `AstronautComponent` is destroyed.
|
||||
This is a memory-leak guard step. There is no actual risk in this app because the
|
||||
lifetime of a `AstronautComponent` is the same as the lifetime of the app itself.
|
||||
That *would not* always be true in a more complex application.
|
||||
|
||||
We do not add this guard to the `MissionControlComponent` because, as the parent,
|
||||
it controls the lifetime of the `MissionService`.
|
||||
:marked
|
||||
The *History* log demonstrates that messages travel in both directions between
|
||||
the parent `MissionControlComponent` and the `AstronoutComponent` children,
|
||||
facilitated by the service:
|
||||
|
||||
figure.image-display
|
||||
img(src="/resources/images/cookbooks/component-communication/bidirectional-service.gif" alt="bidirectional-service")
|
||||
|
||||
:marked
|
||||
### Test it
|
||||
|
||||
Tests click buttons of both the parent `MissionControlComponent` and the `AstronoutComponent` children
|
||||
and verify that the *History* meets expectations:
|
||||
|
||||
+makeExample('cb-component-communication/e2e-spec.js', 'bidirectional-service')
|
||||
|
||||
:marked
|
||||
[Back to top](#top)
|
|
@ -0,0 +1,27 @@
|
|||
include ../../../../_includes/_util-fns
|
||||
|
||||
:marked
|
||||
# Angular 2 Cookbooks
|
||||
|
||||
The *Cookbook* documentation series offers answers to common implementation questions.
|
||||
|
||||
Each cookbook is a collection of recipes focused on a particular Angular 2 feature or application challenge
|
||||
such as data binding, cross-component interaction, and communicating with a remote server via HTTP.
|
||||
|
||||
.l-sub-section
|
||||
:marked
|
||||
We have one cookbook so far with more on the way.
|
||||
:marked
|
||||
Recipes are deliberately brief and code-centric. Each recipe links to a chapter of the Developer Guide or the API Guide
|
||||
where you can learn more about the purpose, context, and design choices behind the code snippets.
|
||||
|
||||
Each cookbook links to a live sample with every recipe included.
|
||||
|
||||
## Feedback
|
||||
|
||||
Cookbooks are a perpetual *work-in-progress*. We welcome feedback! Leave a comment by clicking the icon in upper right corner of the banner.
|
||||
|
||||
Post *documentation* issues and pull requests on the
|
||||
[angular.io](https://github.com/angular/angular.io) github repository.
|
||||
|
||||
Post issues with *Angular 2 itself* to the [angular](https://github.com/angular/angular) github repository.
|
|
@ -224,4 +224,9 @@
|
|||
|
||||
.l-layer-10 {
|
||||
z-index: $layer-10;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Other
|
||||
*/
|
||||
.to-top {margin-top: $unit * 8; display: block;}
|
||||
|
|
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 21 KiB |