所有前端框架都會遇到一個共同的問題,就是資料與狀態如何在組件之間傳遞,根據元件之間不同的組合關係,大致可分類為父子,兄弟與沒有關係,在 Angular 中資料的傳遞,除了有直接父子關係
的組件組合外,當遇到跨級或沒有關係的組件之間傳遞資料時,我選擇使用 Rxjs
來協助完成工作,本篇與大家分享各種資料傳遞方式並實現幾個簡單的範例。
父元件
和一個或多個直接子元件
之間共享資料。可以用 @Input() 和 @Output() 來實現這個模式。
以上是引用 Angular 官網的教學文件
新需求是在 my-app 中建一個土地管理頁面,讓使用者自行輸入土地號碼到資料庫,考量到之後還會用到的輸入功能
,決定把它作為子元件
抽取出來,讓父元件負責顯示,子元件負責輸入。
範例中我們在 src\app
中建立一個 LandComponent
的共用元件並設定路由,詳細過程請參考前面章節。
src\app\land\land.component.html
<nb-card class="land">
<strong>土地號碼列表 (父元件)</strong>
<!-- [data] => 傳到子元件 (newItemEvent) => 傳回父元件 -->
<app-input-data [data]="items" (newItemEvent)="actionFromChild($event)"></app-input-data>
<ul>
<!-- 土地號碼列表 -->
<li *ngFor="let item of items">
{{ item }}
</li>
</ul>
</nb-card>
src\app\land\land.component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-land',
templateUrl: './land.component.html',
styleUrls: ['./land.component.scss'],
})
export class LandComponent implements OnInit {
constructor() {}
items = ['Default : 大同區0001-0000地號'];
/**
* ### 父元件接收來自子元件的操作
*
* - type: add 加入
* - type: remove 刪除
*
* @param {string} $event
* @memberof LandComponent
*/
actionFromChild($event: string) {
const doFromChildObj = JSON.parse($event);
if (doFromChildObj.type === 'add') this.items.push(doFromChildObj.item);
else {
const idx = this.items.indexOf(doFromChildObj.item);
if (!!~idx) this.items.splice(idx, 1);
}
}
ngOnInit(): void {}
}
如同共用服務 Http 一樣,共用的元件也可以再建一個公用模組 shared
來管理,可自行規劃本篇不詳細說明。
範例中我們在 src\app\shared\components
中建立一個 InputDataComponent
的共用元件。
src\app\shared\components\input-data\input-data.component.html
<nb-layout-column class="colored-column-info">
<p for="">地號 (子元件)</p>
<input type="text" nbInput placeholder="輸入地號" [(ngModel)]="item" />
<button nbButton status="primary" (click)="create('add')">加入</button>
<button nbButton status="danger" (click)="delete('remove')">刪除</button>
</nb-layout-column>
src\app\shared\components\input-data\input-data.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-input-data',
templateUrl: './input-data.component.html',
styleUrls: ['./input-data.component.scss'],
})
export class InputDataComponent implements OnInit {
constructor() {}
item = '';
/**
* ### 從父元件傳來的資料
*
* @type {*}
* @memberof InputDataComponent
*/
@Input() data: any;
/**
* ### 傳回父元件的事件
*
* @memberof InputDataComponent
*/
@Output() newItemEvent = new EventEmitter<string>();
/**
* ### 加入地號
*
* @param {string} type
* @memberof InputDataComponent
*/
create(type: string) {
this.newItemEvent.emit(
JSON.stringify({
type,
item: this.item,
})
);
}
/**
* ### 刪除輸入的地號
*
* @param {string} type
* @memberof InputDataComponent
*/
delete(type: string) {
this.newItemEvent.emit(
JSON.stringify({
type,
item: this.item,
})
);
}
ngOnInit(): void {}
}
最後可規劃在
LandComponent
或InputDataComponent
實作 API 調用。
Angular 可以利用 Rxjs BehaviorSubject
觀察與訂閱的特性處理非直接父子元件之間的資料溝通,例如 A元件 將運行狀態存入 BehaviorSubject
再由 B元件 去訂閱 A元件 的狀態,當 A元件 狀態有所改變,B元件 就會收到通知,達到資料傳遞的效果。
Install Rxjs
npm i -S rxjs
範例中我們在 src\app\core\state\land-record
中建立一個 LandRecordService
Rxjs 服務。
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class LandRecordService {
constructor() {}
// 提供訂閱服務 - landRecord
public landRecord = new BehaviorSubject<any>('');
landRecord$ = this.landRecord.asObservable();
// 寫入 landRecord$
setLandRecord(value: any): void {
this.landRecord.next(value);
}
}
src\app\land\land.component.ts
將土地總筆數寫入 Rxjs。import { LandRecordService } from '@core/state';
@Component({
selector: 'app-land',
templateUrl: './land.component.html',
styleUrls: ['./land.component.scss'],
})
export class LandComponent implements OnInit {
constructor(private landRecordService: LandRecordService) {}
...
actionFromChild($event: string) {
...
// 狀態寫入 landRecord$
this.landRecordService.setLandRecord(this.items.length);
}
ngOnInit(): void {
// 狀態寫入 landRecord$
this.landRecordService.setLandRecord(this.items.length);
}
}
src\app\land-record
建立一個 LandRecordComponent
顯示土地總筆數的元件。src\app\land-record\land-record.component.html
<div class="land-record">
<div>
<strong>(兄弟元件)</strong>
</div>
<br />
<label> 總筆數 </label>
<span>
{{ landRecords }}
</span>
</div>
src\app\land-record\land-record.component.ts
import { Component, OnInit } from '@angular/core';
import { LandRecordService } from '@core/state';
@Component({
selector: 'app-land-record',
templateUrl: './land-record.component.html',
styleUrls: ['./land-record.component.scss'],
})
export class LandRecordComponent implements OnInit {
constructor(private landRecordService: LandRecordService) {}
// landRecord$
landRecordRxjs: any = null;
// 總筆數
landRecords = 0;
ngOnInit(): void {
// 訂閱 landRecord$
this.landRecordRxjs = this.landRecordService.landRecord$.subscribe((resp) => {
// 更新總筆數
this.landRecords = resp;
});
}
ngOnDestroy(): void {
// 取消訂閱 landRecord$
if (!!this.landRecordRxjs) this.landRecordRxjs.unsubscribe();
}
}
Angular 狀態管理套件有 ngrx/store 有興趣的可以參考看看。
本篇用 @Input() 和 @Output()
實現直接父子元件通訊,用 Rxjs Observable
實現兄弟元件通訊,並以實際案例製作一個 my-app 的土地管理頁面範例。
下一篇再進一步介紹如何自己製作共用的指令與管道。
Sharing data between child and parent