组件之间的交互
Component interaction
本文包含了常见的组件通讯场景,也就是让两个或多个组件之间共享信息的方法。
This cookbook contains recipes for common component communication scenarios in which two or more components share information.
通过输入型绑定把数据从父组件传到子组件。
Pass data from parent to child with input binding
HeroChildComponent
有两个输入型属性,它们通常带@Input 装饰器。
HeroChildComponent
has two input properties, typically adorned with @Input() decorator.
import { Component, Input } from '@angular/core';
import { Hero } from './hero';
@Component({
selector: 'app-hero-child',
template: `
<h3>{{hero.name}} says:</h3>
<p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
`
})
export class HeroChildComponent {
@Input() hero: Hero;
@Input('master') masterName: string; // tslint:disable-line: no-input-rename
}
第二个 @Input
为子组件的属性名 masterName
指定一个别名 master
(译者注:不推荐为起别名,请参阅风格指南).
The second @Input
aliases the child component property name masterName
as 'master'
.
父组件 HeroParentComponent
把子组件的 HeroChildComponent
放到 *ngFor
循环器中,把自己的 master
字符串属性绑定到子组件的 master
别名上,并把每个循环的 hero
实例绑定到子组件的 hero
属性。
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.
import { Component } from '@angular/core';
import { HEROES } from './hero';
@Component({
selector: 'app-hero-parent',
template: `
<h2>{{master}} controls {{heroes.length}} heroes</h2>
<app-hero-child *ngFor="let hero of heroes"
[hero]="hero"
[master]="master">
</app-hero-child>
`
})
export class HeroParentComponent {
heroes = HEROES;
master = 'Master';
}
运行应用程序会显示三个英雄:
The running application displays three heroes:
测试一下!
Test it
端到端测试,用于确保所有的子组件都如预期般初始化并显示出来:
E2E test that all children were instantiated and displayed as expected:
// ...
const heroNames = ['Dr IQ', 'Magneta', 'Bombasto'];
const masterName = 'Master';
it('should pass properties to children properly', async () => {
const parent = element(by.tagName('app-hero-parent'));
const heroes = parent.all(by.tagName('app-hero-child'));
for (let i = 0; i < heroNames.length; i++) {
const childTitle = await heroes.get(i).element(by.tagName('h3')).getText();
const childDetail = await heroes.get(i).element(by.tagName('p')).getText();
expect(childTitle).toEqual(heroNames[i] + ' says:');
expect(childDetail).toContain(masterName);
}
});
// ...
通过 setter 截听输入属性值的变化
Intercept input property changes with a setter
使用一个输入属性的 setter,以拦截父组件中值的变化,并采取行动。
Use an input property setter to intercept and act upon a value from the parent.
子组件 NameChildComponent
的输入属性 name
上的这个 setter,会 trim 掉名字里的空格,并把空值替换成默认字符串。
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.
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-name-child',
template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
@Input()
get name(): string { return this._name; }
set name(name: string) {
this._name = (name && name.trim()) || '<no name set>';
}
private _name = '';
}
下面的 NameParentComponent
展示了各种名字的处理方式,包括一个全是空格的名字。
Here's the NameParentComponent
demonstrating name variations including a name with all spaces:
import { Component } from '@angular/core';
@Component({
selector: 'app-name-parent',
template: `
<h2>Master controls {{names.length}} names</h2>
<app-name-child *ngFor="let name of names" [name]="name"></app-name-child>
`
})
export class NameParentComponent {
// Displays 'Dr IQ', '<no name set>', 'Bombasto'
names = ['Dr IQ', ' ', ' Bombasto '];
}
测试一下!
Test it
端到端测试:输入属性的 setter,分别使用空名字和非空名字。
E2E tests of input property setter with empty and non-empty names:
// ...
it('should display trimmed, non-empty names', async () => {
const nonEmptyNameIndex = 0;
const nonEmptyName = '"Dr IQ"';
const parent = element(by.tagName('app-name-parent'));
const hero = parent.all(by.tagName('app-name-child')).get(nonEmptyNameIndex);
const displayName = await hero.element(by.tagName('h3')).getText();
expect(displayName).toEqual(nonEmptyName);
});
it('should replace empty name with default name', async () => {
const emptyNameIndex = 1;
const defaultName = '"<no name set>"';
const parent = element(by.tagName('app-name-parent'));
const hero = parent.all(by.tagName('app-name-child')).get(emptyNameIndex);
const displayName = await hero.element(by.tagName('h3')).getText();
expect(displayName).toEqual(defaultName);
});
// ...
通过ngOnChanges()来截听输入属性值的变化
Intercept input property changes with ngOnChanges()
使用 OnChanges
生命周期钩子接口的 ngOnChanges()
方法来监测输入属性值的变化并做出回应。
Detect and act upon changes to input property values with the ngOnChanges()
method of the OnChanges
lifecycle hook interface.
当需要监视多个、交互式输入属性的时候,本方法比用属性的 setter 更合适。
You may prefer this approach to the property setter when watching multiple, interacting input properties.
学习关于 ngOnChanges()
的更多知识,参阅生命周期钩子一章。
Learn about ngOnChanges()
in the Lifecycle Hooks chapter.
这个 VersionChildComponent
会监测输入属性 major
和 minor
的变化,并把这些变化编写成日志以报告这些变化。
This VersionChildComponent
detects changes to the major
and minor
input properties and composes a log message reporting these changes:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-version-child',
template: `
<h3>Version {{major}}.{{minor}}</h3>
<h4>Change log:</h4>
<ul>
<li *ngFor="let change of changeLog">{{change}}</li>
</ul>
`
})
export class VersionChildComponent implements OnChanges {
@Input() major: number;
@Input() minor: number;
changeLog: string[] = [];
ngOnChanges(changes: SimpleChanges) {
const log: string[] = [];
for (const propName in changes) {
const changedProp = changes[propName];
const to = JSON.stringify(changedProp.currentValue);
if (changedProp.isFirstChange()) {
log.push(`Initial value of ${propName} set to ${to}`);
} else {
const from = JSON.stringify(changedProp.previousValue);
log.push(`${propName} changed from ${from} to ${to}`);
}
}
this.changeLog.push(log.join(', '));
}
}
VersionParentComponent
提供 minor
和 major
值,把修改它们值的方法绑定到按钮上。
The VersionParentComponent
supplies the minor
and major
values and binds buttons to methods that change them.
import { Component } from '@angular/core';
@Component({
selector: 'app-version-parent',
template: `
<h2>Source code version</h2>
<button (click)="newMinor()">New minor version</button>
<button (click)="newMajor()">New major version</button>
<app-version-child [major]="major" [minor]="minor"></app-version-child>
`
})
export class VersionParentComponent {
major = 1;
minor = 23;
newMinor() {
this.minor++;
}
newMajor() {
this.major++;
this.minor = 0;
}
}
下面是点击按钮的结果。
Here's the output of a button-pushing sequence:
测试一下!
Test it
测试确保这两个输入属性值都被初始化了,当点击按钮后,ngOnChanges
应该被调用,属性的值也符合预期。
Test that both input properties are set initially and that button clicks trigger the expected ngOnChanges
calls and values:
// ...
// Test must all execute in this exact order
it('should set expected initial values', async () => {
const actual = await getActual();
const initialLabel = 'Version 1.23';
const initialLog = 'Initial value of major set to 1, Initial value of minor set to 23';
expect(actual.label).toBe(initialLabel);
expect(actual.count).toBe(1);
expect(await actual.logs.get(0).getText()).toBe(initialLog);
});
it('should set expected values after clicking \'Minor\' twice', async () => {
const repoTag = element(by.tagName('app-version-parent'));
const newMinorButton = repoTag.all(by.tagName('button')).get(0);
await newMinorButton.click();
await newMinorButton.click();
const actual = await getActual();
const labelAfter2Minor = 'Version 1.25';
const logAfter2Minor = 'minor changed from 24 to 25';
expect(actual.label).toBe(labelAfter2Minor);
expect(actual.count).toBe(3);
expect(await actual.logs.get(2).getText()).toBe(logAfter2Minor);
});
it('should set expected values after clicking \'Major\' once', async () => {
const repoTag = element(by.tagName('app-version-parent'));
const newMajorButton = repoTag.all(by.tagName('button')).get(1);
await newMajorButton.click();
const actual = await getActual();
const labelAfterMajor = 'Version 2.0';
const logAfterMajor = 'major changed from 1 to 2, minor changed from 23 to 0';
expect(actual.label).toBe(labelAfterMajor);
expect(actual.count).toBe(2);
expect(await actual.logs.get(1).getText()).toBe(logAfterMajor);
});
async function getActual() {
const versionTag = element(by.tagName('app-version-child'));
const label = await versionTag.element(by.tagName('h3')).getText();
const ul = versionTag.element((by.tagName('ul')));
const logs = ul.all(by.tagName('li'));
return {
label,
logs,
count: await logs.count(),
};
}
// ...
父组件监听子组件的事件
Parent listens for child event
子组件暴露一个 EventEmitter
属性,当事件发生时,子组件利用该属性 emits
(向上弹射)事件。父组件绑定到这个事件属性,并在事件发生时作出回应。
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.
子组件的 EventEmitter
属性是一个输出属性,通常带有@Output 装饰器,就像在 VoterComponent
中看到的。
The child's EventEmitter
property is an output property, typically adorned with an @Output() decorator as seen in this VoterComponent
:
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-voter',
template: `
<h4>{{name}}</h4>
<button (click)="vote(true)" [disabled]="didVote">Agree</button>
<button (click)="vote(false)" [disabled]="didVote">Disagree</button>
`
})
export class VoterComponent {
@Input() name: string;
@Output() voted = new EventEmitter<boolean>();
didVote = false;
vote(agreed: boolean) {
this.voted.emit(agreed);
this.didVote = true;
}
}
点击按钮会触发 true
或 false
(布尔型有效载荷)的事件。
Clicking a button triggers emission of a true
or false
, the boolean payload.
父组件 VoteTakerComponent
绑定了一个事件处理器(onVoted()
),用来响应子组件的事件($event
)并更新一个计数器。
The parent VoteTakerComponent
binds an event handler called onVoted()
that responds to the child event payload $event
and updates a counter.
import { Component } from '@angular/core';
@Component({
selector: 'app-vote-taker',
template: `
<h2>Should mankind colonize the Universe?</h2>
<h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
<app-voter *ngFor="let voter of voters"
[name]="voter"
(voted)="onVoted($event)">
</app-voter>
`
})
export class VoteTakerComponent {
agreed = 0;
disagreed = 0;
voters = ['Narco', 'Celeritas', 'Bombasto'];
onVoted(agreed: boolean) {
agreed ? this.agreed++ : this.disagreed++;
}
}
本框架把事件参数(用 $event
表示)传给事件处理方法,该方法会处理它:
The framework passes the event argument—represented by $event
—to the handler method, and the method processes it:
测试一下!
Test it
测试确保点击 Agree 和 Disagree 按钮时,计数器被正确更新。
Test that clicking the Agree and Disagree buttons update the appropriate counters:
// ...
it('should not emit the event initially', async () => {
const voteLabel = element(by.tagName('app-vote-taker')).element(by.tagName('h3'));
expect(await voteLabel.getText()).toBe('Agree: 0, Disagree: 0');
});
it('should process Agree vote', async () => {
const voteLabel = element(by.tagName('app-vote-taker')).element(by.tagName('h3'));
const agreeButton1 = element.all(by.tagName('app-voter')).get(0)
.all(by.tagName('button')).get(0);
await agreeButton1.click();
expect(await voteLabel.getText()).toBe('Agree: 1, Disagree: 0');
});
it('should process Disagree vote', async () => {
const voteLabel = element(by.tagName('app-vote-taker')).element(by.tagName('h3'));
const agreeButton1 = element.all(by.tagName('app-voter')).get(1)
.all(by.tagName('button')).get(1);
await agreeButton1.click();
expect(await voteLabel.getText()).toBe('Agree: 0, Disagree: 1');
});
// ...
父组件与子组件通过本地变量互动
Parent interacts with child via local variable
父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但可以在父组件模板里,新建一个本地变量来代表子组件,然后利用这个变量来读取子组件的属性和调用子组件的方法,如下例所示。
A parent component cannot use data binding to read child properties or invoke child methods. You can do both by creating a template reference variable for the child element and then reference that variable within the parent template as seen in the following example.
子组件 CountdownTimerComponent
进行倒计时,归零时发射一个导弹。start
和 stop
方法负责控制时钟并在模板里显示倒计时的状态信息。
The following is a child CountdownTimerComponent
that repeatedly counts down to zero and launches a rocket. It has start
and stop
methods that control the clock and it displays a countdown status message in its own template.
import { Component, OnDestroy } from '@angular/core';
@Component({
selector: 'app-countdown-timer',
template: '<p>{{message}}</p>'
})
export class CountdownTimerComponent implements OnDestroy {
intervalId = 0;
message = '';
seconds = 11;
ngOnDestroy() { this.clearTimer(); }
start() { this.countDown(); }
stop() {
this.clearTimer();
this.message = `Holding at T-${this.seconds} seconds`;
}
private clearTimer() { clearInterval(this.intervalId); }
private countDown() {
this.clearTimer();
this.intervalId = window.setInterval(() => {
this.seconds -= 1;
if (this.seconds === 0) {
this.message = 'Blast off!';
} else {
if (this.seconds < 0) { this.seconds = 10; } // reset
this.message = `T-${this.seconds} seconds and counting`;
}
}, 1000);
}
}
计时器组件的宿主组件 CountdownLocalVarParentComponent
如下:
The CountdownLocalVarParentComponent
that hosts the timer component is as follows:
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';
@Component({
selector: 'app-countdown-parent-lv',
template: `
<h3>Countdown to Liftoff (via local variable)</h3>
<button (click)="timer.start()">Start</button>
<button (click)="timer.stop()">Stop</button>
<div class="seconds">{{timer.seconds}}</div>
<app-countdown-timer #timer></app-countdown-timer>
`,
styleUrls: ['../assets/demo.css']
})
export class CountdownLocalVarParentComponent { }
父组件不能通过数据绑定使用子组件的 start
和 stop
方法,也不能访问子组件的 seconds
属性。
The parent component cannot data bind to the child's start
and stop
methods nor to its seconds
property.
把本地变量(#timer
)放到(<countdown-timer>
)标签中,用来代表子组件。这样父组件的模板就得到了子组件的引用,于是可以在父组件的模板中访问子组件的所有属性和方法。
You can place a local variable, #timer
, on the tag <countdown-timer>
representing the child component. That gives you a reference to the child component and the ability to access any of its properties or methods from within the parent template.
这个例子把父组件的按钮绑定到子组件的 start
和 stop
方法,并用插值来显示子组件的 seconds
属性。
This example wires parent buttons to the child's start
and stop
and uses interpolation to display the child's seconds
property.
下面是父组件和子组件一起工作时的效果。
Here we see the parent and child working together.
测试一下!
Test it
测试确保在父组件模板中显示的秒数和子组件状态信息里的秒数同步。它还会点击 Stop 按钮来停止倒计时:
Test that the seconds displayed in the parent template match the seconds displayed in the child's status message. Test also that clicking the Stop button pauses the countdown timer:
// ...
// The tests trigger periodic asynchronous operations (via `setInterval()`), which will prevent
// the app from stabilizing. See https://angular.io/api/core/ApplicationRef#is-stable-examples
// for more details.
// To allow the tests to complete, we will disable automatically waiting for the Angular app to
// stabilize.
beforeEach(() => browser.waitForAngularEnabled(false));
afterEach(() => browser.waitForAngularEnabled(true));
it('timer and parent seconds should match', async () => {
const parent = element(by.tagName(parentTag));
const startButton = parent.element(by.buttonText('Start'));
const seconds = parent.element(by.className('seconds'));
const timer = parent.element(by.tagName('app-countdown-timer'));
await startButton.click();
// Wait for `<app-countdown-timer>` to be populated with any text.
await browser.wait(() => timer.getText(), 2000);
expect(await timer.getText()).toContain(await seconds.getText());
});
it('should stop the countdown', async () => {
const parent = element(by.tagName(parentTag));
const startButton = parent.element(by.buttonText('Start'));
const stopButton = parent.element(by.buttonText('Stop'));
const timer = parent.element(by.tagName('app-countdown-timer'));
await startButton.click();
expect(await timer.getText()).not.toContain('Holding');
await stopButton.click();
expect(await timer.getText()).toContain('Holding');
});
// ...
父组件调用@ViewChild()
Parent calls an @ViewChild()
这个本地变量方法是个简单便利的方法。但是它也有局限性,因为父组件-子组件的连接必须全部在父组件的模板中进行。父组件本身的代码对子组件没有访问权。
The local variable approach is simple and easy. But it is limited because the parent-child wiring must be done entirely within the parent template. The parent component itself has no access to the child.
如果父组件的类需要读取子组件的属性值或调用子组件的方法,就不能使用本地变量方法。
You can't use the local variable technique if an instance of the parent component class must read or write child component values or must call child component methods.
当父组件类需要这种访问时,可以把子组件作为 ViewChild,注入到父组件里面。
When the parent component class requires that kind of access, inject the child component into the parent as a ViewChild.
下面的例子用与倒计时相同的范例来解释这种技术。 它的外观或行为没有变化。子组件CountdownTimerComponent也和原来一样。
The following example illustrates this technique with the same Countdown Timer example. Neither its appearance nor its behavior will change. The child CountdownTimerComponent is the same as well.
由本地变量切换到 ViewChild 技术的唯一目的就是做示范。
The switch from the local variable to the ViewChild technique is solely for the purpose of demonstration.
下面是父组件 CountdownViewChildParentComponent
:
Here is the parent, CountdownViewChildParentComponent
:
import { AfterViewInit, ViewChild } from '@angular/core';
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';
@Component({
selector: 'app-countdown-parent-vc',
template: `
<h3>Countdown to Liftoff (via ViewChild)</h3>
<button (click)="start()">Start</button>
<button (click)="stop()">Stop</button>
<div class="seconds">{{ seconds() }}</div>
<app-countdown-timer></app-countdown-timer>
`,
styleUrls: ['../assets/demo.css']
})
export class CountdownViewChildParentComponent implements AfterViewInit {
@ViewChild(CountdownTimerComponent)
private timerComponent: CountdownTimerComponent;
seconds() { return 0; }
ngAfterViewInit() {
// Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...
// but wait a tick first to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
}
start() { this.timerComponent.start(); }
stop() { this.timerComponent.stop(); }
}
把子组件的视图插入到父组件类需要做一点额外的工作。
It takes a bit more work to get the child view into the parent component class.
首先,你必须导入对装饰器 ViewChild
以及生命周期钩子 AfterViewInit
的引用。
First, you have to import references to the ViewChild
decorator and the AfterViewInit
lifecycle hook.
接着,通过 @ViewChild
属性装饰器,将子组件 CountdownTimerComponent
注入到私有属性 timerComponent
里面。
Next, inject the child CountdownTimerComponent
into the private timerComponent
property via the @ViewChild
property decoration.
组件元数据里就不再需要 #timer
本地变量了。而是把按钮绑定到父组件自己的 start
和 stop
方法,使用父组件的 seconds
方法的插值来展示秒数变化。
The #timer
local variable is gone from the component metadata. Instead, bind the buttons to the parent component's own start
and stop
methods and present the ticking seconds in an interpolation around the parent component's seconds
method.
这些方法可以直接访问被注入的计时器组件。
These methods access the injected timer component directly.
ngAfterViewInit()
生命周期钩子是非常重要的一步。被注入的计时器组件只有在 Angular 显示了父组件视图之后才能访问,所以它先把秒数显示为 0.
The ngAfterViewInit()
lifecycle hook is an important wrinkle. The timer component isn't available until after Angular displays the parent view. So it displays 0
seconds initially.
然后 Angular 会调用 ngAfterViewInit
生命周期钩子,但这时候再更新父组件视图的倒计时就已经太晚了。Angular 的单向数据流规则会阻止在同一个周期内更新父组件视图。应用在显示秒数之前会被迫再等一轮。
Then Angular calls the ngAfterViewInit
lifecycle hook at which time it is too late to update the parent view's display of the countdown seconds. Angular's unidirectional data flow rule prevents updating the parent view's in the same cycle. The app has to wait one turn before it can display the seconds.
使用 setTimeout()
来等下一轮,然后改写 seconds()
方法,这样它接下来就会从注入的这个计时器组件里获取秒数的值。
Use setTimeout()
to wait one tick and then revise the seconds()
method so that it takes future values from the timer component.
测试一下!
Test it
使用和之前一样的倒计时测试。
Use the same countdown timer tests as before.
父组件和子组件通过服务来通讯
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.
这个 MissionService
把 MissionControlComponent
和多个 AstronautComponent
子组件连接起来。
This MissionService
connects the MissionControlComponent
to multiple AstronautComponent
children.
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable()
export class MissionService {
// Observable string sources
private missionAnnouncedSource = new Subject<string>();
private missionConfirmedSource = new Subject<string>();
// 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);
}
}
MissionControlComponent
提供服务的实例,并将其共享给它的子组件(通过 providers
元数据数组),子组件可以通过构造函数将该实例注入到自身。
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:
import { Component } from '@angular/core';
import { MissionService } from './mission.service';
@Component({
selector: 'app-mission-control',
template: `
<h2>Mission Control</h2>
<button (click)="announce()">Announce mission</button>
<app-astronaut *ngFor="let astronaut of astronauts"
[astronaut]="astronaut">
</app-astronaut>
<h3>History</h3>
<ul>
<li *ngFor="let event of history">{{event}}</li>
</ul>
`,
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() {
const mission = this.missions[this.nextMission++];
this.missionService.announceMission(mission);
this.history.push(`Mission "${mission}" announced`);
if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
}
}
AstronautComponent
也通过自己的构造函数注入该服务。由于每个 AstronautComponent
都是 MissionControlComponent
的子组件,所以它们获取到的也是父组件的这个服务实例。
The AstronautComponent
also injects the service in its constructor. Each AstronautComponent
is a child of the MissionControlComponent
and therefore receives its parent's service instance:
import { Component, Input, OnDestroy } from '@angular/core';
import { MissionService } from './mission.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-astronaut',
template: `
<p>
{{astronaut}}: <strong>{{mission}}</strong>
<button
(click)="confirm()"
[disabled]="!announced || confirmed">
Confirm
</button>
</p>
`
})
export class AstronautComponent implements OnDestroy {
@Input() astronaut: string;
mission = '<no mission announced>';
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();
}
}
注意,这个例子保存了 subscription
变量,并在 AstronautComponent
被销毁时调用 unsubscribe()
退订。 这是一个用于防止内存泄漏的保护措施。实际上,在这个应用程序中并没有这个风险,因为 AstronautComponent
的生命期和应用程序的生命期一样长。但在更复杂的应用程序环境中就不一定了。
Notice that this example captures 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.
不需要在 MissionControlComponent
中添加这个保护措施,因为它作为父组件,控制着 MissionService
的生命期。
You don't add this guard to the MissionControlComponent
because, as the parent, it controls the lifetime of the MissionService
.
History 日志证明了:在父组件 MissionControlComponent
和子组件 AstronautComponent
之间,信息通过该服务实现了双向传递。
The History log demonstrates that messages travel in both directions between the parent MissionControlComponent
and the AstronautComponent
children, facilitated by the service:
测试一下!
Test it
测试确保点击父组件 MissionControlComponent
和子组件 AstronautComponent
两个的组件的按钮时,History 日志和预期的一样。
Tests click buttons of both the parent MissionControlComponent
and the AstronautComponent
children and verify that the history meets expectations:
// ...
it('should announce a mission', async () => {
const missionControl = element(by.tagName('app-mission-control'));
const announceButton = missionControl.all(by.tagName('button')).get(0);
const history = missionControl.all(by.tagName('li'));
await announceButton.click();
expect(await history.count()).toBe(1);
expect(await history.get(0).getText()).toMatch(/Mission.* announced/);
});
it('should confirm the mission by Lovell', async () => {
await testConfirmMission(1, 'Lovell');
});
it('should confirm the mission by Haise', async () => {
await testConfirmMission(3, 'Haise');
});
it('should confirm the mission by Swigert', async () => {
await testConfirmMission(2, 'Swigert');
});
async function testConfirmMission(buttonIndex: number, astronaut: string) {
const missionControl = element(by.tagName('app-mission-control'));
const announceButton = missionControl.all(by.tagName('button')).get(0);
const confirmButton = missionControl.all(by.tagName('button')).get(buttonIndex);
const history = missionControl.all(by.tagName('li'));
await announceButton.click();
await confirmButton.click();
expect(await history.count()).toBe(2);
expect(await history.get(1).getText()).toBe(`${astronaut} confirmed the mission`);
}
// ...