1. 程式人生 > >Angular學習(二)元件互動的幾種方式

Angular學習(二)元件互動的幾種方式

通過@Input(),從父元件傳資料到子元件

在子元件中通過 @Input() 裝飾器修飾的屬性,在父元件裡可以引用並傳遞資料

// 子元件
export class Hero {
	name: string;
}

export const HEROES = [
	{name: 'Mr. IQ'},
	{name: 'Magneta'},
	{name: 'Bombasto'}
];

export class HeroChildComponent {
	@Input() hero: Hero;
	@Input('master') masterName: string;
}
<!-- 父元件模板 -->
<app-hero-child *ngFor="let hero of heroes" [hero]="hero" [master]="master"> </app-hero-child>

通過@Input() + setter, 監聽父元件屬性值變化

使用一個輸入屬性的 setter,以攔截父元件中值的變化,並採取行動。
子元件 NameChildComponent 的輸入屬性 name 上的這個 setter,會 trim 掉名字裡的空格,並把空值替換成預設字串

// 子元件
export class NameChildComponent {
  private
_name = ''; @Input() set name(name: string) { this._name = (name && name.trim()) || 'NULL'; } get name(): string { return this._name; } } // 父元件 export class NameParentComponent { names = ['Mr. IQ', ' ', ' Bombasto ']; }
<!-- 父元件模板內 -->
<h2>Master controls {{names.length}} names</
h2
>
<app-name-child *ngFor="let name of names" [name]="name"></app-name-child>

通過@Input() + ngOnChanges(),監聽父元件輸入屬性值的變化

使用 OnChanges 生命週期鉤子介面的 ngOnChanges() 方法來監測輸入屬性值的變化並做處理。
子元件會監測輸入屬性 major 和 minor 的變化,並把這些變化編寫成日誌以報告這些變化。
父元件提供 minor 和 major 值,把修改它們值的方法繫結到按鈕上。

// 子元件
export class VersionChildComponent implements OnChanges {
  @Input() major: number;
  @Input() minor: number;
  changeLog: string[] = [];
 
  ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
    let log: string[] = [];
    for (let propName in changes) {
      let changedProp = changes[propName];
      let to = JSON.stringify(changedProp.currentValue);
      if (changedProp.isFirstChange()) {
        log.push(`Initial value of ${propName} set to ${to}`);
      } else {
        let from = JSON.stringify(changedProp.previousValue);
        log.push(`${propName} changed from ${from} to ${to}`);
      }
    }
    this.changeLog.push(log.join(', '));
  }
}
// 父元件
export class VersionParentComponent {
  major = 1;
  minor = 23;
 
  newMinor() {
    this.minor++;
  }
 
  newMajor() {
    this.major++;
    this.minor = 0;
  }
}
<!-- 父模板 -->
<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>

通過@Output() + EventEmitter,父元件監聽子元件的事件

子元件暴露一個 EventEmitter 屬性,當事件發生時,子元件利用該屬性 emits(向上彈射)事件。父元件繫結到這個事件屬性,並在事件發生時作出迴應。

// 子元件
// 子元件的 EventEmitter 屬性是一個輸出屬性,通常帶有@Output 裝飾器,點選按鈕會觸發 true 或 false(布林型有效載荷)的事件。
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;
  }
}
// 父元件
// 父元件綁定了一個事件處理器(onVoted()),用來響應子元件的事件($event)並更新一個計數器。
@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 = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];
 
  onVoted(agreed: boolean) {
    agreed ? this.agreed++ : this.disagreed++;
  }
}

通過 # ,父子元件在模板內通過變數互動

父元件不能使用資料繫結來讀取子元件的屬性或呼叫子元件的方法。但可以在父元件模板裡,新建一個本地變數來代表子元件,然後利用這個變數來讀取子元件的屬性和呼叫子元件的方法,

父元件不能通過資料繫結使用子元件的 start 和 stop 方法,也不能訪問子元件的 seconds 屬性。
把本地變數(#timer)放到()標籤中,用來代表子元件。這樣父元件的模板就得到了子元件的引用,於是可以在父元件的模板中訪問子元件的所有屬性和方法。

這個例子把父元件的按鈕繫結到子元件的 start 和 stop 方法,並用插值表示式來顯示子元件的 seconds 屬性。

// 子元件
// 子元件進行倒計時,歸零時發射一個導彈。start 和 stop 方法負責控制時鐘並在模板裡顯示倒計時的狀態資訊
@Component({
  selector: 'app-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);
  }
}
// 父元件
@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 { }

通過 # + @ViewChild(),將模板內的子元件引用注入父元件類中

這個本地變數方法是個簡單便利的方法。但是它也有侷限性,因為父元件-子元件的連線必須全部在父元件的模板中進行。父元件本身的程式碼對子元件沒有訪問權。
如果父元件的類需要讀取子元件的屬性值或呼叫子元件的方法,就不能使用本地變數方法。
當父元件類需要這種訪問時,可以把子元件作為 ViewChild,注入到父元件裡面。

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(); }
}

把子元件的檢視插入到父元件類需要做一點額外的工作。
首先,你要使用 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<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 元資料陣列)
// 子元件可以通過建構函式將該例項注入到自身。
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() {
    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';
 
@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 的生命期和應用程式的生命期一樣長。但在更復雜的應用程式環境中就不一定了。
不需要在 MissionControlComponent 中新增這個保護措施,因為它作為父元件,控制著 MissionService 的生命期。
History 日誌可以證明:在父元件 MissionControlComponent 和子元件 AstronautComponent 之間,資訊通過該服務實現了雙向傳遞。

通過匯入一個單例物件來共享資料

用ts或js寫一個單例的物件,可以共享資料