iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 6
2
Modern Web

Angular 元件庫 NG-ZORRO 基礎入門系列 第 6

[Angular 元件庫 NG-ZORRO 基礎入門] Day 06 - 待辦事項 + 雙向繫結

前言回顧

這幾天我們已經完成了 TODO 待辦事項 的一些基本功能,涉及多個元件的使用方式,今天我們將 TODO 待辦事項 的一些元件獨立出來維護,介紹一些元件設計的小方法後,我們將對這個專案裡涉及的元件進行一一講解,有助於大家更加深刻地理解 NG-ZORRO 的常用元件。

看一下我们目前的项目情况:
https://ithelp.ithome.com.tw/upload/images/20190907/20112829osviDG40qC.png

待辦事項

選單目錄

todo
├── task-detail
│   ├── task-detail.component.html
│   ├── task-detail.component.less
│   └── task-detail.component.ts
├── todo.component.html
├── todo.component.less
└── todo.component.ts

我們看到,待辦事項 專案檔案結構如圖,我們現在只將 task 詳情抽離出來,看一下 todo.component.html 裡渲染待辦任務列表的程式碼,這裡顯示邏輯全部寫在該 component 裡面,對於後續維護十分困難:

<div class="task-container">
  <div class="task-todo">
    <nz-table [nzData]="listOfTodoTasks" [nzNoResult]="noResultTpl" [nzFrontPagination]="false" [nzShowPagination]="false" [nzWidthConfig]="tableWidthConfig">
      <tbody>
        <tr *ngFor="let task of listOfTodoTasks">
          <td
            nzShowCheckbox
            (nzCheckedChange)="checkTask(task)"
          ></td>
          <td>{{task.name}}</td>
          <td>
            <i class="more-actions" nz-icon nzType="ellipsis" nz-dropdown [nzDropdownMenu]="actions" nzPlacement="bottomRight" nzTrigger="click" (click)="setActivatedTask(task)"></i>
          </td>
        </tr>
      </tbody>
    </nz-table>
  </div>
</div>

列表元件

那麼我們想剝離列表部分,作為一個獨立模組來維護該怎麼做呢?

建立元件

很簡單,讓我們先建立一個元件 task-list,然後看下我們新的專案結構:

$ cd ng-zorro-ironman2020
$ ng g c components/demos/todo/task-list --skip-import
todo
├── task-detail
│   ├── task-detail.component.html
│   ├── task-detail.component.less
│   └── task-detail.component.ts
├── task-list
│   ├── task-list.component.html
│   ├── task-list.component.less
│   └── task-list.component.ts
├── todo.component.html
├── todo.component.less
└── todo.component.ts

元件設計

我們把列表相關的程式碼全部遷移至 TaskListComponent,這時我們面臨一個問題,如何渲染待辦任務資料並和 TodoComponent 內的資料同步。

我們先來第一種設計方案(stackblitz 線上程式碼演示):

