diff --git a/public/docs/_examples/cb-component-communication/e2e-spec.js b/public/docs/_examples/cb-component-communication/e2e-spec.js index 84729e0cef..37d97f4bd5 100644 --- a/public/docs/_examples/cb-component-communication/e2e-spec.js +++ b/public/docs/_examples/cb-component-communication/e2e-spec.js @@ -149,24 +149,39 @@ 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 diff --git a/public/docs/_examples/cb-component-communication/ts/app/app.component.html b/public/docs/_examples/cb-component-communication/ts/app/app.component.html index 6cfb5b76ed..0c71b7379f 100644 --- a/public/docs/_examples/cb-component-communication/ts/app/app.component.html +++ b/public/docs/_examples/cb-component-communication/ts/app/app.component.html @@ -1,11 +1,12 @@

Component Communication Cookbook

-Pass data from parent to child with input binding
-Intercept input property changes with a setter
-Intercept input property changes with ngOnChanges
-Parent listens for child event
-Parent calls ViewChild
-Parent and children communicate via a service
+Pass data from parent to child with input binding ("Heros")
+Intercept input property changes with a setter ("Master")
+Intercept input property changes with ngOnChanges ("Source code version")
+Parent listens for child event ("Colonize Universe")
+Parent to child via local variable("Countdown to Liftoff")
+Parent calls ViewChild("Countdown to Liftoff")
+Parent and children communicate via a service ("Mission Control")
@@ -31,8 +32,14 @@ Back to Top
+
+ +
+Back to Top +
+
- +
Back to Top
diff --git a/public/docs/_examples/cb-component-communication/ts/app/app.component.ts b/public/docs/_examples/cb-component-communication/ts/app/app.component.ts index a1d11ed518..1cb24ac989 100644 --- a/public/docs/_examples/cb-component-communication/ts/app/app.component.ts +++ b/public/docs/_examples/cb-component-communication/ts/app/app.component.ts @@ -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,8 +15,9 @@ import {MissionControlComponent} from './missioncontrol.component'; NameParentComponent, VersionParentComponent, VoteTakerComponent, - CountdownParentComponent, + CountdownLocalVarParentComponent, + CountdownViewChildParentComponent, MissionControlComponent ] }) -export class AppComponent { } \ No newline at end of file +export class AppComponent { } diff --git a/public/docs/_examples/cb-component-communication/ts/app/countdown-parent.component.ts b/public/docs/_examples/cb-component-communication/ts/app/countdown-parent.component.ts index d1e2a00cc9..bc3487c688 100644 --- a/public/docs/_examples/cb-component-communication/ts/app/countdown-parent.component.ts +++ b/public/docs/_examples/cb-component-communication/ts/app/countdown-parent.component.ts @@ -1,22 +1,59 @@ -// #docregion -import {Component, ViewChild} from 'angular2/core'; -import {CountdownTimerComponent} from './countdown-timer.component'; +// #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: ` -

Countdown to Liftoff

+

Countdown to Liftoff (via local variable)

+ + +
{{timer.seconds}}
+ + `, + directives: [CountdownTimerComponent], + styleUrls: ['demo.css'] +}) +export class CountdownLocalVarParentComponent { } +// #enddocregion lv + +//// View Child version +// #docregion vc +@Component({ + selector:'countdown-parent-vc', + template: ` +

Countdown to Liftoff (via ViewChild)

+
{{ seconds() }}
`, - 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(); } -} \ No newline at end of file +} +// #enddocregion vc diff --git a/public/docs/_examples/cb-component-communication/ts/app/countdown-timer.component.ts b/public/docs/_examples/cb-component-communication/ts/app/countdown-timer.component.ts index 0175993303..5df84c5e50 100644 --- a/public/docs/_examples/cb-component-communication/ts/app/countdown-timer.component.ts +++ b/public/docs/_examples/cb-component-communication/ts/app/countdown-timer.component.ts @@ -12,24 +12,24 @@ export class CountdownTimerComponent implements OnInit, OnDestroy { seconds = 11; clearTimer() {clearInterval(this.intervalId);} - + ngOnInit() { this.start(); } ngOnDestroy() { this.clearTimer(); } - + start() { this._countDown(); } stop() { this.clearTimer(); this.message = `Holding at T-${this.seconds} seconds`; } - + private _countDown() { this.clearTimer(); this.intervalId = setInterval(()=>{ 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); diff --git a/public/docs/_examples/cb-component-communication/ts/app/missioncontrol.component.ts b/public/docs/_examples/cb-component-communication/ts/app/missioncontrol.component.ts index 2f0ac129c0..edcb0ae344 100644 --- a/public/docs/_examples/cb-component-communication/ts/app/missioncontrol.component.ts +++ b/public/docs/_examples/cb-component-communication/ts/app/missioncontrol.component.ts @@ -11,7 +11,7 @@ import {MissionService} from './mission.service'; -

History

+

History

diff --git a/public/docs/_examples/cb-component-communication/ts/demo.css b/public/docs/_examples/cb-component-communication/ts/demo.css new file mode 100644 index 0000000000..b63a8b38dd --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/demo.css @@ -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; +} diff --git a/public/docs/_examples/cb-component-communication/ts/index.html b/public/docs/_examples/cb-component-communication/ts/index.html index 8678794506..56965e27cf 100644 --- a/public/docs/_examples/cb-component-communication/ts/index.html +++ b/public/docs/_examples/cb-component-communication/ts/index.html @@ -7,11 +7,12 @@ .to-top {margin-top: 8px; display: block;} + - + diff --git a/public/docs/ts/latest/cookbook/component-communication.jade b/public/docs/ts/latest/cookbook/component-communication.jade index 4f814464ff..02f040b4c2 100644 --- a/public/docs/ts/latest/cookbook/component-communication.jade +++ b/public/docs/ts/latest/cookbook/component-communication.jade @@ -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) @@ -170,38 +172,112 @@ figure.image-display :marked [Back to top](#top) +parent-to-child-local-var +.l-main-section + +: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. + + + 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 (``) 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 :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 + We are switching from the *local variable* to the *ViewChild* technique + solely for the purpose of demonstration. :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`. + 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. -+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') - + Use [the same countdown timer tests](#countdown-tests) as before. :marked [Back to top](#top) - + .l-main-section :marked diff --git a/public/docs/ts/latest/glossary.jade b/public/docs/ts/latest/glossary.jade index 6498392b59..4bbdca4c5c 100644 --- a/public/docs/ts/latest/glossary.jade +++ b/public/docs/ts/latest/glossary.jade @@ -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' : 'decorator' +- var atSym = lang === 'js' ? '' : '@' .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' : 'decorator' +- var atSym = lang === 'js' ? '' : '@' .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. diff --git a/public/resources/images/cookbooks/component-communication/countdown-timer-anim.gif b/public/resources/images/cookbooks/component-communication/countdown-timer-anim.gif index 31df3ec69f..d225b9df16 100644 Binary files a/public/resources/images/cookbooks/component-communication/countdown-timer-anim.gif and b/public/resources/images/cookbooks/component-communication/countdown-timer-anim.gif differ