iT邦幫忙

2022 iThome 鐵人賽

DAY 18
0
Modern Web

angular專案開發指南系列 第 18

Angular 組件間資料的傳遞方式

  • 分享至 

  • xImage
  •  

前言

所有前端框架都會遇到一個共同的問題,就是資料與狀態如何在組件之間傳遞,根據元件之間不同的組合關係,大致可分類為父子,兄弟與沒有關係,在 Angular 中資料的傳遞,除了有直接父子關係的組件組合外,當遇到跨級或沒有關係的組件之間傳遞資料時,我選擇使用 Rxjs 來協助完成工作,本篇與大家分享各種資料傳遞方式並實現幾個簡單的範例。


直接父子元件 - @Input() 和 @Output()

父元件和一個或多個直接子元件之間共享資料。可以用 @Input()@Output() 來實現這個模式。

把資料傳送到子元件

input

target

把資料傳送到父元件

output

同時使用 @Input() 和 @Output()

diagram

以上是引用 Angular 官網的教學文件

官網範例


在 my-app 中實現父子元件溝通範例

新需求是在 my-app 中建一個土地管理頁面,讓使用者自行輸入土地號碼到資料庫,考量到之後還會用到的輸入功能,決定把它作為子元件抽取出來,讓父元件負責顯示,子元件負責輸入。

土地管理頁面父元件 - 負責顯示土地號碼內容

範例中我們在 src\app 中建立一個 LandComponent 的共用元件並設定路由,詳細過程請參考前面章節。

p119

[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>

[LandComponent]

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 的共用元件。

[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>

[InputDataComponent]

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 {}
}

最後可規劃在 LandComponentInputDataComponent 實作 API 調用。


非直接父子元件 - Rxjs

Angular 可以利用 Rxjs BehaviorSubject 觀察與訂閱的特性處理非直接父子元件之間的資料溝通,例如 A元件 將運行狀態存入 BehaviorSubject 再由 B元件 去訂閱 A元件 的狀態,當 A元件 狀態有所改變,B元件 就會收到通知,達到資料傳遞的效果。

安裝 Rxjs

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 顯示土地總筆數的元件。

[LandRecordComponent]

src\app\land-record\land-record.component.html

<div class="land-record">
    <div>
        <strong>(兄弟元件)</strong>
    </div>
    <br />
    <label> 總筆數 </label>
    <span>
        {{ landRecords }}
    </span>
</div>

[LandRecordComponent]

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 有興趣的可以參考看看。


成果畫面

g9


結論

本篇用 @Input() 和 @Output() 實現直接父子元件通訊,用 Rxjs Observable 實現兄弟元件通訊,並以實際案例製作一個 my-app 的土地管理頁面範例。

下一篇再進一步介紹如何自己製作共用的指令與管道。


參考

Practical observable usage

Using observables

The RxJS library

Sharing data between child and parent

Angular:在元件之間傳遞資料的4種方式

angular組件間的信息傳遞

rxjs-primer

ngrx.io


上一篇
Angular 專業圖表套件 - Ngx-Echarts
下一篇
製作共用指令與管道
系列文
angular專案開發指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言