{ "id": "guide/component-interaction", "title": "Component interaction", "contents": "\n\n\n
\n mode_edit\n
\n\n\n
\n

Component interactionlink

\n\n

This cookbook contains recipes for common component communication scenarios\nin which two or more components share information.\n

\n\n

See the .

\n\n

Pass data from parent to child with input bindinglink

\n

HeroChildComponent has two input properties,\ntypically adorned with @Input() decorator.

\n\nimport { Component, Input } from '@angular/core';\n\nimport { Hero } from './hero';\n\n@Component({\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 @Input() hero: Hero;\n @Input('master') masterName: string; // tslint:disable-line: no-input-rename\n}\n\n\n

The second @Input aliases the child component property name masterName as 'master'.

\n

The HeroParentComponent nests the child HeroChildComponent inside an *ngFor repeater,\nbinding its master string property to the child's master alias,\nand each iteration's hero instance to the child's hero property.

\n\nimport { Component } from '@angular/core';\n\nimport { HEROES } from './hero';\n\n@Component({\n selector: 'app-hero-parent',\n template: `\n <h2>{{master}} controls {{heroes.length}} heroes</h2>\n <app-hero-child *ngFor=\"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\n

The running application displays three heroes:

\n
\n \"Parent-to-child\"\n
\n

Test itlink

\n

E2E test that all children were instantiated and displayed as expected:

\n\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\n

Back to top

\n\n

Intercept input property changes with a setterlink

\n

Use an input property setter to intercept and act upon a value from the parent.

\n

The setter of the name input property in the child NameChildComponent\ntrims the whitespace from a name and replaces an empty value with default text.

\n\nimport { Component, Input } from '@angular/core';\n\n@Component({\n selector: 'app-name-child',\n template: '<h3>\"{{name}}\"</h3>'\n})\nexport class NameChildComponent {\n @Input()\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\n

Here's the NameParentComponent demonstrating name variations including a name with all spaces:

\n\nimport { Component } from '@angular/core';\n\n@Component({\n selector: 'app-name-parent',\n template: `\n <h2>Master controls {{names.length}} names</h2>\n <app-name-child *ngFor=\"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\n
\n \"Parent-to-child-setter\"\n
\n

Test itlink

\n

E2E tests of input property setter with empty and non-empty names:

\n\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\n

Back to top

\n\n

Intercept input property changes with ngOnChanges()link

\n

Detect and act upon changes to input property values with the ngOnChanges() method of the OnChanges lifecycle hook interface.

\n
\n

You may prefer this approach to the property setter when watching multiple, interacting input properties.

\n

Learn about ngOnChanges() in the Lifecycle Hooks chapter.

\n
\n

This VersionChildComponent detects changes to the major and minor input properties and composes a log message reporting these changes:

\n\nimport { Component, Input, OnChanges, SimpleChanges } from '@angular/core';\n\n@Component({\n selector: 'app-version-child',\n template: `\n <h3>Version {{major}}.{{minor}}</h3>\n <h4>Change log:</h4>\n <ul>\n <li *ngFor=\"let change of changeLog\">{{change}}</li>\n </ul>\n `\n})\nexport class VersionChildComponent implements OnChanges {\n @Input() major: number;\n @Input() minor: number;\n changeLog: string[] = [];\n\n ngOnChanges(changes: SimpleChanges) {\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\n

The VersionParentComponent supplies the minor and major values and binds buttons to methods that change them.

\n\nimport { Component } from '@angular/core';\n\n@Component({\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\n

Here's the output of a button-pushing sequence:

\n
\n \"Parent-to-child-onchanges\"\n
\n

Test itlink

\n

Test that both input properties are set initially and that button clicks trigger\nthe expected ngOnChanges calls and values:

\n\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 = 'Version 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 = 'Version 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 = 'Version 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\n

Back to top

\n\n

Parent listens for child eventlink

\n

The child component exposes an EventEmitter property with which it emits events when something happens.\nThe parent binds to that event property and reacts to those events.

\n

The child's EventEmitter property is an output property,\ntypically adorned with an @Output() decorator\nas seen in this VoterComponent:

\n\nimport { Component, EventEmitter, Input, Output } from '@angular/core';\n\n@Component({\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 @Input() name: string;\n @Output() voted = new EventEmitter<boolean>();\n didVote = false;\n\n vote(agreed: boolean) {\n this.voted.emit(agreed);\n this.didVote = true;\n }\n}\n\n\n

Clicking a button triggers emission of a true or false, the boolean payload.

\n

The parent VoteTakerComponent binds an event handler called onVoted() that responds to the child event\npayload $event and updates a counter.

\n\nimport { Component } from '@angular/core';\n\n@Component({\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 *ngFor=\"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\n

The framework passes the event argument—represented by $event—to the handler method,\nand the method processes it:

\n
\n \"Child-to-parent\"\n
\n

Test itlink

\n

Test that clicking the Agree and Disagree buttons update the appropriate counters:

\n\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\n

Back to top

\n

Parent interacts with child via local variablelink

\n

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 within the parent template\nas seen in the following example.

\n\n

The following is a child CountdownTimerComponent that repeatedly counts down to zero and launches a rocket.\nIt has start and stop methods that control the clock and it displays a\ncountdown status message in its own template.

\n\nimport { Component, OnDestroy } from '@angular/core';\n\n@Component({\n selector: 'app-countdown-timer',\n template: '<p>{{message}}</p>'\n})\nexport class CountdownTimerComponent implements OnDestroy {\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\n

The CountdownLocalVarParentComponent that hosts the timer component is as follows:

\n\nimport { Component } from '@angular/core';\nimport { CountdownTimerComponent } from './countdown-timer.component';\n\n@Component({\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\n

The parent component cannot data bind to the child's\nstart and stop methods nor to its seconds property.

\n

You can place a local variable, #timer, on the tag <countdown-timer> representing the child component.\nThat gives you a reference to the child component and the ability to access\nany of its properties or methods from within the parent template.

\n

This example wires parent buttons to the child's start and stop and\nuses interpolation to display the child's seconds property.

\n

Here we see the parent and child working together.

\n
\n \"countdown\n
\n\n

Test itlink

\n

Test that the seconds displayed in the parent template\nmatch the seconds displayed in the child's status message.\nTest also that clicking the Stop button pauses the countdown timer:

\n\n// ...\n// The tests trigger periodic asynchronous operations (via `setInterval()`), which will prevent\n// the app from stabilizing. See https://angular.io/api/core/ApplicationRef#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\n

Back to top

\n\n

Parent calls an @ViewChild()link

\n

The local variable 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 itself has no access to the child.

\n

You can't use the local variable technique if an instance of the parent component class\nmust read or write child component values or must call child component methods.

\n

When the parent component class requires that kind of access,\ninject the child component into the parent as a ViewChild.

\n

The following example illustrates this technique with the same\nCountdown Timer example.\nNeither its appearance nor its behavior will change.\nThe child CountdownTimerComponent is the same as well.

\n
\n

The switch from the local variable to the ViewChild technique\nis solely for the purpose of demonstration.

\n
\n

Here is the parent, CountdownViewChildParentComponent:

\n\nimport { AfterViewInit, ViewChild } from '@angular/core';\nimport { Component } from '@angular/core';\nimport { CountdownTimerComponent } from './countdown-timer.component';\n\n@Component({\n selector: 'app-countdown-parent-vc',\n template: `\n <h3>Countdown to Liftoff (via ViewChild)</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 AfterViewInit {\n\n @ViewChild(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 tick 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\n

It takes a bit more work to get the child view into the parent component class.

\n

First, you have to import references to the ViewChild decorator and the AfterViewInit lifecycle hook.

\n

Next, inject the child CountdownTimerComponent into the private timerComponent property\nvia the @ViewChild property decoration.

\n

The #timer local variable is gone from the component metadata.\nInstead, bind the buttons to the parent component's own start and stop methods and\npresent the ticking seconds in an interpolation around the parent component's seconds method.

\n

These methods access the injected timer component directly.

\n

The ngAfterViewInit() lifecycle hook is an important wrinkle.\nThe timer component isn't available until after Angular displays the parent view.\nSo it displays 0 seconds initially.

\n

Then Angular calls the ngAfterViewInit lifecycle hook at which time it is too late\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 wait one turn before it can display the seconds.

\n

Use setTimeout() to wait one tick and then revise the seconds() method so\nthat it takes future values from the timer component.

\n

Test itlink

\n

Use the same countdown timer tests as before.

\n

Back to top

\n\n

Parent and children communicate via a servicelink

\n

A parent component and its children share a service whose interface enables bi-directional communication\nwithin the family.

\n

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.

\n

This MissionService connects the MissionControlComponent to multiple AstronautComponent children.

\n\nimport { Injectable } from '@angular/core';\nimport { Subject } from 'rxjs';\n\n@Injectable()\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\n

The MissionControlComponent both provides the instance of the service that it shares with its children\n(through the providers metadata array) and injects that instance into itself through its constructor:

\n\nimport { Component } from '@angular/core';\n\nimport { MissionService } from './mission.service';\n\n@Component({\n selector: 'app-mission-control',\n template: `\n <h2>Mission Control</h2>\n <button (click)=\"announce()\">Announce mission</button>\n <app-astronaut *ngFor=\"let astronaut of astronauts\"\n [astronaut]=\"astronaut\">\n </app-astronaut>\n <h3>History</h3>\n <ul>\n <li *ngFor=\"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\n

The AstronautComponent also injects the service in its constructor.\nEach AstronautComponent is a child of the MissionControlComponent and therefore receives its parent's service instance:

\n\nimport { Component, Input, OnDestroy } from '@angular/core';\n\nimport { MissionService } from './mission.service';\nimport { Subscription } from 'rxjs';\n\n@Component({\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 OnDestroy {\n @Input() 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\n
\n

Notice that this example captures the subscription and unsubscribe() when the AstronautComponent is destroyed.\nThis is a memory-leak guard step. There is no actual risk in this app because the\nlifetime of a AstronautComponent is the same as the lifetime of the app itself.\nThat would not always be true in a more complex application.

\n

You don't add this guard to the MissionControlComponent because, as the parent,\nit controls the lifetime of the MissionService.

\n
\n

The History log demonstrates that messages travel in both directions between\nthe parent MissionControlComponent and the AstronautComponent children,\nfacilitated by the service:

\n
\n \"bidirectional-service\"\n
\n

Test itlink

\n

Tests click buttons of both the parent MissionControlComponent and the AstronautComponent children\nand verify that the history meets expectations:

\n\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\n

Back to top

\n\n \n
\n\n\n" }