docs(cookbook/component-communication): first of the cookbook series

closes #824
This commit is contained in:
Novak Istvan 2016-02-02 14:39:34 +01:00 committed by Ward Bell
parent 1010034249
commit daae4f1429
36 changed files with 994 additions and 3 deletions

View File

@ -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
});
});

View File

@ -0,0 +1 @@
**/*.js

View File

@ -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>

View File

@ -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 { }

View File

@ -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

View File

@ -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(); }
}

View File

@ -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`;
}
}

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,9 @@
export interface Hero {
name: string;
}
export const HEROES = [
{name: 'Mr. IQ'},
{name: 'Magneta'},
{name: 'Bombasto'}
];

View File

@ -0,0 +1,4 @@
import {bootstrap} from 'angular2/platform/browser';
import {AppComponent} from './app.component';
bootstrap(AppComponent);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -0,0 +1,8 @@
{
"description": "Component Communication Cookbook samples",
"files":[
"!**/*.d.ts",
"!**/*.js"
],
"tags":["cookbook", "component"]
}

View File

@ -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;

View File

@ -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",

View File

@ -0,0 +1,11 @@
{
"_listtype": "alpha",
"index": {
"title": "Cookbooks"
},
"component-communication": {
"title": "Component Communication"
}
}

View File

@ -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 &mdash; represented by `$event` &mdash; 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)

View File

@ -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.

View File

@ -224,4 +224,9 @@
.l-layer-10 {
z-index: $layer-10;
}
}
/*
* Other
*/
.to-top {margin-top: $unit * 8; display: block;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB