前言回顾
这几天我们已经完成了 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
来实现自定义组件的双向绑定,这对于一些表单业务场景有很大的作用,能够保证我们同一份数据在多组件模块下的同步问题。
之前在 待办事项
项目中,很多组件都是使用了最简单常用的使用方式和属性,我们在接下来几天会对这个项目中涉及的组件进行专项解读,帮助大家更容易地理解怎么使用这些组件。
相关资源
[Angular 元件庫 NG-ZORRO 基礎入門] Day 06 - 待辦事項 + 雙向繫結 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天ithelp.ithome.com.tw
simplejason/ng-zorro-ironman2020github.com
angular-s3c1qh-qplzgm - StackBlitzstackblitz.com
Angular - NgModelangular.cn
Angular - ControlValueAccessorangular.cn