5 lines
42 KiB
JSON
5 lines
42 KiB
JSON
{
|
|
"id": "guide/component-interaction",
|
|
"title": "Component interaction",
|
|
"contents": "\n\n\n<div class=\"github-links\">\n <a href=\"https://github.com/angular/angular/edit/master/aio/content/guide/component-interaction.md?message=docs%3A%20describe%20your%20change...\" aria-label=\"Suggest Edits\" title=\"Suggest Edits\"><i class=\"material-icons\" aria-hidden=\"true\" role=\"img\">mode_edit</i></a>\n</div>\n\n\n<div class=\"content\">\n <h1 id=\"component-interaction\">Component interaction<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#component-interaction\"><i class=\"material-icons\">link</i></a></h1>\n<a id=\"top\"></a>\n<p>This cookbook contains recipes for common component communication scenarios\nin which two or more components share information.\n<a id=\"toc\"></a></p>\n<!--\n\n# Contents\n\n* [Pass data from parent to child with input binding](guide/component-interaction#parent-to-child)\n* [Intercept input property changes with a setter](guide/component-interaction#parent-to-child-setter)\n* [Intercept input property changes with `ngOnChanges()`](guide/component-interaction#parent-to-child-on-changes)\n* [Parent calls an `@ViewChild()`](guide/component-interaction#parent-to-view-child)\n* [Parent and children communicate via a service](guide/component-interaction#bidirectional-service)\n\n-->\n<p><strong>See the <live-example name=\"component-interaction\"></live-example></strong>.</p>\n<a id=\"parent-to-child\"></a>\n<h2 id=\"pass-data-from-parent-to-child-with-input-binding\">Pass data from parent to child with input binding<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#pass-data-from-parent-to-child-with-input-binding\"><i class=\"material-icons\">link</i></a></h2>\n<p><code>HeroChildComponent</code> has two <strong><em>input properties</em></strong>,\ntypically adorned with <a href=\"guide/inputs-outputs#input\">@Input() decorator</a>.</p>\n<code-example path=\"component-interaction/src/app/hero-child.component.ts\" header=\"component-interaction/src/app/hero-child.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a>, <a href=\"api/core/Input\" class=\"code-anchor\">Input</a> } from '@angular/core';\n\nimport { Hero } from './hero';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-hero-child',\n template: `\n <h3>{{hero.name}} says:</h3>\n <p>I, {{hero.name}}, am at your service, {{masterName}}.</p>\n `\n})\nexport class HeroChildComponent {\n @<a href=\"api/core/Input\" class=\"code-anchor\">Input</a>() hero: Hero;\n @<a href=\"api/core/Input\" class=\"code-anchor\">Input</a>('master') masterName: string; // tslint:disable-line: no-input-rename\n}\n\n</code-example>\n<p>The second <code>@<a href=\"api/core/Input\" class=\"code-anchor\">Input</a></code> aliases the child component property name <code>masterName</code> as <code>'master'</code>.</p>\n<p>The <code>HeroParentComponent</code> nests the child <code>HeroChildComponent</code> inside an <code>*<a href=\"api/common/NgForOf\" class=\"code-anchor\">ngFor</a></code> repeater,\nbinding its <code>master</code> string property to the child's <code>master</code> alias,\nand each iteration's <code>hero</code> instance to the child's <code>hero</code> property.</p>\n<code-example path=\"component-interaction/src/app/hero-parent.component.ts\" header=\"component-interaction/src/app/hero-parent.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a> } from '@angular/core';\n\nimport { HEROES } from './hero';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-hero-parent',\n template: `\n <h2>{{master}} controls {{heroes.length}} heroes</h2>\n <app-hero-child *<a href=\"api/common/NgForOf\" class=\"code-anchor\">ngFor</a>=\"let hero of heroes\"\n [hero]=\"hero\"\n [master]=\"master\">\n </app-hero-child>\n `\n})\nexport class HeroParentComponent {\n heroes = HEROES;\n master = 'Master';\n}\n\n</code-example>\n<p>The running application displays three heroes:</p>\n<div class=\"lightbox\">\n <img src=\"generated/images/guide/component-interaction/parent-to-child.png\" alt=\"Parent-to-child\" width=\"272\" height=\"267\">\n</div>\n<h3 class=\"no-toc\" id=\"test-it\">Test it<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#test-it\"><i class=\"material-icons\">link</i></a></h3>\n<p>E2E test that all children were instantiated and displayed as expected:</p>\n<code-example path=\"component-interaction/e2e/src/app.e2e-spec.ts\" region=\"parent-to-child\" header=\"component-interaction/e2e/src/app.e2e-spec.ts\">\n// ...\nconst heroNames = ['Dr IQ', 'Magneta', 'Bombasto'];\nconst masterName = 'Master';\n\nit('should pass properties to children properly', async () => {\n const parent = element(by.tagName('app-hero-parent'));\n const heroes = parent.all(by.tagName('app-hero-child'));\n\n for (let i = 0; i < heroNames.length; i++) {\n const childTitle = await heroes.get(i).element(by.tagName('h3')).getText();\n const childDetail = await heroes.get(i).element(by.tagName('p')).getText();\n expect(childTitle).toEqual(heroNames[i] + ' says:');\n expect(childDetail).toContain(masterName);\n }\n});\n// ...\n\n</code-example>\n<p><a href=\"guide/component-interaction#top\">Back to top</a></p>\n<a id=\"parent-to-child-setter\"></a>\n<h2 id=\"intercept-input-property-changes-with-a-setter\">Intercept input property changes with a setter<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#intercept-input-property-changes-with-a-setter\"><i class=\"material-icons\">link</i></a></h2>\n<p>Use an input property setter to intercept and act upon a value from the parent.</p>\n<p>The setter of the <code>name</code> input property in the child <code>NameChildComponent</code>\ntrims the whitespace from a name and replaces an empty value with default text.</p>\n<code-example path=\"component-interaction/src/app/name-child.component.ts\" header=\"component-interaction/src/app/name-child.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a>, <a href=\"api/core/Input\" class=\"code-anchor\">Input</a> } from '@angular/core';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-name-child',\n template: '<h3>\"{{name}}\"</h3>'\n})\nexport class NameChildComponent {\n @<a href=\"api/core/Input\" class=\"code-anchor\">Input</a>()\n get name(): string { return this._name; }\n set name(name: string) {\n this._name = (name && name.trim()) || '<no name set>';\n }\n private _name = '';\n}\n\n</code-example>\n<p>Here's the <code>NameParentComponent</code> demonstrating name variations including a name with all spaces:</p>\n<code-example path=\"component-interaction/src/app/name-parent.component.ts\" header=\"component-interaction/src/app/name-parent.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a> } from '@angular/core';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-name-parent',\n template: `\n <h2>Master controls {{names.length}} names</h2>\n <app-name-child *<a href=\"api/common/NgForOf\" class=\"code-anchor\">ngFor</a>=\"let name of names\" [name]=\"name\"></app-name-child>\n `\n})\nexport class NameParentComponent {\n // Displays 'Dr IQ', '<no name set>', 'Bombasto'\n names = ['Dr IQ', ' ', ' Bombasto '];\n}\n\n</code-example>\n<div class=\"lightbox\">\n <img src=\"generated/images/guide/component-interaction/setter.png\" alt=\"Parent-to-child-setter\" width=\"332\" height=\"190\">\n</div>\n<h3 class=\"no-toc\" id=\"test-it-1\">Test it<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#test-it-1\"><i class=\"material-icons\">link</i></a></h3>\n<p>E2E tests of input property setter with empty and non-empty names:</p>\n<code-example path=\"component-interaction/e2e/src/app.e2e-spec.ts\" region=\"parent-to-child-setter\" header=\"component-interaction/e2e/src/app.e2e-spec.ts\">\n// ...\nit('should display trimmed, non-empty names', async () => {\n const nonEmptyNameIndex = 0;\n const nonEmptyName = '\"Dr IQ\"';\n const parent = element(by.tagName('app-name-parent'));\n const hero = parent.all(by.tagName('app-name-child')).get(nonEmptyNameIndex);\n\n const displayName = await hero.element(by.tagName('h3')).getText();\n expect(displayName).toEqual(nonEmptyName);\n});\n\nit('should replace empty name with default name', async () => {\n const emptyNameIndex = 1;\n const defaultName = '\"<no name set>\"';\n const parent = element(by.tagName('app-name-parent'));\n const hero = parent.all(by.tagName('app-name-child')).get(emptyNameIndex);\n\n const displayName = await hero.element(by.tagName('h3')).getText();\n expect(displayName).toEqual(defaultName);\n});\n// ...\n\n</code-example>\n<p><a href=\"guide/component-interaction#top\">Back to top</a></p>\n<a id=\"parent-to-child-on-changes\"></a>\n<h2 id=\"intercept-input-property-changes-with-ngonchanges\">Intercept input property changes with <em>ngOnChanges()</em><a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#intercept-input-property-changes-with-ngonchanges\"><i class=\"material-icons\">link</i></a></h2>\n<p>Detect and act upon changes to input property values with the <code>ngOnChanges()</code> method of the <code><a href=\"api/core/OnChanges\" class=\"code-anchor\">OnChanges</a></code> lifecycle hook interface.</p>\n<div class=\"alert is-helpful\">\n<p>You may prefer this approach to the property setter when watching multiple, interacting input properties.</p>\n<p>Learn about <code>ngOnChanges()</code> in the <a href=\"guide/lifecycle-hooks\">Lifecycle Hooks</a> chapter.</p>\n</div>\n<p>This <code>VersionChildComponent</code> detects changes to the <code>major</code> and <code>minor</code> input properties and composes a log message reporting these changes:</p>\n<code-example path=\"component-interaction/src/app/version-child.component.ts\" header=\"component-interaction/src/app/version-child.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a>, <a href=\"api/core/Input\" class=\"code-anchor\">Input</a>, <a href=\"api/core/OnChanges\" class=\"code-anchor\">OnChanges</a>, <a href=\"api/core/SimpleChanges\" class=\"code-anchor\">SimpleChanges</a> } from '@angular/core';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-version-child',\n template: `\n <h3><a href=\"api/core/Version\" class=\"code-anchor\">Version</a> {{major}}.{{minor}}</h3>\n <h4>Change log:</h4>\n <ul>\n <li *<a href=\"api/common/NgForOf\" class=\"code-anchor\">ngFor</a>=\"let change of changeLog\">{{change}}</li>\n </ul>\n `\n})\nexport class VersionChildComponent implements <a href=\"api/core/OnChanges\" class=\"code-anchor\">OnChanges</a> {\n @<a href=\"api/core/Input\" class=\"code-anchor\">Input</a>() major: number;\n @<a href=\"api/core/Input\" class=\"code-anchor\">Input</a>() minor: number;\n changeLog: string[] = [];\n\n ngOnChanges(changes: <a href=\"api/core/SimpleChanges\" class=\"code-anchor\">SimpleChanges</a>) {\n const log: string[] = [];\n for (const propName in changes) {\n const changedProp = changes[propName];\n const to = JSON.stringify(changedProp.currentValue);\n if (changedProp.isFirstChange()) {\n log.push(`Initial value of ${propName} set to ${to}`);\n } else {\n const from = JSON.stringify(changedProp.previousValue);\n log.push(`${propName} changed from ${from} to ${to}`);\n }\n }\n this.changeLog.push(log.join(', '));\n }\n}\n\n</code-example>\n<p>The <code>VersionParentComponent</code> supplies the <code>minor</code> and <code>major</code> values and binds buttons to methods that change them.</p>\n<code-example path=\"component-interaction/src/app/version-parent.component.ts\" header=\"component-interaction/src/app/version-parent.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a> } from '@angular/core';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-version-parent',\n template: `\n <h2>Source code version</h2>\n <button (click)=\"newMinor()\">New minor version</button>\n <button (click)=\"newMajor()\">New major version</button>\n <app-version-child [major]=\"major\" [minor]=\"minor\"></app-version-child>\n `\n})\nexport class VersionParentComponent {\n major = 1;\n minor = 23;\n\n newMinor() {\n this.minor++;\n }\n\n newMajor() {\n this.major++;\n this.minor = 0;\n }\n}\n\n</code-example>\n<p>Here's the output of a button-pushing sequence:</p>\n<div class=\"lightbox\">\n <img src=\"generated/images/guide/component-interaction/parent-to-child-on-changes.gif\" alt=\"Parent-to-child-onchanges\" width=\"481\" height=\"264\">\n</div>\n<h3 class=\"no-toc\" id=\"test-it-2\">Test it<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#test-it-2\"><i class=\"material-icons\">link</i></a></h3>\n<p>Test that <strong><em>both</em></strong> input properties are set initially and that button clicks trigger\nthe expected <code>ngOnChanges</code> calls and values:</p>\n<code-example path=\"component-interaction/e2e/src/app.e2e-spec.ts\" region=\"parent-to-child-onchanges\" header=\"component-interaction/e2e/src/app.e2e-spec.ts\">\n// ...\n// Test must all execute in this exact order\nit('should set expected initial values', async () => {\n const actual = await getActual();\n\n const initialLabel = '<a href=\"api/core/Version\" class=\"code-anchor\">Version</a> 1.23';\n const initialLog = 'Initial value of major set to 1, Initial value of minor set to 23';\n\n expect(actual.label).toBe(initialLabel);\n expect(actual.count).toBe(1);\n expect(await actual.logs.get(0).getText()).toBe(initialLog);\n});\n\nit('should set expected values after clicking \\'Minor\\' twice', async () => {\n const repoTag = element(by.tagName('app-version-parent'));\n const newMinorButton = repoTag.all(by.tagName('button')).get(0);\n\n await newMinorButton.click();\n await newMinorButton.click();\n\n const actual = await getActual();\n\n const labelAfter2Minor = '<a href=\"api/core/Version\" class=\"code-anchor\">Version</a> 1.25';\n const logAfter2Minor = 'minor changed from 24 to 25';\n\n expect(actual.label).toBe(labelAfter2Minor);\n expect(actual.count).toBe(3);\n expect(await actual.logs.get(2).getText()).toBe(logAfter2Minor);\n});\n\nit('should set expected values after clicking \\'Major\\' once', async () => {\n const repoTag = element(by.tagName('app-version-parent'));\n const newMajorButton = repoTag.all(by.tagName('button')).get(1);\n\n await newMajorButton.click();\n const actual = await getActual();\n\n const labelAfterMajor = '<a href=\"api/core/Version\" class=\"code-anchor\">Version</a> 2.0';\n const logAfterMajor = 'major changed from 1 to 2, minor changed from 23 to 0';\n\n expect(actual.label).toBe(labelAfterMajor);\n expect(actual.count).toBe(2);\n expect(await actual.logs.get(1).getText()).toBe(logAfterMajor);\n});\n\nasync function getActual() {\n const versionTag = element(by.tagName('app-version-child'));\n const label = await versionTag.element(by.tagName('h3')).getText();\n const ul = versionTag.element((by.tagName('ul')));\n const logs = ul.all(by.tagName('li'));\n\n return {\n label,\n logs,\n count: await logs.count(),\n };\n}\n// ...\n\n</code-example>\n<p><a href=\"guide/component-interaction#top\">Back to top</a></p>\n<a id=\"child-to-parent\"></a>\n<h2 id=\"parent-listens-for-child-event\">Parent listens for child event<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#parent-listens-for-child-event\"><i class=\"material-icons\">link</i></a></h2>\n<p>The child component exposes an <code><a href=\"api/core/EventEmitter\" class=\"code-anchor\">EventEmitter</a></code> property with which it <code>emits</code> events when something happens.\nThe parent binds to that event property and reacts to those events.</p>\n<p>The child's <code><a href=\"api/core/EventEmitter\" class=\"code-anchor\">EventEmitter</a></code> property is an <strong><em>output property</em></strong>,\ntypically adorned with an <a href=\"guide/inputs-outputs#output\">@Output() decorator</a>\nas seen in this <code>VoterComponent</code>:</p>\n<code-example path=\"component-interaction/src/app/voter.component.ts\" header=\"component-interaction/src/app/voter.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a>, <a href=\"api/core/EventEmitter\" class=\"code-anchor\">EventEmitter</a>, <a href=\"api/core/Input\" class=\"code-anchor\">Input</a>, <a href=\"api/core/Output\" class=\"code-anchor\">Output</a> } from '@angular/core';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-voter',\n template: `\n <h4>{{name}}</h4>\n <button (click)=\"vote(true)\" [disabled]=\"didVote\">Agree</button>\n <button (click)=\"vote(false)\" [disabled]=\"didVote\">Disagree</button>\n `\n})\nexport class VoterComponent {\n @<a href=\"api/core/Input\" class=\"code-anchor\">Input</a>() name: string;\n @<a href=\"api/core/Output\" class=\"code-anchor\">Output</a>() voted = new <a href=\"api/core/EventEmitter\" class=\"code-anchor\">EventEmitter</a><boolean>();\n didVote = false;\n\n vote(agreed: boolean) {\n this.voted.emit(agreed);\n this.didVote = true;\n }\n}\n\n</code-example>\n<p>Clicking a button triggers emission of a <code>true</code> or <code>false</code>, the boolean <em>payload</em>.</p>\n<p>The parent <code>VoteTakerComponent</code> binds an event handler called <code>onVoted()</code> that responds to the child event\npayload <code>$event</code> and updates a counter.</p>\n<code-example path=\"component-interaction/src/app/votetaker.component.ts\" header=\"component-interaction/src/app/votetaker.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a> } from '@angular/core';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-vote-taker',\n template: `\n <h2>Should mankind colonize the Universe?</h2>\n <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>\n <app-voter *<a href=\"api/common/NgForOf\" class=\"code-anchor\">ngFor</a>=\"let voter of voters\"\n [name]=\"voter\"\n (voted)=\"onVoted($event)\">\n </app-voter>\n `\n})\nexport class VoteTakerComponent {\n agreed = 0;\n disagreed = 0;\n voters = ['Narco', 'Celeritas', 'Bombasto'];\n\n onVoted(agreed: boolean) {\n agreed ? this.agreed++ : this.disagreed++;\n }\n}\n\n</code-example>\n<p>The framework passes the event argument—represented by <code>$event</code>—to the handler method,\nand the method processes it:</p>\n<div class=\"lightbox\">\n <img src=\"generated/images/guide/component-interaction/child-to-parent.gif\" alt=\"Child-to-parent\" width=\"418\" height=\"342\">\n</div>\n<h3 class=\"no-toc\" id=\"test-it-3\">Test it<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#test-it-3\"><i class=\"material-icons\">link</i></a></h3>\n<p>Test that clicking the <em>Agree</em> and <em>Disagree</em> buttons update the appropriate counters:</p>\n<code-example path=\"component-interaction/e2e/src/app.e2e-spec.ts\" region=\"child-to-parent\" header=\"component-interaction/e2e/src/app.e2e-spec.ts\">\n// ...\nit('should not emit the event initially', async () => {\n const voteLabel = element(by.tagName('app-vote-taker')).element(by.tagName('h3'));\n expect(await voteLabel.getText()).toBe('Agree: 0, Disagree: 0');\n});\n\nit('should process Agree vote', async () => {\n const voteLabel = element(by.tagName('app-vote-taker')).element(by.tagName('h3'));\n const agreeButton1 = element.all(by.tagName('app-voter')).get(0)\n .all(by.tagName('button')).get(0);\n\n await agreeButton1.click();\n\n expect(await voteLabel.getText()).toBe('Agree: 1, Disagree: 0');\n});\n\nit('should process Disagree vote', async () => {\n const voteLabel = element(by.tagName('app-vote-taker')).element(by.tagName('h3'));\n const agreeButton1 = element.all(by.tagName('app-voter')).get(1)\n .all(by.tagName('button')).get(1);\n\n await agreeButton1.click();\n\n expect(await voteLabel.getText()).toBe('Agree: 0, Disagree: 1');\n});\n// ...\n\n</code-example>\n<p><a href=\"guide/component-interaction#top\">Back to top</a></p>\n<h2 id=\"parent-interacts-with-child-via-local-variable\">Parent interacts with child via <em>local variable</em><a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#parent-interacts-with-child-via-local-variable\"><i class=\"material-icons\">link</i></a></h2>\n<p>A parent component cannot use data binding to read child properties\nor invoke child methods. You can do both\nby creating a template reference variable for the child element\nand then reference that variable <em>within the parent template</em>\nas seen in the following example.</p>\n<a id=\"countdown-timer-example\"></a>\n<p>The following is a child <code>CountdownTimerComponent</code> that repeatedly counts down to zero and launches a rocket.\nIt has <code>start</code> and <code>stop</code> methods that control the clock and it displays a\ncountdown status message in its own template.</p>\n<code-example path=\"component-interaction/src/app/countdown-timer.component.ts\" header=\"component-interaction/src/app/countdown-timer.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a>, <a href=\"api/core/OnDestroy\" class=\"code-anchor\">OnDestroy</a> } from '@angular/core';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-countdown-timer',\n template: '<p>{{message}}</p>'\n})\nexport class CountdownTimerComponent implements <a href=\"api/core/OnDestroy\" class=\"code-anchor\">OnDestroy</a> {\n\n intervalId = 0;\n message = '';\n seconds = 11;\n\n ngOnDestroy() { this.clearTimer(); }\n\n start() { this.countDown(); }\n stop() {\n this.clearTimer();\n this.message = `Holding at T-${this.seconds} seconds`;\n }\n\n private clearTimer() { clearInterval(this.intervalId); }\n\n private countDown() {\n this.clearTimer();\n this.intervalId = window.setInterval(() => {\n this.seconds -= 1;\n if (this.seconds === 0) {\n this.message = 'Blast off!';\n } else {\n if (this.seconds < 0) { this.seconds = 10; } // reset\n this.message = `T-${this.seconds} seconds and counting`;\n }\n }, 1000);\n }\n}\n\n\n</code-example>\n<p>The <code>CountdownLocalVarParentComponent</code> that hosts the timer component is as follows:</p>\n<code-example path=\"component-interaction/src/app/countdown-parent.component.ts\" region=\"lv\" header=\"component-interaction/src/app/countdown-parent.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a> } from '@angular/core';\nimport { CountdownTimerComponent } from './countdown-timer.component';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-countdown-parent-lv',\n template: `\n <h3>Countdown to Liftoff (via local variable)</h3>\n <button (click)=\"timer.start()\">Start</button>\n <button (click)=\"timer.stop()\">Stop</button>\n <div class=\"seconds\">{{timer.seconds}}</div>\n <app-countdown-timer #timer></app-countdown-timer>\n `,\n styleUrls: ['../assets/demo.css']\n})\nexport class CountdownLocalVarParentComponent { }\n\n</code-example>\n<p>The parent component cannot data bind to the child's\n<code>start</code> and <code>stop</code> methods nor to its <code>seconds</code> property.</p>\n<p>You can place a local variable, <code>#timer</code>, on the tag <code><countdown-timer></code> representing the child component.\nThat gives you a reference to the child component and the ability to access\n<em>any of its properties or methods</em> from within the parent template.</p>\n<p>This example wires parent buttons to the child's <code>start</code> and <code>stop</code> and\nuses interpolation to display the child's <code>seconds</code> property.</p>\n<p>Here we see the parent and child working together.</p>\n<div class=\"lightbox\">\n <img src=\"generated/images/guide/component-interaction/countdown-timer-anim.gif\" alt=\"countdown timer\" width=\"212\" height=\"172\">\n</div>\n<a id=\"countdown-tests\"></a>\n<h3 class=\"no-toc\" id=\"test-it-4\">Test it<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#test-it-4\"><i class=\"material-icons\">link</i></a></h3>\n<p>Test that the seconds displayed in the parent template\nmatch the seconds displayed in the child's status message.\nTest also that clicking the <em>Stop</em> button pauses the countdown timer:</p>\n<code-example path=\"component-interaction/e2e/src/app.e2e-spec.ts\" region=\"countdown-timer-tests\" header=\"component-interaction/e2e/src/app.e2e-spec.ts\">\n// ...\n// The tests <a href=\"api/animations/trigger\" class=\"code-anchor\">trigger</a> periodic asynchronous operations (via `setInterval()`), which will prevent\n// the app from stabilizing. See https://angular.io/api/core/<a href=\"api/core/ApplicationRef\" class=\"code-anchor\">ApplicationRef</a>#is-stable-examples\n// for more details.\n// To allow the tests to complete, we will disable automatically waiting for the Angular app to\n// stabilize.\nbeforeEach(() => browser.waitForAngularEnabled(false));\nafterEach(() => browser.waitForAngularEnabled(true));\n\nit('timer and parent seconds should match', async () => {\n const parent = element(by.tagName(parentTag));\n const startButton = parent.element(by.buttonText('Start'));\n const seconds = parent.element(by.className('seconds'));\n const timer = parent.element(by.tagName('app-countdown-timer'));\n\n await startButton.click();\n\n // Wait for `<app-countdown-timer>` to be populated with any text.\n await browser.wait(() => timer.getText(), 2000);\n\n expect(await timer.getText()).toContain(await seconds.getText());\n});\n\nit('should stop the countdown', async () => {\n const parent = element(by.tagName(parentTag));\n const startButton = parent.element(by.buttonText('Start'));\n const stopButton = parent.element(by.buttonText('Stop'));\n const timer = parent.element(by.tagName('app-countdown-timer'));\n\n await startButton.click();\n expect(await timer.getText()).not.toContain('Holding');\n\n await stopButton.click();\n expect(await timer.getText()).toContain('Holding');\n});\n// ...\n\n</code-example>\n<p><a href=\"guide/component-interaction#top\">Back to top</a></p>\n<a id=\"parent-to-view-child\"></a>\n<h2 id=\"parent-calls-an-viewchild\">Parent calls an <em>@ViewChild()</em><a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#parent-calls-an-viewchild\"><i class=\"material-icons\">link</i></a></h2>\n<p>The <em>local variable</em> approach is simple and easy. But it is limited because\nthe parent-child wiring must be done entirely within the parent template.\nThe parent component <em>itself</em> has no access to the child.</p>\n<p>You can't use the <em>local variable</em> technique if an instance of the parent component <em>class</em>\nmust read or write child component values or must call child component methods.</p>\n<p>When the parent component <em>class</em> requires that kind of access,\n<strong><em>inject</em></strong> the child component into the parent as a <em>ViewChild</em>.</p>\n<p>The following example illustrates this technique with the same\n<a href=\"guide/component-interaction#countdown-timer-example\">Countdown Timer</a> example.\nNeither its appearance nor its behavior will change.\nThe child <a href=\"guide/component-interaction#countdown-timer-example\">CountdownTimerComponent</a> is the same as well.</p>\n<div class=\"alert is-helpful\">\n<p>The switch from the <em>local variable</em> to the <em>ViewChild</em> technique\nis solely for the purpose of demonstration.</p>\n</div>\n<p>Here is the parent, <code>CountdownViewChildParentComponent</code>:</p>\n<code-example path=\"component-interaction/src/app/countdown-parent.component.ts\" region=\"vc\" header=\"component-interaction/src/app/countdown-parent.component.ts\">\nimport { <a href=\"api/core/AfterViewInit\" class=\"code-anchor\">AfterViewInit</a>, <a href=\"api/core/ViewChild\" class=\"code-anchor\">ViewChild</a> } from '@angular/core';\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a> } from '@angular/core';\nimport { CountdownTimerComponent } from './countdown-timer.component';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-countdown-parent-vc',\n template: `\n <h3>Countdown to Liftoff (via <a href=\"api/core/ViewChild\" class=\"code-anchor\">ViewChild</a>)</h3>\n <button (click)=\"start()\">Start</button>\n <button (click)=\"stop()\">Stop</button>\n <div class=\"seconds\">{{ seconds() }}</div>\n <app-countdown-timer></app-countdown-timer>\n `,\n styleUrls: ['../assets/demo.css']\n})\nexport class CountdownViewChildParentComponent implements <a href=\"api/core/AfterViewInit\" class=\"code-anchor\">AfterViewInit</a> {\n\n @<a href=\"api/core/ViewChild\" class=\"code-anchor\">ViewChild</a>(CountdownTimerComponent)\n private timerComponent: CountdownTimerComponent;\n\n seconds() { return 0; }\n\n ngAfterViewInit() {\n // Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...\n // but wait a <a href=\"api/core/testing/tick\" class=\"code-anchor\">tick</a> first to avoid one-time devMode\n // unidirectional-data-flow-violation error\n setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);\n }\n\n start() { this.timerComponent.start(); }\n stop() { this.timerComponent.stop(); }\n}\n\n</code-example>\n<p>It takes a bit more work to get the child view into the parent component <em>class</em>.</p>\n<p>First, you have to import references to the <code><a href=\"api/core/ViewChild\" class=\"code-anchor\">ViewChild</a></code> decorator and the <code><a href=\"api/core/AfterViewInit\" class=\"code-anchor\">AfterViewInit</a></code> lifecycle hook.</p>\n<p>Next, inject the child <code>CountdownTimerComponent</code> into the private <code>timerComponent</code> property\nvia the <code>@<a href=\"api/core/ViewChild\" class=\"code-anchor\">ViewChild</a></code> property decoration.</p>\n<p>The <code>#timer</code> local variable is gone from the component metadata.\nInstead, bind the buttons to the parent component's own <code>start</code> and <code>stop</code> methods and\npresent the ticking seconds in an interpolation around the parent component's <code>seconds</code> method.</p>\n<p>These methods access the injected timer component directly.</p>\n<p>The <code>ngAfterViewInit()</code> lifecycle hook is an important wrinkle.\nThe timer component isn't available until <em>after</em> Angular displays the parent view.\nSo it displays <code>0</code> seconds initially.</p>\n<p>Then Angular calls the <code>ngAfterViewInit</code> lifecycle hook at which time it is <em>too late</em>\nto update the parent view's display of the countdown seconds.\nAngular's unidirectional data flow rule prevents updating the parent view's\nin the same cycle. The app has to <em>wait one turn</em> before it can display the seconds.</p>\n<p>Use <code>setTimeout()</code> to wait one tick and then revise the <code>seconds()</code> method so\nthat it takes future values from the timer component.</p>\n<h3 class=\"no-toc\" id=\"test-it-5\">Test it<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#test-it-5\"><i class=\"material-icons\">link</i></a></h3>\n<p>Use <a href=\"guide/component-interaction#countdown-tests\">the same countdown timer tests</a> as before.</p>\n<p><a href=\"guide/component-interaction#top\">Back to top</a></p>\n<a id=\"bidirectional-service\"></a>\n<h2 id=\"parent-and-children-communicate-via-a-service\">Parent and children communicate via a service<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#parent-and-children-communicate-via-a-service\"><i class=\"material-icons\">link</i></a></h2>\n<p>A parent component and its children share a service whose interface enables bi-directional communication\n<em>within the family</em>.</p>\n<p>The scope of the service instance is the parent component and its children.\nComponents outside this component subtree have no access to the service or their communications.</p>\n<p>This <code>MissionService</code> connects the <code>MissionControlComponent</code> to multiple <code>AstronautComponent</code> children.</p>\n<code-example path=\"component-interaction/src/app/mission.service.ts\" header=\"component-interaction/src/app/mission.service.ts\">\nimport { <a href=\"api/core/Injectable\" class=\"code-anchor\">Injectable</a> } from '@angular/core';\nimport { Subject } from 'rxjs';\n\n@<a href=\"api/core/Injectable\" class=\"code-anchor\">Injectable</a>()\nexport class MissionService {\n\n // Observable string sources\n private missionAnnouncedSource = new Subject<string>();\n private missionConfirmedSource = new Subject<string>();\n\n // Observable string streams\n missionAnnounced$ = this.missionAnnouncedSource.asObservable();\n missionConfirmed$ = this.missionConfirmedSource.asObservable();\n\n // Service message commands\n announceMission(mission: string) {\n this.missionAnnouncedSource.next(mission);\n }\n\n confirmMission(astronaut: string) {\n this.missionConfirmedSource.next(astronaut);\n }\n}\n\n</code-example>\n<p>The <code>MissionControlComponent</code> both provides the instance of the service that it shares with its children\n(through the <code>providers</code> metadata array) and injects that instance into itself through its constructor:</p>\n<code-example path=\"component-interaction/src/app/missioncontrol.component.ts\" header=\"component-interaction/src/app/missioncontrol.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a> } from '@angular/core';\n\nimport { MissionService } from './mission.service';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-mission-control',\n template: `\n <h2>Mission Control</h2>\n <button (click)=\"announce()\">Announce mission</button>\n <app-astronaut *<a href=\"api/common/NgForOf\" class=\"code-anchor\">ngFor</a>=\"let astronaut of astronauts\"\n [astronaut]=\"astronaut\">\n </app-astronaut>\n <h3>History</h3>\n <ul>\n <li *<a href=\"api/common/NgForOf\" class=\"code-anchor\">ngFor</a>=\"let event of history\">{{event}}</li>\n </ul>\n `,\n providers: [MissionService]\n})\nexport class MissionControlComponent {\n astronauts = ['Lovell', 'Swigert', 'Haise'];\n history: string[] = [];\n missions = ['Fly to the moon!',\n 'Fly to mars!',\n 'Fly to Vegas!'];\n nextMission = 0;\n\n constructor(private missionService: MissionService) {\n missionService.missionConfirmed$.subscribe(\n astronaut => {\n this.history.push(`${astronaut} confirmed the mission`);\n });\n }\n\n announce() {\n const mission = this.missions[this.nextMission++];\n this.missionService.announceMission(mission);\n this.history.push(`Mission \"${mission}\" announced`);\n if (this.nextMission >= this.missions.length) { this.nextMission = 0; }\n }\n}\n\n</code-example>\n<p>The <code>AstronautComponent</code> also injects the service in its constructor.\nEach <code>AstronautComponent</code> is a child of the <code>MissionControlComponent</code> and therefore receives its parent's service instance:</p>\n<code-example path=\"component-interaction/src/app/astronaut.component.ts\" header=\"component-interaction/src/app/astronaut.component.ts\">\nimport { <a href=\"api/core/Component\" class=\"code-anchor\">Component</a>, <a href=\"api/core/Input\" class=\"code-anchor\">Input</a>, <a href=\"api/core/OnDestroy\" class=\"code-anchor\">OnDestroy</a> } from '@angular/core';\n\nimport { MissionService } from './mission.service';\nimport { Subscription } from 'rxjs';\n\n@<a href=\"api/core/Component\" class=\"code-anchor\">Component</a>({\n selector: 'app-astronaut',\n template: `\n <p>\n {{astronaut}}: <strong>{{mission}}</strong>\n <button\n (click)=\"confirm()\"\n [disabled]=\"!announced || confirmed\">\n Confirm\n </button>\n </p>\n `\n})\nexport class AstronautComponent implements <a href=\"api/core/OnDestroy\" class=\"code-anchor\">OnDestroy</a> {\n @<a href=\"api/core/Input\" class=\"code-anchor\">Input</a>() astronaut: string;\n mission = '<no mission announced>';\n confirmed = false;\n announced = false;\n subscription: Subscription;\n\n constructor(private missionService: MissionService) {\n this.subscription = missionService.missionAnnounced$.subscribe(\n mission => {\n this.mission = mission;\n this.announced = true;\n this.confirmed = false;\n });\n }\n\n confirm() {\n this.confirmed = true;\n this.missionService.confirmMission(this.astronaut);\n }\n\n ngOnDestroy() {\n // prevent memory leak when component destroyed\n this.subscription.unsubscribe();\n }\n}\n\n</code-example>\n<div class=\"alert is-helpful\">\n<p>Notice that this example captures the <code>subscription</code> and <code>unsubscribe()</code> when the <code>AstronautComponent</code> is destroyed.\nThis is a memory-leak guard step. There is no actual risk in this app because the\nlifetime of a <code>AstronautComponent</code> is the same as the lifetime of the app itself.\nThat <em>would not</em> always be true in a more complex application.</p>\n<p>You don't add this guard to the <code>MissionControlComponent</code> because, as the parent,\nit controls the lifetime of the <code>MissionService</code>.</p>\n</div>\n<p>The <em>History</em> log demonstrates that messages travel in both directions between\nthe parent <code>MissionControlComponent</code> and the <code>AstronautComponent</code> children,\nfacilitated by the service:</p>\n<div class=\"lightbox\">\n <img src=\"generated/images/guide/component-interaction/bidirectional-service.gif\" alt=\"bidirectional-service\" width=\"310\" height=\"346\">\n</div>\n<h3 class=\"no-toc\" id=\"test-it-6\">Test it<a title=\"Link to this heading\" class=\"header-link\" aria-hidden=\"true\" href=\"guide/component-interaction#test-it-6\"><i class=\"material-icons\">link</i></a></h3>\n<p>Tests click buttons of both the parent <code>MissionControlComponent</code> and the <code>AstronautComponent</code> children\nand verify that the history meets expectations:</p>\n<code-example path=\"component-interaction/e2e/src/app.e2e-spec.ts\" region=\"bidirectional-service\" header=\"component-interaction/e2e/src/app.e2e-spec.ts\">\n// ...\nit('should announce a mission', async () => {\n const missionControl = element(by.tagName('app-mission-control'));\n const announceButton = missionControl.all(by.tagName('button')).get(0);\n const history = missionControl.all(by.tagName('li'));\n\n await announceButton.click();\n\n expect(await history.count()).toBe(1);\n expect(await history.get(0).getText()).toMatch(/Mission.* announced/);\n});\n\nit('should confirm the mission by Lovell', async () => {\n await testConfirmMission(1, 'Lovell');\n});\n\nit('should confirm the mission by Haise', async () => {\n await testConfirmMission(3, 'Haise');\n});\n\nit('should confirm the mission by Swigert', async () => {\n await testConfirmMission(2, 'Swigert');\n});\n\nasync function testConfirmMission(buttonIndex: number, astronaut: string) {\n const missionControl = element(by.tagName('app-mission-control'));\n const announceButton = missionControl.all(by.tagName('button')).get(0);\n const confirmButton = missionControl.all(by.tagName('button')).get(buttonIndex);\n const history = missionControl.all(by.tagName('li'));\n\n await announceButton.click();\n await confirmButton.click();\n\n expect(await history.count()).toBe(2);\n expect(await history.get(1).getText()).toBe(`${astronaut} confirmed the mission`);\n}\n// ...\n\n</code-example>\n<p><a href=\"guide/component-interaction#top\">Back to top</a></p>\n\n \n</div>\n\n<!-- links to this doc:\n - guide/example-apps-list\n - start\n-->\n<!-- links from this doc:\n - api/animations/trigger\n - api/common/NgForOf\n - api/core/AfterViewInit\n - api/core/ApplicationRef\n - api/core/Component\n - api/core/EventEmitter\n - api/core/Injectable\n - api/core/Input\n - api/core/OnChanges\n - api/core/OnDestroy\n - api/core/Output\n - api/core/SimpleChanges\n - api/core/Version\n - api/core/ViewChild\n - api/core/testing/tick\n - guide/component-interaction#component-interaction\n - guide/component-interaction#countdown-tests\n - guide/component-interaction#countdown-timer-example\n - guide/component-interaction#intercept-input-property-changes-with-a-setter\n - guide/component-interaction#intercept-input-property-changes-with-ngonchanges\n - guide/component-interaction#parent-and-children-communicate-via-a-service\n - guide/component-interaction#parent-calls-an-viewchild\n - guide/component-interaction#parent-interacts-with-child-via-local-variable\n - guide/component-interaction#parent-listens-for-child-event\n - guide/component-interaction#pass-data-from-parent-to-child-with-input-binding\n - guide/component-interaction#test-it\n - guide/component-interaction#test-it-1\n - guide/component-interaction#test-it-2\n - guide/component-interaction#test-it-3\n - guide/component-interaction#test-it-4\n - guide/component-interaction#test-it-5\n - guide/component-interaction#test-it-6\n - guide/component-interaction#top\n - guide/inputs-outputs#input\n - guide/inputs-outputs#output\n - guide/lifecycle-hooks\n - https://github.com/angular/angular/edit/master/aio/content/guide/component-interaction.md?message=docs%3A%20describe%20your%20change...\n-->"
|
|
} |