diff --git a/public/docs/_examples/cb-component-communication/e2e-spec.js b/public/docs/_examples/cb-component-communication/e2e-spec.js new file mode 100644 index 0000000000..84729e0cef --- /dev/null +++ b/public/docs/_examples/cb-component-communication/e2e-spec.js @@ -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 = '""'; + 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 + }); + +}); diff --git a/public/docs/_examples/cb-component-communication/ts/.gitignore b/public/docs/_examples/cb-component-communication/ts/.gitignore new file mode 100644 index 0000000000..2cb7d2a2e9 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/.gitignore @@ -0,0 +1 @@ +**/*.js 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 new file mode 100644 index 0000000000..6cfb5b76ed --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/app.component.html @@ -0,0 +1,44 @@ +

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
+ +
+ +
+Back to Top + +
+
+ +
+Back to Top + +
+
+ +
+Back to Top + +
+
+ +
+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 new file mode 100644 index 0000000000..a1d11ed518 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/app.component.ts @@ -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 { } \ No newline at end of file diff --git a/public/docs/_examples/cb-component-communication/ts/app/astronaut.component.ts b/public/docs/_examples/cb-component-communication/ts/app/astronaut.component.ts new file mode 100644 index 0000000000..686543be33 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/astronaut.component.ts @@ -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: ` +

+ {{astronaut}}: {{mission}} + +

+ ` +}) +export class AstronautComponent implements OnDestroy{ + @Input() astronaut: string; + mission = ""; + 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 \ No newline at end of file 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 new file mode 100644 index 0000000000..d1e2a00cc9 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/countdown-parent.component.ts @@ -0,0 +1,22 @@ +// #docregion +import {Component, ViewChild} from 'angular2/core'; +import {CountdownTimerComponent} from './countdown-timer.component'; + +@Component({ + selector:'countdown-parent', + template: ` +

Countdown to Liftoff

+ + + + `, + directives: [CountdownTimerComponent] +}) +export class CountdownParentComponent { + + @ViewChild(CountdownTimerComponent) + private _timerComponent:CountdownTimerComponent; + + start(){ this._timerComponent.start(); } + stop() { this._timerComponent.stop(); } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..aedbbf5906 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/countdown-timer.component.ts @@ -0,0 +1,33 @@ +// #docregion +import {Component, EventEmitter, OnInit, Output} from 'angular2/core'; + +@Component({ + selector:'countdown-timer', + template: '

{{message}}

' +}) +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`; + } +} \ No newline at end of file diff --git a/public/docs/_examples/cb-component-communication/ts/app/hero-child.component.ts b/public/docs/_examples/cb-component-communication/ts/app/hero-child.component.ts new file mode 100644 index 0000000000..748d543983 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/hero-child.component.ts @@ -0,0 +1,16 @@ +// #docregion +import {Component, Input} from 'angular2/core'; +import {Hero} from './hero'; + +@Component({ + selector: 'hero-child', + template: ` +

{{hero.name}} says:

+

I, {{hero.name}}, am at your service, {{masterName}}.

+ ` +}) +export class HeroChildComponent { + @Input() hero: Hero; + @Input('master') masterName: string; +} +// #enddocregion diff --git a/public/docs/_examples/cb-component-communication/ts/app/hero-parent.component.ts b/public/docs/_examples/cb-component-communication/ts/app/hero-parent.component.ts new file mode 100644 index 0000000000..17a2dcc93c --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/hero-parent.component.ts @@ -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: ` +

{{master}} controls {{heroes.length}} heroes

+ + + `, + directives: [HeroChildComponent] +}) +export class HeroParentComponent { + heroes = HEROES; + master: string = 'Master'; +} +// #enddocregion diff --git a/public/docs/_examples/cb-component-communication/ts/app/hero.ts b/public/docs/_examples/cb-component-communication/ts/app/hero.ts new file mode 100644 index 0000000000..5b99f17132 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/hero.ts @@ -0,0 +1,9 @@ +export interface Hero { + name: string; +} + +export const HEROES = [ + {name: 'Mr. IQ'}, + {name: 'Magneta'}, + {name: 'Bombasto'} +]; \ No newline at end of file diff --git a/public/docs/_examples/cb-component-communication/ts/app/main.ts b/public/docs/_examples/cb-component-communication/ts/app/main.ts new file mode 100644 index 0000000000..dc1879e9b5 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/main.ts @@ -0,0 +1,4 @@ +import {bootstrap} from 'angular2/platform/browser'; +import {AppComponent} from './app.component'; + +bootstrap(AppComponent); \ No newline at end of file diff --git a/public/docs/_examples/cb-component-communication/ts/app/mission.service.ts b/public/docs/_examples/cb-component-communication/ts/app/mission.service.ts new file mode 100644 index 0000000000..b651646b4b --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/mission.service.ts @@ -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(); + private _missionConfirmedSource = new Subject(); + + // 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 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 new file mode 100644 index 0000000000..2f0ac129c0 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/missioncontrol.component.ts @@ -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: ` +

Mission Control

+ + + +

History

+
    +
  • {{event}}
  • +
+ `, + 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 \ No newline at end of file diff --git a/public/docs/_examples/cb-component-communication/ts/app/name-child.component.ts b/public/docs/_examples/cb-component-communication/ts/app/name-child.component.ts new file mode 100644 index 0000000000..a02fa7949e --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/name-child.component.ts @@ -0,0 +1,20 @@ +// #docregion +import {Component, Input} from 'angular2/core'; + +@Component({ + selector: 'name-child', + template: ` +

"{{name}}"

+ ` +}) +export class NameChildComponent { + _name: string = ''; + + @Input() + set name(name: string) { + this._name = (name && name.trim()) || ''; + } + + get name() { return this._name; } +} +// #enddocregion diff --git a/public/docs/_examples/cb-component-communication/ts/app/name-parent.component.ts b/public/docs/_examples/cb-component-communication/ts/app/name-parent.component.ts new file mode 100644 index 0000000000..ec99f31675 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/name-parent.component.ts @@ -0,0 +1,19 @@ +// #docregion +import {Component} from 'angular2/core'; +import {NameChildComponent} from './name-child.component'; + +@Component({ + selector: 'name-parent', + template: ` +

Master controls {{names.length}} names

+ + + `, + directives: [NameChildComponent] +}) +export class NameParentComponent { + // Displays 'Mr. IQ', '', 'Bombasto' + names = ['Mr. IQ', ' ', ' Bombasto ']; +} +// #enddocregion diff --git a/public/docs/_examples/cb-component-communication/ts/app/version-child.component.ts b/public/docs/_examples/cb-component-communication/ts/app/version-child.component.ts new file mode 100644 index 0000000000..944c2888f6 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/version-child.component.ts @@ -0,0 +1,30 @@ +// #docregion +import {Component, Input, OnChanges, SimpleChange} from 'angular2/core'; + +@Component({ + selector: 'version-child', + template: ` +

Version {{major}}.{{minor}}

+

Change log:

+
    +
  • {{change}}
  • +
+ ` +}) +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 \ No newline at end of file diff --git a/public/docs/_examples/cb-component-communication/ts/app/version-parent.component.ts b/public/docs/_examples/cb-component-communication/ts/app/version-parent.component.ts new file mode 100644 index 0000000000..fa3cff767a --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/version-parent.component.ts @@ -0,0 +1,28 @@ +// #docregion +import {Component} from 'angular2/core'; +import {VersionChildComponent} from './version-child.component'; + +@Component({ + selector: 'version-parent', + template: ` +

Source code version

+ + + + `, + directives: [VersionChildComponent] +}) +export class VersionParentComponent { + major: number = 1; + minor: number = 23; + + newMinor() { + this.minor++; + } + + newMajor() { + this.major++; + this.minor = 0; + } +} +// #enddocregion diff --git a/public/docs/_examples/cb-component-communication/ts/app/voter.component.ts b/public/docs/_examples/cb-component-communication/ts/app/voter.component.ts new file mode 100644 index 0000000000..1c5fd16e25 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/voter.component.ts @@ -0,0 +1,22 @@ +// #docregion +import {Component, EventEmitter, Input, Output} from 'angular2/core'; + +@Component({ + selector: 'my-voter', + template: ` +

{{name}}

+ + + ` +}) +export class VoterComponent { + @Input() name: string; + @Output() onVoted = new EventEmitter(); + voted = false; + + vote(agreed:boolean){ + this.onVoted.emit(agreed); + this.voted = true; + } +} +// #enddocregion \ No newline at end of file diff --git a/public/docs/_examples/cb-component-communication/ts/app/votetaker.component.ts b/public/docs/_examples/cb-component-communication/ts/app/votetaker.component.ts new file mode 100644 index 0000000000..02aeda9fd9 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/app/votetaker.component.ts @@ -0,0 +1,26 @@ +// #docregion +import {Component} from 'angular2/core'; +import {VoterComponent} from './voter.component'; + +@Component({ + selector: 'vote-taker', + template: ` +

Should mankind colonize the Universe?

+

Agree: {{agreed}}, Disagree: {{disagreed}}

+ + + `, + directives: [VoterComponent] +}) +export class VoteTakerComponent { + agreed = 0; + disagreed = 0; + voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'] + + onVoted(agreed: boolean) { + agreed ? this.agreed++ : this.disagreed++; + } +} +// #enddocregion \ No newline at end of file diff --git a/public/docs/_examples/cb-component-communication/ts/example-config.json b/public/docs/_examples/cb-component-communication/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/cb-component-communication/ts/index.html b/public/docs/_examples/cb-component-communication/ts/index.html new file mode 100644 index 0000000000..549f836c4c --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/index.html @@ -0,0 +1,36 @@ + + + + + Passing information from parent to child + + + + + + + + + + + + + + + loading... + + + diff --git a/public/docs/_examples/cb-component-communication/ts/plnkr.json b/public/docs/_examples/cb-component-communication/ts/plnkr.json new file mode 100644 index 0000000000..3c83b2d0f9 --- /dev/null +++ b/public/docs/_examples/cb-component-communication/ts/plnkr.json @@ -0,0 +1,8 @@ +{ + "description": "Component Communication Cookbook samples", + "files":[ + "!**/*.d.ts", + "!**/*.js" + ], + "tags":["cookbook", "component"] +} \ No newline at end of file diff --git a/public/docs/_includes/sidenav/_secondary.jade b/public/docs/_includes/sidenav/_secondary.jade index 0a5bb47506..f43406def7 100644 --- a/public/docs/_includes/sidenav/_secondary.jade +++ b/public/docs/_includes/sidenav/_secondary.jade @@ -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; diff --git a/public/docs/ts/latest/_data.json b/public/docs/ts/latest/_data.json index f9ea1d922d..87fcd6d9d3 100644 --- a/public/docs/ts/latest/_data.json +++ b/public/docs/ts/latest/_data.json @@ -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", diff --git a/public/docs/ts/latest/cookbooks/_data.json b/public/docs/ts/latest/cookbooks/_data.json new file mode 100644 index 0000000000..eff6f5f019 --- /dev/null +++ b/public/docs/ts/latest/cookbooks/_data.json @@ -0,0 +1,11 @@ +{ + "_listtype": "alpha", + + "index": { + "title": "Cookbooks" + }, + + "component-communication": { + "title": "Component Communication" + } +} \ No newline at end of file diff --git a/public/docs/ts/latest/cookbooks/component-communication.jade b/public/docs/ts/latest/cookbooks/component-communication.jade new file mode 100644 index 0000000000..b623cbd02f --- /dev/null +++ b/public/docs/ts/latest/cookbooks/component-communication.jade @@ -0,0 +1,257 @@ +include ../../../../_includes/_util-fns + + +: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. + + +: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 + +: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 + +: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 + +: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 + +: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 + +: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 + +: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) diff --git a/public/docs/ts/latest/cookbooks/index.jade b/public/docs/ts/latest/cookbooks/index.jade new file mode 100644 index 0000000000..87fba289f6 --- /dev/null +++ b/public/docs/ts/latest/cookbooks/index.jade @@ -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. diff --git a/public/resources/css/layout/_layout.scss b/public/resources/css/layout/_layout.scss index d5d280be14..3f0babea9a 100644 --- a/public/resources/css/layout/_layout.scss +++ b/public/resources/css/layout/_layout.scss @@ -224,4 +224,9 @@ .l-layer-10 { z-index: $layer-10; -} \ No newline at end of file +} + +/* + * Other + */ +.to-top {margin-top: $unit * 8; display: block;} diff --git a/public/resources/images/cookbooks/component-communication/bidirectional-service.gif b/public/resources/images/cookbooks/component-communication/bidirectional-service.gif new file mode 100644 index 0000000000..a0d8c07dd1 Binary files /dev/null and b/public/resources/images/cookbooks/component-communication/bidirectional-service.gif differ diff --git a/public/resources/images/cookbooks/component-communication/child-to-parent.gif b/public/resources/images/cookbooks/component-communication/child-to-parent.gif new file mode 100644 index 0000000000..05225d09a5 Binary files /dev/null and b/public/resources/images/cookbooks/component-communication/child-to-parent.gif differ diff --git a/public/resources/images/cookbooks/component-communication/contentchildren.png b/public/resources/images/cookbooks/component-communication/contentchildren.png new file mode 100644 index 0000000000..097feb688c Binary files /dev/null and b/public/resources/images/cookbooks/component-communication/contentchildren.png differ diff --git a/public/resources/images/cookbooks/component-communication/countdown-timer-anim.gif b/public/resources/images/cookbooks/component-communication/countdown-timer-anim.gif new file mode 100644 index 0000000000..31df3ec69f Binary files /dev/null and b/public/resources/images/cookbooks/component-communication/countdown-timer-anim.gif differ diff --git a/public/resources/images/cookbooks/component-communication/parent-to-child-on-changes.gif b/public/resources/images/cookbooks/component-communication/parent-to-child-on-changes.gif new file mode 100644 index 0000000000..4ff7657f5b Binary files /dev/null and b/public/resources/images/cookbooks/component-communication/parent-to-child-on-changes.gif differ diff --git a/public/resources/images/cookbooks/component-communication/parent-to-child.png b/public/resources/images/cookbooks/component-communication/parent-to-child.png new file mode 100644 index 0000000000..03b9a19c4a Binary files /dev/null and b/public/resources/images/cookbooks/component-communication/parent-to-child.png differ diff --git a/public/resources/images/cookbooks/component-communication/setter.png b/public/resources/images/cookbooks/component-communication/setter.png new file mode 100644 index 0000000000..4edc5041b0 Binary files /dev/null and b/public/resources/images/cookbooks/component-communication/setter.png differ diff --git a/public/resources/images/cookbooks/component-communication/unrelated-service.gif b/public/resources/images/cookbooks/component-communication/unrelated-service.gif new file mode 100644 index 0000000000..cfa92270e2 Binary files /dev/null and b/public/resources/images/cookbooks/component-communication/unrelated-service.gif differ