使用輸入繫結將資料從父級傳遞給子級

HeroChildComponent 有兩個輸入屬性,通常用 @Input 裝飾。

import { Component, Input } from '@angular/core';
import { Hero } from './hero';
@Component({
  selector: '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;
}

使用 setter 攔截輸入屬性更改

使用輸入屬性 setter 攔截並處理來自父級的值。

子 NameChildComponent 中的 name 輸入屬性的 setter 從名稱中修剪空白,並用預設文字替換空值。

import { Component, Input } from '@angular/core';
@Component({
  selector: 'name-child',
  template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
  private _name = '';
  @Input()
  set name(name: string) {
    this._name = (name && name.trim()) || '<no name set>';
  }
  get name(): string { return this._name; }
}

這是 NameParentComponent 演示名稱變體,包括具有所有空格的名稱:

import { Component } from '@angular/core';
@Component({
  selector: 'name-parent',
  template: `
  <h2>Master controls {{names.length}} names</h2>
  <name-child *ngFor="let name of names" [name]="name"></name-child>
  `
})
export class NameParentComponent {
  // Displays 'Mr. IQ', '<no name set>', 'Bombasto'
  names = ['Mr. IQ', '   ', '  Bombasto  '];
}

父聽取兒童事件

子元件公開 EventEmitter 屬性,當事情發生時,它會使用該屬性發出事件。父級繫結到該事件屬性並對這些事件做出反應。

子項的 EventEmitter 屬性是一個輸出屬性,通常使用 @Output 裝飾進行裝飾,如此 VoterComponent 中所示:

import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
  selector: 'my-voter',
  template: `
    <h4>{{name}}</h4>
    <button (click)="vote(true)"  [disabled]="voted">Agree</button>
    <button (click)="vote(false)" [disabled]="voted">Disagree</button>
  `
})
export class VoterComponent {
  @Input()  name: string;
  @Output() onVoted = new EventEmitter<boolean>();
  voted = false;
  vote(agreed: boolean) {
    this.onVoted.emit(agreed);
    this.voted = true;
  }
}

單擊按鈕會觸發 true 或 false(布林有效負載)的發射。

父 VoteTakerComponent 繫結一個事件處理程式(onVoted),它響應子事件有效負載($ event)並更新計數器。

import { Component }      from '@angular/core';
@Component({
  selector: 'vote-taker',
  template: `
    <h2>Should mankind colonize the Universe?</h2>
    <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
    <my-voter *ngFor="let voter of voters"
      [name]="voter"
      (onVoted)="onVoted($event)">
    </my-voter>
  `
})
export class VoteTakerComponent {
  agreed = 0;
  disagreed = 0;
  voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];
  onVoted(agreed: boolean) {
    agreed ? this.agreed++ : this.disagreed++;
  }
}

父通過區域性變數與子進行互動

父元件不能使用資料繫結來讀取子屬性或呼叫子方法。我們可以通過為子元素建立模板引用變數,然後在父模板中引用該變數來實現這兩者,如以下示例所示。

我們有一個子 CountdownTimerComponent,它反覆計數到零並啟動一個火箭。它具有控制時鐘的啟動和停止方法,並在其自己的模板中顯示倒計時狀態訊息。

