本文包含了常见的组件通讯场景,也就是让两个或多个组件之间共享信息的方法。

我们提供的服务有:网站设计制作、网站设计、微信公众号开发、网站优化、网站认证、南通ssl等。为1000+企事业单位解决了网站和推广的问题。提供周到的售前咨询和贴心的售后服务,是有科学管理、有技术的南通网站制作公司
参阅现场演练 / 下载范例。
HeroChildComponent 有两个输入型属性,它们通常带@Input 装饰器。
import { Component, Input } from '@Angular/core';
import { Hero } from './hero';
@Component({
  selector: 'app-hero-child',
  template: `
    {{hero.name}} says:
    I, {{hero.name}}, am at your service, {{masterName}}.
  `
})
export class HeroChildComponent {
  @Input() hero!: Hero;
  @Input('master') masterName = '';
}第二个 @Input 为子组件的属性名 masterName 指定一个别名 master(译者注:不推荐为起别名,请参阅风格指南).
父组件 HeroParentComponent 把子组件的 HeroChildComponent 放到 *ngFor 循环器中,把自己的 master 字符串属性绑定到子组件的 master 别名上,并把每个循环的 hero 实例绑定到子组件的 hero 属性。
import { Component } from '@angular/core';
import { HEROES } from './hero';
@Component({
  selector: 'app-hero-parent',
  template: `
    {{master}} controls {{heroes.length}} heroes
    
     
  `
})
export class HeroParentComponent {
  heroes = HEROES;
  master = 'Master';
}运行应用程序会显示三个英雄:
端到端测试,用于确保所有的子组件都如预期般初始化并显示出来:
// ...
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,以拦截父组件中值的变化,并采取行动。
子组件 NameChildComponent 的输入属性 name 上的这个 setter,会 trim 掉名字里的空格,并把空值替换成默认字符串。
import { Component, Input } from '@angular/core';
@Component({
  selector: 'app-name-child',
  template: '"{{name}}"
'
})
export class NameChildComponent {
  @Input()
  get name(): string { return this._name; }
  set name(name: string) {
    this._name = (name && name.trim()) || '';
  }
  private _name = '';
} 下面的 NameParentComponent 展示了各种名字的处理方式,包括一个全是空格的名字。
import { Component } from '@angular/core';
@Component({
  selector: 'app-name-parent',
  template: `
    Master controls {{names.length}} names
    ', 'Bombasto'
  names = ['Dr IQ', '   ', '  Bombasto  '];
} 端到端测试:输入属性的 setter,分别使用空名字和非空名字。
// ...
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 = '""';
  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);
});
// ... 使用 OnChanges 生命周期钩子接口的 ngOnChanges() 方法来监测输入属性值的变化并做出回应。
当需要监视多个、交互式输入属性的时候,本方法比用属性的 setter 更合适。
这个 VersionChildComponent 会监测输入属性 major 和 minor 的变化,并把这些变化编写成日志以报告这些变化。
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
  selector: 'app-version-child',
  template: `
    Version {{major}}.{{minor}}
    Change log:
    
      - {{change}}
`
})
export class VersionChildComponent implements OnChanges {
  @Input() major = 0;
  @Input() minor = 0;
  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 值,把修改它们值的方法绑定到按钮上。
import { Component } from '@angular/core';
@Component({
  selector: 'app-version-parent',
  template: `
    Source code version
    
    
    下面是点击按钮的结果。
测试确保这两个输入属性值都被初始化了,当点击按钮后,ngOnChanges 应该被调用,属性的值也符合预期。
// ...
// 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(),
  };
}
// ...子组件暴露一个 EventEmitter 属性,当事件发生时,子组件利用该属性 emits(向上弹射)事件。父组件绑定到这个事件属性,并在事件发生时作出回应。
子组件的 EventEmitter 属性是一个输出属性,通常带有@Output 装饰器,就像在 VoterComponent 中看到的。
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
  selector: 'app-voter',
  template: `
    {{name}}
    
    
  `
})
export class VoterComponent {
  @Input()  name = '';
  @Output() voted = new EventEmitter();
  didVote = false;
  vote(agreed: boolean) {
    this.voted.emit(agreed);
    this.didVote = true;
  }
} 点击按钮会触发 true 或 false(布尔型有效载荷)的事件。
父组件 VoteTakerComponent 绑定了一个事件处理器(onVoted()),用来响应子组件的事件($event)并更新一个计数器。
import { Component } from '@angular/core';
@Component({
  selector: 'app-vote-taker',
  template: `
    Should mankind colonize the Universe?
    Agree: {{agreed}}, Disagree: {{disagreed}}
    
     
  `
})
export class VoteTakerComponent {
  agreed = 0;
  disagreed = 0;
  voters = ['Narco', 'Celeritas', 'Bombasto'];
  onVoted(agreed: boolean) {
    if (agreed) {
      this.agreed++;
    } else {
      this.disagreed++;
    }
  }
}本框架把事件参数(用 $event 表示)传给事件处理方法,该方法会处理它:
测试确保点击 Agree 和 Disagree 按钮时,计数器被正确更新。
// ...
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');
});
// ...父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但可以在父组件模板里,新建一个本地变量来代表子组件,然后利用这个变量来读取子组件的属性和调用子组件的方法,如下例所示。
子组件 CountdownTimerComponent 进行倒计时,归零时发射一个导弹。start 和 stop 方法负责控制时钟并在模板里显示倒计时的状态信息。
import { Component, OnDestroy } from '@angular/core';
@Component({
  selector: 'app-countdown-timer',
  template: '{{message}}
'
})
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 如下:
import { Component } from '@angular/core';
import { CountdownTimerComponent } from './countdown-timer.component';
@Component({
  selector: 'app-countdown-parent-lv',
  template: `
    Countdown to Liftoff (via local variable)
    
    
    {{timer.seconds}}
    父组件不能通过数据绑定使用子组件的 start 和 stop 方法,也不能访问子组件的 seconds 属性。
把本地变量(#timer)放到(
这个例子把父组件的按钮绑定到子组件的 start 和 stop 方法,并用插值来显示子组件的 seconds 属性。
下面是父组件和子组件一起工作时的效果。
测试确保在父组件模板中显示的秒数和子组件状态信息里的秒数同步。它还会点击 Stop 按钮来停止倒计时:
// ...
// 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 `` 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,注入到父组件里面。
下面的例子用与倒计时相同的范例来解释这种技术。 它的外观或行为没有变化。子组件CountdownTimerComponent也和原来一样。
由本地变量切换到 ViewChild 技术的唯一目的就是做示范。
下面是父组件 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: `
    Countdown to Liftoff (via ViewChild)
    
    
    {{ seconds() }}
    把子组件的视图插入到父组件类需要做一点额外的工作。
首先,你必须导入对装饰器 ViewChild 以及生命周期钩子 AfterViewInit 的引用。
接着,通过 @ViewChild 属性装饰器,将子组件 CountdownTimerComponent 注入到私有属性 timerComponent 里面。
组件元数据里就不再需要 #timer 本地变量了。而是把按钮绑定到父组件自己的 start 和 stop 方法,使用父组件的 seconds 方法的插值来展示秒数变化。
这些方法可以直接访问被注入的计时器组件。
ngAfterViewInit() 生命周期钩子是非常重要的一步。被注入的计时器组件只有在 Angular 显示了父组件视图之后才能访问,所以它先把秒数显示为 0.
然后 Angular 会调用 ngAfterViewInit 生命周期钩子,但这时候再更新父组件视图的倒计时就已经太晚了。Angular 的单向数据流规则会阻止在同一个周期内更新父组件视图。应用在显示秒数之前会被迫再等一轮。
使用 setTimeout() 来等下一轮,然后改写 seconds() 方法,这样它接下来就会从注入的这个计时器组件里获取秒数的值。
父组件和它的子组件共享同一个服务,利用该服务在组件家族内部实现双向通讯。
该服务实例的作用域被限制在父组件和其子组件内。这个组件子树之外的组件将无法访问该服务或者与它们通讯。
这个 MissionService 把 MissionControlComponent 和多个 AstronautComponent 子组件连接起来。
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable()
export class MissionService {
  // Observable string sources
  private missionAnnouncedSource = new Subject();
  private missionConfirmedSource = new Subject();
  // Observable string streams
  missionAnnounced$ = this.missionAnnouncedSource.asObservable();
  missionConfirmed$ = this.missionConfirmedSource.asObservable();
  // Service message commands
  announceMission(mission: string) {
    this.missionAnnouncedSource.next(mission);
  }
  confirmMission(astronaut: string) {
    this.missionConfirmedSource.next(astronaut);
  }
}  MissionControlComponent 提供服务的实例,并将其共享给它的子组件(通过 providers 元数据数组),子组件可以通过构造函数将该实例注入到自身。
import { Component } from '@angular/core';
import { MissionService } from './mission.service';
@Component({
  selector: 'app-mission-control',
  template: `
    Mission Control
    
    
     
    History
    
      - {{event}}
`,
  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 的子组件,所以它们获取到的也是父组件的这个服务实例。
import { Component, Input, OnDestroy } from '@angular/core';
import { MissionService } from './mission.service';
import { Subscription } from 'rxjs';
@Component({
  selector: 'app-astronaut',
  template: `
    
      {{astronaut}}: {{mission}}
      
    
  `
})
export class AstronautComponent implements OnDestroy {
  @Input() astronaut = '';
  mission = '';
  confirmed = false;
  announced = false;
  subscription: Subscription;
  constructor(private missionService: MissionService) {
    this.subscription = missionService.missionAnnounced$.subscribe(
      mission => {
        this.mission = mission;
        this.announced = true;
        this.confirmed = false;
    });
  }
  confirm() {
    this.confirmed = true;
    this.missionService.confirmMission(this.astronaut);
  }
  ngOnDestroy() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
  }
} 注意,这个例子保存了 
subscription变量,并在 
AstronautComponent被销毁时调用 
unsubscribe() 退订。 这是一个用于防止内存泄漏的保护措施。实际上,在这个应用程序中并没有这个风险,因为 
AstronautComponent的生命期和应用程序的生命期一样长。但在更复杂的应用程序环境中就不一定了。
不需要在 
MissionControlComponent中添加这个保护措施,因为它作为父组件,控制着 
MissionService的生命期。
History 日志证明了:在父组件 MissionControlComponent 和子组件 AstronautComponent 之间,信息通过该服务实现了双向传递。
测试确保点击父组件 MissionControlComponent 和子组件 AstronautComponent 两个的组件的按钮时,History 日志和预期的一样。
// ...
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`);
}
// ...