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

@ -150,23 +150,38 @@ describe('Component Communication Cookbook Tests', function () {
// #enddocregion child-to-parent
});
describe('Parent calls child via local var', function() {
countDownTimerTests('countdown-parent-lv')
});
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 () {
var stopButton = element
.all(by.tagName('countdown-parent')).get(0)
.all(by.tagName('button')).get(1);
var parent = element(by.tagName(parentTag));
var stopButton = parent.all(by.tagName('button')).get(1);
stopButton.click().then(function() {
var message = element(by.tagName('countdown-timer'))
.element(by.tagName('p')).getText();
var message = parent.element(by.tagName('countdown-timer')).getText();
expect(message).toContain('Holding');
});
});
// ...
// #enddocregion parent-to-view-child
});
// #enddocregion countdown-timer-tests
}
describe('Parent and children communicate via a service', function() {
// #docregion bidirectional-service

View File

@ -1,11 +1,12 @@
<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/>
<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 ("Master")</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 ("Colonize Universe")</a><br/>
<a href="#parent-to-child-local-var">Parent to child via <i>local variable</i>("Countdown to Liftoff")</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">
<hero-parent></hero-parent>
@ -31,8 +32,14 @@
<a href="#top" class="to-top">Back to Top</a>
<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">
<countdown-parent></countdown-parent>
<countdown-parent-vc></countdown-parent-vc>
</div>
<a href="#top" class="to-top">Back to Top</a>
<hr>

View File

@ -3,7 +3,8 @@ 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 {CountdownLocalVarParentComponent,
CountdownViewChildParentComponent} from './countdown-parent.component';
import {MissionControlComponent} from './missioncontrol.component';
@Component({
@ -14,7 +15,8 @@ import {MissionControlComponent} from './missioncontrol.component';
NameParentComponent,
VersionParentComponent,
VoteTakerComponent,
CountdownParentComponent,
CountdownLocalVarParentComponent,
CountdownViewChildParentComponent,
MissionControlComponent
]
})

View File

@ -1,22 +1,59 @@
// #docregion
import {Component, ViewChild} from 'angular2/core';
// #docplaster
// #docregion vc
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({
selector:'countdown-parent',
selector:'countdown-parent-lv',
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)="stop()">Stop</button>
<div class="seconds">{{ seconds() }}</div>
<countdown-timer></countdown-timer>
`,
directives: [CountdownTimerComponent]
directives: [CountdownTimerComponent],
styleUrls: ['demo.css']
})
export class CountdownParentComponent {
export class CountdownViewChildParentComponent implements AfterViewInit {
@ViewChild(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(); }
stop() { this._timerComponent.stop(); }
}
// #enddocregion vc

View File

@ -28,8 +28,8 @@ export class CountdownTimerComponent implements OnInit, OnDestroy {
this.seconds -= 1;
if (this.seconds == 0) {
this.message = "Blast off!";
this.seconds = 11; // reset
} else {
if (this.seconds < 0) { this.seconds = 10;} // reset
this.message = `T-${this.seconds} seconds and counting`;
}
}, 1000);

View File

@ -11,7 +11,7 @@ import {MissionService} from './mission.service';
<my-astronaut *ngFor="#astronaut of astronauts"
[astronaut]="astronaut">
</my-astronaut>
<h2>History</h2>
<h3>History</h3>
<ul>
<li *ngFor="#event of history">{{event}}</li>
</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,6 +7,7 @@
.to-top {margin-top: 8px; display: block;}
</style>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="demo.css">
<!-- IE required polyfills, in this exact order -->
<script src="node_modules/es6-shim/es6-shim.min.js"></script>

View File

@ -23,6 +23,8 @@ include ../_util-fns
[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 and children communicate via a service](#bidirectional-service)
@ -167,6 +169,56 @@ figure.image-display
+makeExample('cb-component-communication/e2e-spec.js', 'child-to-parent')
:marked
[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)
@ -174,31 +226,55 @@ figure.image-display
<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')
The *local variable* approach is simple and easy. But it is limited because
the parent-child wiring must be done entirely within the parent template.
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
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')
We are switching from the *local variable* to the *ViewChild* technique
solely for the purpose of demonstration.
:marked
figure.image-display
img(src="/resources/images/cookbooks/component-communication/countdown-timer-anim.gif" alt="countdown timer")
Here is the parent, `CountdownViewChildParentComponent`:
+makeExample('cb-component-communication/ts/app/countdown-parent.component.ts', 'vc')
:marked
It takes a bit more work to get the child view into the parent component classs.
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.
### Test it
Test that clicking the *Stop* button pauses the countdown timer:
+makeExample('cb-component-communication/e2e-spec.js', 'parent-to-view-child')
Use [the same countdown timer tests](#countdown-tests) as before.
:marked
[Back to top](#top)

View File

@ -42,8 +42,8 @@ include _util-fns
// #docregion b-c
- var lang = current.path[1]
- var decorator = lang = 'dart' ? 'annotation' : '[decorator](#decorator)'
- var atSym = lang == 'js' ? '' : '@'
- var decorator = lang === 'dart' ? 'annotation' : '<a href="#decorator">decorator</a>'
- var atSym = lang === 'js' ? '' : '@'
<a id="B"></a>
.l-main-section
:marked
@ -116,7 +116,7 @@ include _util-fns
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).
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
that Angular needs to create a component instance and render it with its template
as a view.
@ -446,8 +446,8 @@ include _util-fns
// #docregion n-s
- var lang = current.path[1]
- var decorator = lang = 'dart' ? 'annotation' : '[decorator](#decorator)'
- var atSym = lang == 'js' ? '' : '@'
- var decorator = lang === 'dart' ? 'annotation' : '<a href="#decorator">decorator</a>'
- var atSym = lang === 'js' ? '' : '@'
<a id="N"></a>
<a id="O"></a>
.l-main-section
@ -469,7 +469,7 @@ include _util-fns
.l-sub-section
:marked
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
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