這幾天我們已經完成了 TODO 待辦事項
的一些基本功能,涉及多個元件的使用方式,今天我們將 TODO 待辦事項
的一些元件獨立出來維護,介紹一些元件設計的小方法後,我們將對這個專案裡涉及的元件進行一一講解,有助於大家更加深刻地理解 NG-ZORRO 的常用元件。
看一下我们目前的项目情况:
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
类型的数组,然后渲染需要的数据,咋一看并没有什么问题,但是当尝试去新增一个新数据的时候出现了问题,我们看一下:
發現問題了嗎?我們在 TaskListComponent
元件中完成的任務又被添加回來了,原因很簡單,就是 TodoComponent
的 listOfTodoTasks
資料和 TaskListComponent
的 listOfTasks
資料不是同步的,我們可以通過 雙向繫結
的方法來實現資料同步(對於通過 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
雙向繫結的元件已經完成了,我們再來看看 這個例子 ,現在已經能正常渲染資料了:
今天我們介紹瞭如何通過 implements ControlValueAccessor
來實現自定義元件的雙向繫結,這對於一些表單業務場景有很大的作用,能夠保證我們同一份資料在多元件模組下的同步問題。
之前在 待辦事項
專案中,很多元件都是使用了最簡單常用的使用方式和屬性,我們在接下來幾天會對這個專案中涉及的元件進行專項解讀,幫助大家更容易地理解怎麼使用這些元件。