import { Component, OnDestroy, OnInit } from '@angular/core';
@Component({
  selector: 'countdown-timer',
  template: '<p>{{message}}</p>'
})
export class CountdownTimerComponent implements OnInit, OnDestroy {
  intervalId = 0;
  message = '';
  seconds = 11;
  clearTimer() { clearInterval(this.intervalId); }
  ngOnInit()    { this.start(); }
  ngOnDestroy() { this.clearTimer(); }
  start() { this.countDown(); }
  stop()  {
    this.clearTimer();
    this.message = `Holding at T-${this.seconds} seconds`;
  }
  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: '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>
  <countdown-timer #timer></countdown-timer>
  `,
  styleUrls: ['demo.css']
})
export class CountdownLocalVarParentComponent { }

父元件無法將資料繫結到子程序的 start 和 stop 方法,也無法繫結到其 seconds 屬性。

我們可以在表示子元件的 tag() 上放置一個區域性變數(#timer)。這為我們提供了對子元件本身的引用,以及從父模板中訪問其任何屬性或方法的能力。

在此示例中,我們將父按鈕連線到子項的開始和停止,並使用插值顯示子項的秒屬性。

在這裡,我們看到父母和孩子一起工作。

Parent 呼叫 ViewChild

區域性變數方法簡單易行。但它是有限的,因為父子線路必須完全在父模板內完成。父元件本身無權訪問子項。

如果父元件類的例項必須讀取或寫入子元件值或必須呼叫子元件方法,則不能使用區域性變數技術。

當父元件類需要這種訪問時,我們將子元件作為 ViewChild 注入父元件。

我們將使用相同的倒數計時器示例來說明此技術。我們不會改變它的外觀或行為。子 CountdownTimerComponent 也是一樣的。

我們只是為了演示而從區域性變數切換到 ViewChild 技術。這是父,CountdownViewChildParentComponent:

import { AfterViewInit, ViewChild } from '@angular/core';
import { Component }                from '@angular/core';
import { CountdownTimerComponent }  from './countdown-timer.component';
@Component({
  selector: '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>
  <countdown-timer></countdown-timer>
  `,
  styleUrls: ['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(); }
}

將子檢視放入父元件類需要更多的工作。

我們匯入對 ViewChild 裝飾器和 AfterViewInit 生命週期鉤子的引用。

我們通過 @ViewChild 屬性修飾將子 CountdownTimerComponent 注入私有 timerComponent 屬性。

#timer 區域性變數已從元件後設資料中消失。相反,我們將按鈕繫結到父元件自己的 start 和 stop 方法,並在父元件的 seconds 方法周圍插值時顯示滴答秒。

這些方法直接訪問注入的計時器元件。

ngAfterViewInit 生命週期鉤子是一個重要的皺紋。在 Angular 顯示父檢視之後,計時器元件才可用。所以我們最初顯示 0 秒。

然後 Angular 呼叫 ngAfterViewInit 生命週期鉤子,此時更新父檢視的倒計時秒顯示為時已晚。Angular 的單向資料流規則阻止我們在同一週期中更新父檢視。在我們顯示秒數之前,我們必須等待一轉。

我們使用 setTimeout 等待一個 tick,然後修改 seconds 方法,以便從計時器元件中獲取未來的值。

家長和孩子通過服務進行交流

父元件及其子元件共享服務,其介面支援在系列內進行雙向通訊。

服務例項的範圍是父元件及其子元件。此元件子樹外部的元件無法訪問服務或其通訊。

此 MissionService 將 MissionControlComponent 連線到多個 AstronautComponent 子級。

import { Injectable } from '@angular/core';
import { Subject }    from 'rxjs/Subject';
@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 既提供與其子代共享的服務例項(通過提供程式後設資料陣列),也通過其建構函式將該例項注入其自身:

import { Component }          from '@angular/core';
import { MissionService }     from './mission.service';
@Component({
  selector: 'mission-control',
  template: `
    <h2>Mission Control</h2>
    <button (click)="announce()">Announce mission</button>
    <my-astronaut *ngFor="let astronaut of astronauts"
      [astronaut]="astronaut">
    </my-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() {
    let mission = this.missions[this.nextMission++];
    this.missionService.announceMission(mission);
    this.history.push(`Mission "${mission}" announced`);
    if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
  }
}

AstronautComponent 也在其建構函式中注入服務。每個 AstronautComponent 都是 MissionControlComponent 的子節點,因此接收其父節點的服務例項:

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

請注意,我們捕獲訂閱並在 AstronautComponent 銷燬時取消訂閱。這是一個記憶體洩漏保護步驟。此應用程式中沒有實際風險,因為 AstronautComponent 的生命週期與應用程式本身的生命週期相同。在更復雜的應用程式中,這並非總是如此。

我們不會將此保護新增到 MissionControlComponent,因為作為父級,它控制 MissionService 的生命週期。歷史記錄日誌顯示訊息在父 MissionControlComponent 和 AstronautComponent 子節點之間雙向傳播,由服務促進: