docs(comp comm cookbook): add #localvar alternative for parent calling child

also fixes glossary decorator-flag bug
This commit is contained in:
Ward Bell 2016-03-07 11:50:14 -08:00
parent b0f1f3a4b8
commit 513e919be5
11 changed files with 208 additions and 61 deletions

View File

@ -149,24 +149,39 @@ describe('Component Communication Cookbook Tests', function () {
// ... // ...
// #enddocregion child-to-parent // #enddocregion child-to-parent
}); });
describe('Parent calls child via local var', function() {
countDownTimerTests('countdown-parent-lv')
});
describe('Parent calls ViewChild', function() { describe('Parent calls ViewChild', function() {
// #docregion parent-to-view-child countDownTimerTests('countdown-parent-vc')
});
function countDownTimerTests(parentTag) {
// #docregion countdown-timer-tests
// ... // ...
it('timer and parent seconds should match', function () {
var parent = element(by.tagName(parentTag));
var message = parent.element(by.tagName('countdown-timer')).getText();
browser.sleep(10); // give `seconds` a chance to catchup with `message`
var seconds = parent.element(by.className('seconds')).getText();
expect(message).toContain(seconds);
});
it('should stop the countdown', function () { it('should stop the countdown', function () {
var stopButton = element var parent = element(by.tagName(parentTag));
.all(by.tagName('countdown-parent')).get(0) var stopButton = parent.all(by.tagName('button')).get(1);
.all(by.tagName('button')).get(1);
stopButton.click().then(function() { stopButton.click().then(function() {
var message = element(by.tagName('countdown-timer')) var message = parent.element(by.tagName('countdown-timer')).getText();
.element(by.tagName('p')).getText();
expect(message).toContain('Holding'); expect(message).toContain('Holding');
}); });
}); });
// ... // ...
// #enddocregion parent-to-view-child // #enddocregion countdown-timer-tests
}); }
describe('Parent and children communicate via a service', function() { describe('Parent and children communicate via a service', function() {
// #docregion bidirectional-service // #docregion bidirectional-service

View File

@ -1,11 +1,12 @@
<h1 id="top">Component Communication Cookbook</h1> <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">Pass data from parent to child with input binding ("Heros")</a><br/>
<a href="#parent-to-child-setter">Intercept input property changes with a setter</a><br/> <a href="#parent-to-child-setter">Intercept input property changes with a setter ("Master")</a><br/>
<a href="#parent-to-child-on-changes">Intercept input property changes with <i>ngOnChanges</i></a><br/> <a href="#parent-to-child-on-changes">Intercept input property changes with <i>ngOnChanges</i> ("Source code version")</a><br/>
<a href="#child-to-parent">Parent listens for child event</a><br/> <a href="#child-to-parent">Parent listens for child event ("Colonize Universe")</a><br/>
<a href="#parent-to-view-child">Parent calls <i>ViewChild</i></a><br/> <a href="#parent-to-child-local-var">Parent to child via <i>local variable</i>("Countdown to Liftoff")</a><br/>
<a href="#bidirectional-service">Parent and children communicate via a service</a><br/> <a href="#parent-to-view-child">Parent calls <i>ViewChild</i>("Countdown to Liftoff")</a><br/>
<a href="#bidirectional-service">Parent and children communicate via a service ("Mission Control")</a><br/>
<div id="parent-to-child"> <div id="parent-to-child">
<hero-parent></hero-parent> <hero-parent></hero-parent>
@ -31,8 +32,14 @@
<a href="#top" class="to-top">Back to Top</a> <a href="#top" class="to-top">Back to Top</a>
<hr> <hr>
<div id="parent-to-child-local-var">
<countdown-parent-lv></countdown-parent-lv>
</div>
<a href="#top" class="to-top">Back to Top</a>
<hr>
<div id="parent-to-view-child"> <div id="parent-to-view-child">
<countdown-parent></countdown-parent> <countdown-parent-vc></countdown-parent-vc>
</div> </div>
<a href="#top" class="to-top">Back to Top</a> <a href="#top" class="to-top">Back to Top</a>
<hr> <hr>

View File

@ -3,7 +3,8 @@ import {HeroParentComponent} from './hero-parent.component';
import {NameParentComponent} from './name-parent.component'; import {NameParentComponent} from './name-parent.component';
import {VersionParentComponent} from './version-parent.component'; import {VersionParentComponent} from './version-parent.component';
import {VoteTakerComponent} from './votetaker.component'; import {VoteTakerComponent} from './votetaker.component';
import {CountdownParentComponent} from './countdown-parent.component'; import {CountdownLocalVarParentComponent,
CountdownViewChildParentComponent} from './countdown-parent.component';
import {MissionControlComponent} from './missioncontrol.component'; import {MissionControlComponent} from './missioncontrol.component';
@Component({ @Component({
@ -14,8 +15,9 @@ import {MissionControlComponent} from './missioncontrol.component';
NameParentComponent, NameParentComponent,
VersionParentComponent, VersionParentComponent,
VoteTakerComponent, VoteTakerComponent,
CountdownParentComponent, CountdownLocalVarParentComponent,
CountdownViewChildParentComponent,
MissionControlComponent MissionControlComponent
] ]
}) })
export class AppComponent { } export class AppComponent { }

View File

@ -1,22 +1,59 @@
// #docregion // #docplaster
import {Component, ViewChild} from 'angular2/core'; // #docregion vc
import {CountdownTimerComponent} from './countdown-timer.component'; import {AfterViewInit, ViewChild} from 'angular2/core';
// #docregion lv
import {Component} from 'angular2/core';
import {CountdownTimerComponent} from './countdown-timer.component';
// #enddocregion lv
// #enddocregion vc
//// Local variable, #timer, version
// #docregion lv
@Component({ @Component({
selector:'countdown-parent', selector:'countdown-parent-lv',
template: ` template: `
<h3>Countdown to Liftoff</h3> <h3>Countdown to Liftoff (via local variable)</h3>
<button (click)="timer.start()">Start</button>
<button (click)="timer.stop()">Stop</button>
<div class="seconds">{{timer.seconds}}</div>
<countdown-timer #timer></countdown-timer>
`,
directives: [CountdownTimerComponent],
styleUrls: ['demo.css']
})
export class CountdownLocalVarParentComponent { }
// #enddocregion lv
//// View Child version
// #docregion vc
@Component({
selector:'countdown-parent-vc',
template: `
<h3>Countdown to Liftoff (via ViewChild)</h3>
<button (click)="start()">Start</button> <button (click)="start()">Start</button>
<button (click)="stop()">Stop</button> <button (click)="stop()">Stop</button>
<div class="seconds">{{ seconds() }}</div>
<countdown-timer></countdown-timer> <countdown-timer></countdown-timer>
`, `,
directives: [CountdownTimerComponent] directives: [CountdownTimerComponent],
styleUrls: ['demo.css']
}) })
export class CountdownParentComponent { export class CountdownViewChildParentComponent implements AfterViewInit {
@ViewChild(CountdownTimerComponent) @ViewChild(CountdownTimerComponent)
private _timerComponent:CountdownTimerComponent; private _timerComponent:CountdownTimerComponent;
seconds() { return 0; }
ngAfterViewInit() {
// Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...
// but wait a tick first to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(() => this.seconds = () => this._timerComponent.seconds, 0)
}
start(){ this._timerComponent.start(); } start(){ this._timerComponent.start(); }
stop() { this._timerComponent.stop(); } stop() { this._timerComponent.stop(); }
} }
// #enddocregion vc

View File

@ -12,24 +12,24 @@ export class CountdownTimerComponent implements OnInit, OnDestroy {
seconds = 11; seconds = 11;
clearTimer() {clearInterval(this.intervalId);} clearTimer() {clearInterval(this.intervalId);}
ngOnInit() { this.start(); } ngOnInit() { this.start(); }
ngOnDestroy() { this.clearTimer(); } ngOnDestroy() { this.clearTimer(); }
start() { this._countDown(); } start() { this._countDown(); }
stop() { stop() {
this.clearTimer(); this.clearTimer();
this.message = `Holding at T-${this.seconds} seconds`; this.message = `Holding at T-${this.seconds} seconds`;
} }
private _countDown() { private _countDown() {
this.clearTimer(); this.clearTimer();
this.intervalId = setInterval(()=>{ this.intervalId = setInterval(()=>{
this.seconds -= 1; this.seconds -= 1;
if (this.seconds == 0) { if (this.seconds == 0) {
this.message = "Blast off!"; this.message = "Blast off!";
this.seconds = 11; // reset
} else { } else {
if (this.seconds < 0) { this.seconds = 10;} // reset
this.message = `T-${this.seconds} seconds and counting`; this.message = `T-${this.seconds} seconds and counting`;
} }
}, 1000); }, 1000);

View File

@ -11,7 +11,7 @@ import {MissionService} from './mission.service';
<my-astronaut *ngFor="#astronaut of astronauts" <my-astronaut *ngFor="#astronaut of astronauts"
[astronaut]="astronaut"> [astronaut]="astronaut">
</my-astronaut> </my-astronaut>
<h2>History</h2> <h3>History</h3>
<ul> <ul>
<li *ngFor="#event of history">{{event}}</li> <li *ngFor="#event of history">{{event}}</li>
</ul> </ul>

View File

@ -0,0 +1,9 @@
/* Component Communication cookbook specific styles */
.seconds {
background-color: black;
color: red;
font-size: 3em;
margin: 0.3em 0;
text-align: center;
width: 1.5em;
}

View File

@ -7,11 +7,12 @@
.to-top {margin-top: 8px; display: block;} .to-top {margin-top: 8px; display: block;}
</style> </style>
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="demo.css">
<!-- IE required polyfills, in this exact order --> <!-- IE required polyfills, in this exact order -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script> <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/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script> <script src="node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
<script src="node_modules/angular2/bundles/angular2-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/systemjs/dist/system.src.js"></script>

View File

@ -23,6 +23,8 @@ include ../_util-fns
[Parent listens for child event](#child-to-parent) [Parent listens for child event](#child-to-parent)
[Parent interacts with child via a *local variable*](#parent-to-child-local-var)
[Parent calls a *ViewChild*](#parent-to-view-child) [Parent calls a *ViewChild*](#parent-to-view-child)
[Parent and children communicate via a service](#bidirectional-service) [Parent and children communicate via a service](#bidirectional-service)
@ -170,38 +172,112 @@ figure.image-display
:marked :marked
[Back to top](#top) [Back to top](#top)
parent-to-child-local-var
.l-main-section
<a id="parent-to-child-local-var"></a>
:marked
## Parent interacts with child via *local variable*
A parent component cannot use data binding to read child properties
or invoke child methods. We can do both
by creating a template local variable for the child element
and then reference that variable *within the parent template*
as seen in the following example.
<a id="countdown-timer-example"></a>
We have a child `CountdownTimerComponent` that repeatedly counts down to zero and launches a rocket.
It has `start` and `stop` methods that control the clock and it displays a
countdown status message in its own template.
+makeExample('cb-component-communication/ts/app/countdown-timer.component.ts')
:marked
Let's see the `CountdownLocalVarParentComponent` that hosts the timer component.
+makeExample('cb-component-communication/ts/app/countdown-parent.component.ts', 'lv')
:marked
The parent component cannot data bind to the child's
`start` and `stop` methods nor to its `seconds` property.
We can place a local variable (`#timer`) on the tag (`<countdown-timer>`) representing the child component.
That gives us a reference to the child component itself and the ability to access
*any of its properties or methods* from within the parent template.
In this example, we wire parent buttons to the child's `start` and `stop` and
use interpolation to display the child's `seconds` property.
Here we see the parent and child working together.
figure.image-display
img(src="/resources/images/cookbooks/component-communication/countdown-timer-anim.gif" alt="countdown timer")
a(id="countdown-tests")
:marked
### Test it
Test that the seconds displayed in the parent template
match the seconds displayed in the child's status message.
Test also that clicking the *Stop* button pauses the countdown timer:
+makeExample('cb-component-communication/e2e-spec.js', 'countdown-timer-tests')
:marked
[Back to top](#top)
.l-main-section .l-main-section
<a id="parent-to-view-child"></a> <a id="parent-to-view-child"></a>
:marked :marked
## Parent calls a *ViewChild* ## 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. The *local variable* approach is simple and easy. But it is limited because
It has `start` and `stop` methods that control the countdown. the parent-child wiring must be done entirely within the parent template.
+makeExample('cb-component-communication/ts/app/countdown-timer.component.ts') The parent component *itself* has no access to the child.
We can't use the *local variable* technique if an instance of the parent component *class*
must read or write child component values or must call child component methods.
When the parent component *class* requires that kind of access,
we ***inject*** the child component into the parent as a *ViewChild*.
We'll illustrate this technique with the same [Countdown Timer](#countdown-timer-example) example.
We won't change its appearance or behavior.
The child [CountdownTimerComponent](#countdown-timer-example) is the same as well.
.l-sub-section
:marked
We are switching from the *local variable* to the *ViewChild* technique
solely for the purpose of demonstration.
:marked :marked
The parent `CountdownParentComponent` cannot bind to the child's `start` and `stop` methods. Here is the parent, `CountdownViewChildParentComponent`:
But it can obtain a reference to the child component by applying a `@ViewChild` decorator +makeExample('cb-component-communication/ts/app/countdown-parent.component.ts', 'vc')
to a receiver property (`timerComponent`) after giving that decorator the type of component to find. :marked
Once it has that reference, it can access *any property or method* of the child component. It takes a bit more work to get the child view into the parent component classs.
Here it wires its own buttons to the child's start` and `stop`. We import references to the `ViewChild` decorator and the `AfterViewInit` lifecycle hook.
We inject the child `CountdownTimerComponent` into the private `_timerComponent` property
via the `@ViewChild` property decoration.
The `#timer` local variable is gone from the component metadata.
Instead we bind the buttons to the parent component's own `start` and `stop` methods and
present the ticking seconds in an interpolation around the parent component's `seconds` method.
These methods access the injected timer component directly.
The `ngAfterViewInit` lifecycle hook is an important wrinkle.
The timer component isn't available until *after* Angular displays the parent view.
So we display `0` seconds initially.
Then Angular calls the `ngAfterViewInit` lifecycle hook at which time it is *too late*
to update the parent view's display of the countdown seconds.
Angular's unidirectional data flow rule prevents us from updating the parent view's
in the same cycle. We have to *wait one turn* before we can display the seconds.
We use `setTimeout` to wait one tick and then revise the `seconds` method so
that it takes future values from the timer component.
+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 it
Use [the same countdown timer tests](#countdown-tests) as before.
Test that clicking the *Stop* button pauses the countdown timer:
+makeExample('cb-component-communication/e2e-spec.js', 'parent-to-view-child')
:marked :marked
[Back to top](#top) [Back to top](#top)
.l-main-section .l-main-section
<a id="bidirectional-service"></a> <a id="bidirectional-service"></a>
:marked :marked

View File

@ -42,8 +42,8 @@ include _util-fns
// #docregion b-c // #docregion b-c
- var lang = current.path[1] - var lang = current.path[1]
- var decorator = lang = 'dart' ? 'annotation' : '[decorator](#decorator)' - var decorator = lang === 'dart' ? 'annotation' : '<a href="#decorator">decorator</a>'
- var atSym = lang == 'js' ? '' : '@' - var atSym = lang === 'js' ? '' : '@'
<a id="B"></a> <a id="B"></a>
.l-main-section .l-main-section
:marked :marked
@ -116,7 +116,7 @@ include _util-fns
The Component is one of the most important building blocks in the Angular system. The Component is one of the most important building blocks in the Angular system.
It is, in fact, an Angular [Directive](#directive) with a companion [Template](#template). It is, in fact, an Angular [Directive](#directive) with a companion [Template](#template).
The developer applies the `#{atSym}Component` #{decorator} to The developer applies the `#{atSym}Component` !{decorator} to
the component class, thereby attaching to the class the essential component metadata the component class, thereby attaching to the class the essential component metadata
that Angular needs to create a component instance and render it with its template that Angular needs to create a component instance and render it with its template
as a view. as a view.
@ -446,8 +446,8 @@ include _util-fns
// #docregion n-s // #docregion n-s
- var lang = current.path[1] - var lang = current.path[1]
- var decorator = lang = 'dart' ? 'annotation' : '[decorator](#decorator)' - var decorator = lang === 'dart' ? 'annotation' : '<a href="#decorator">decorator</a>'
- var atSym = lang == 'js' ? '' : '@' - var atSym = lang === 'js' ? '' : '@'
<a id="N"></a> <a id="N"></a>
<a id="O"></a> <a id="O"></a>
.l-main-section .l-main-section
@ -469,7 +469,7 @@ include _util-fns
.l-sub-section .l-sub-section
:marked :marked
An Angular pipe is a function that transforms input values to output values for An Angular pipe is a function that transforms input values to output values for
display in a [view](#view). We use the `#{atSym}Pipe` #{decorator} display in a [view](#view). We use the `#{atSym}Pipe` !{decorator}
to associate the pipe function with a name. We then can use that to associate the pipe function with a name. We then can use that
name in our HTML to declaratively transform values on screen. name in our HTML to declaratively transform values on screen.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 43 KiB