@Component({
  selector       : 'app-task-list',
  templateUrl    : './task-list.component.html',
  styleUrls      : [ './task-list.component.less' ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TaskListComponent implements OnInit, ControlValueAccessor {
  @Input() listOfTasks: ITask[] = [];
  constructor() {}
  ngOnInit() {}
}

task-list.component.html 使用方式為:

<app-task-list [listOfTasks]="listOfTodoTasks"></app-task-list>

todo.component.ts 新增任務方法仍然不變:

addTask(): void {
  if (this.createForm.valid) {
	const newTask = {
	  ...this.createForm.getRawValue(),
	  id: new Date().getTime()
	};
	this.listOfTodoTasks = this.listOfTodoTasks.concat([ newTask ]);
	// reset after adding new task
	this.createForm.get('name').reset();
  }
}

我们接受一个 ITask 类型的数组,然后渲染需要的数据,咋一看并没有什么问题,但是当尝试去新增一个新数据的时候出现了问题,我们看一下:

https://img.alicdn.com/tfs/TB1BCcyfbj1gK0jSZFOXXc7GpXa-1444-558.gif

發現問題了嗎?我們在 TaskListComponent 元件中完成的任務又被添加回來了,原因很簡單,就是 TodoComponentlistOfTodoTasks 資料和 TaskListComponentlistOfTasks 資料不是同步的,我們可以通過 雙向繫結 的方法來實現資料同步(對於通過 API 方式請求資料渲染的場景可以通過 Rxjs 的 Subject 去訂閱重新渲染,我們暫時不對這種情況深入討論)。

雙向繫結

瞭解雙向繫結

對於上面的例子,如果我們要以 雙向繫結 模式來使用的話,該怎麼寫呢?

<app-task-list [(ngModel)]="listOfTodoTasks"></app-task-list>

那麼到底什麼是 ngModel 呢?看一下 官方介紹,當然,要想使用 ngModel,別忘了引入 FormsModule

它可以接受一個領域模型作為可選的 Input。如果使用 [] 語法來單向繫結到 ngModel,那麼在元件類中修改領域模型將會更新檢視中的值。 如果使用 [()] 語法來雙向繫結到 ngModel,那麼檢視中值的變化會隨時同步回元件類中的領域模型。

如何在自定義元件中實現雙向繫結

在官方文件 模板語法 一節中,專門提到了 ngModel 的實現過程

ngModel 輸入屬性會設定該元素的值,並透過 ngModelChange 的輸出屬性來監聽元素值的變化。
各種元素都有很多特有的處理細節,因此 NgModel 指令只支援實現了ControlValueAccessor的元素, 它們能讓元素適配本協議。 輸入框正是其中之一。 Angular 為所有的基礎 HTML 表單都提供了值訪問器(Value accessor),表單一章展示瞭如何繫結它們。
你不能把 [(ngModel)] 用到非表單類的原生元素或第三方自訂元件上,除非寫一個合適的值訪問器,這種技巧超出了本章的範圍。
你自己寫的 Angular 元件不需要值訪問器,因為你可以讓值和事件的屬性名適應 Angular 基本的雙向繫結語法,而不使用 NgModel。 前面看過的 sizer就是使用這種技巧的例子。

文件中提到“NgModel 指令只支援實現了ControlValueAccessor的元素”,很顯然我們的元件需要繼承 ControlValueAccessor,看到以下定義,我們只需要繼承需要的功能,註冊 NG_VALUE_ACCESSOR 即可實現。

interface ControlValueAccessor {
  writeValue(obj: any): void
  registerOnChange(fn: any): void
  registerOnTouched(fn: any): void
  setDisabledState(isDisabled: boolean)?: void
}

開始改造

讓我們改造一下 TaskListComponent,開啟 task-list.component.ts,重寫如下程式碼:

import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'app-task-list',
  templateUrl: './task-list.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  // register NG_VALUE_ACCESSOR to support ngModel
  providers      : [
    {
      provide    : NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => TaskListComponent),
      multi      : true
    }
  ]
})
export class TaskListComponent implements OnInit {
  // 部分程式碼已省略
  listOfTasks: ITask[] = [];
  constructor(
    private cdr: ChangeDetectorRef,
  ) {}
  ngOnInit() {}
  
  checkTask(task: ITask): void {
    this.listOfTasks = this.listOfTasks.filter(v => v.id !== task.id);
    // ngModelChange事件,同步資料
    this.onChange(this.listOfTasks);
  }
  
  /**
   * Update ngModel -> update listOfSelectedValue
   */
  onChange: (value: ITask[]) => void = () => [];
  onTouched: () => void = () => null;

  writeValue(tasks: ITask[]): void {
    if (tasks) {
      this.listOfTasks = [ ...tasks ];
      // markForCheck to render table data
      this.cdr.markForCheck();
    }
  }

  registerOnChange(fn: (value: ITask[]) => void): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }
}

這樣,一個支援 ngModel 雙向繫結的元件已經完成了,我們再來看看 這個例子 ,現在已經能正常渲染資料了:
https://img.alicdn.com/tfs/TB1uR7xfbH1gK0jSZFwXXc7aXXa-1476-414.gif

總結 & 預告

今天我們介紹瞭如何通過 implements ControlValueAccessor 來實現自定義元件的雙向繫結,這對於一些表單業務場景有很大的作用,能夠保證我們同一份資料在多元件模組下的同步問題。

之前在 待辦事項 專案中,很多元件都是使用了最簡單常用的使用方式和屬性,我們在接下來幾天會對這個專案中涉及的元件進行專項解讀,幫助大家更容易地理解怎麼使用這些元件。

相關資源


上一篇
[Angular 元件庫 NG-ZORRO 基礎入門] Day 05 - 待辦事項 + Form
下一篇
[Angular 元件庫 NG-ZORRO 基礎入門] Day 07 - Table:排序 / 篩選
系列文
Angular 元件庫 NG-ZORRO 基礎入門30

尚未有邦友留言

立即登入留言