文章目录
  1. 1. NgModel相关
    1. 1.1. NgModel
    2. 1.2. NgControl
    3. 1.3. ControlValueAccessor
    4. 1.4. DefaultValueAccessor
    5. 1.5. NG_VALUE_ACCESSOR
  2. 2. 时间选择控件
    1. 2.1. datetimepicker
    2. 2.2. 创建自定义input控件
    3. 2.3. 创建自定义时间选择控件
    4. 2.4. 抽象出class继承
  3. 3. 结束语

因为项目原因又玩上了Angular2(v4.0+),《玩转Angular2》系列用于探索一些灵活或者新的用法。
本文简单介绍封装使用ngModol实现自定义表单控件的过程。

NgModel相关


NgModel

NgModel用于从作用域创建一个FormControl实例,并将它绑定到一个表单控制元素。

  • [ngModel]: 单向绑定,作用域变更将同步到UI木板
  • [(ngModel)]: 双向绑定,UI模版的变更也将同步到作用域

NgModel继承自NgControl

NgControl

NgControl是所有控制指令继承的基础类。它将一个FormControl绑定到DOM元素。

FormControlFormGroupFormArray,三者都用于angular表单的值和状态的跟踪,区别在于是一个控件、一组控件或者是它们的组合。

AbstractControl是三个具体表单类的抽象基类。并为它们提供了一些共同的行为和属性,其中有些是可观察对象(Observable)。
FormControl用于跟踪一个单独的表单控件的值和有效性状态。它对应于一个HTML表单控件,比如输入框和下拉框。
FormGroup用于跟踪一组AbstractControl的实例的值和有效性状态。该组的属性中包含了它的子控件。组件中的顶级表单就是一个FormGroup
FormArray用于跟踪AbstractControl实例组成的有序数组的值和有效性状态。

ControlValueAccessor

ControlValueAccessor用于在控制和原生元素之间建立联系,它封装了赋值到一个表现为input元素的DOM元素。

简单说,就是angular中的input是带有[(ngModel)]这个属性的,而我们想要自己控制这个input的写入过程,使用ControlValueAccessor就可以做到。

ControlValueAccessor提供以下接口:

  • writeValue(obj: any) : void: 写入值到元素
  • registerOnChange(fn: any) : void: 设置当控件接收到change事件时触发的回调
  • registerOnTouched(fn: any) : void: 设置当控件接收到touch事件时触发的回调
  • setDisabledState(isDisabled: boolean) : void: 该函数将在控件状态或者disabled值变化,根据值来对元素启用或失效

ControlValueAccessor继承自DefaultValueAccessor

DefaultValueAccessor

DefaultValueAccessor提供值写入和监听变化的默认访问,像NgModel, FormControlDirective, 和FormControlName指令会使用。

DefaultValueAccessor提供类包括:

  • onChange : (_: any) => {}: change事件变化监听
  • onTouched : () => {}: touch事件变化监听

以及ControlValueAccessor(上面)的接口。

NG_VALUE_ACCESSOR

NG_VALUE_ACCESSOR提供一个ControlValueAccessor供表单控制使用。

时间选择控件


datetimepicker

这里我们主要使用一个Bootstrap和jQuery的日期时间选择器插件–bootstrap-datetimepick

先简单介绍一下,我们可以使用该插件方便地进行日期和时间选择,从最大的十年视图到最小的分钟选择都可以自行调整。
具体一些配置项大家可以到官网上查看,这里就不详细介绍了,后面代码用到的会简单进行说明。

首先我们需要下载代码,这里放在了assets/plugins/datepicker文件夹里面。

然后添加进我们的应用程序就可以使用了:

1
2
3
// boostrap.ts
require('./assets/plugins/datepicker/bootstrap-datetimepicker.min.js');
require('./assets/plugins/datepicker/bootstrap-datetimepicker.zh-CN.js');

创建自定义input控件

我们想要封装后的组件跟原生的angular组件一样,像表现为input的自定义控件,我们想要使用[(ngModel)]来进行双向绑定,我们需要使用ControlValueAccessor来拓展。

而这里ControlValueAccessor只是一个接口,我们应用它,还需要获取一些可用的服务,这时候需要注入NG_VALUE_ACCESSOR

1
2
3
4
5
export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
};

这里简单讲讲几个概念:

  1. 我们自定义了一个访问控制服务,该服务包装为NG_VALUE_ACCESSOR服务,主要用于控制ControlValueAccessor相关的访问。
  2. 我们需要将自定义input控件提供给NG_VALUE_ACCESSOR,以便通过自定义方式控制父组件的[(ngModel)]以及其他表单相关的访问。
  3. forwardRef用于将目前还未获取到的依赖关联起来,这里我们关联后面的自定义Input组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Component({
selector: 'custom-input',
template: `<input [(ngModel)]="value" class="form-control" (blur)="onBlur()" />`,
providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR] // 注入访问控制服务
})
// 继承ControlValueAccessor接口
export class CustomInputComponent implements ControlValueAccessor{

// 内部model值
private innerValue: any = '';

// 定义ControlValueAccessor提供的事件回调
private onTouched: () => void = noop;
private onChange: (_: any) => void = noop;

// 获取值的访问
get value(): any {
return this.innerValue;
};

// 设置值,同时触发change回调
set value(v: any) {
if (v !== this.innerValue) {
this.innerValue = v;
this.onChange(v);
}
}

// 失焦时触发回调
onBlur() {
this.onTouched();
}

// 表单ControlValueAccessor接口
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}

// 表单ControlValueAccessor接口
registerOnChange(fn: any) {
this.onChange = fn;
}

// 表单ControlValueAccessor接口
registerOnTouched(fn: any) {
this.onTouched = fn;
}
}

具体的实现实例参考Angular2 + Connect custom component to ngModel

创建自定义时间选择控件

像我们定义一个时间选择控件,一般需要对外提供一些配置:

  • 选择精度(或自定义视图范围)
    • 这里提供:分钟(默认)、小时和天
  • 可选日期范围
  • 是否禁用
  • 是否必填

以及通常我们提供一个值变更的回调,像(change)这样的事件。
下面看看代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import {Component, Input, AfterViewInit, ElementRef, EventEmitter, Output, forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';

export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DateTimePickerComponent),
multi: true
};

@Component({
selector: 'date-time-picker',
template: `<input type="text" class="form-control" [disabled]="disabled" [(ngModel)]="value" (blur)="onBlur()" />`,
providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR],
})
// ControlValueAccessor: A bridge between a control and a native element.
export class DateTimePickerComponent implements AfterViewInit, ControlValueAccessor {
@Input() accuracy: string; // 日期选择的精度,默认分钟,可选 (hour | day)
@Input() startDate: string; // 可选最早日期
@Input() endDate: string; // 可选最晚日期
@Input() maxView: string; // 最大视图
@Input() disabled: boolean = false; // 是否禁用

@Output() change = new EventEmitter<any>(); // input的change事件回调

private el; // 控件元素
private model: any; // 内部model值

// 定义ControlValueAccessor提供的事件回调
private onChange: (_: any) => void;
private onTouched: () => void;

constructor(el: ElementRef) {
this.el = el;
}

// Lifecycle hook that is called after a component's view has been fully initialized.
ngAfterViewInit() {
/*
source:http://www.bootcss.com/p/bootstrap-datetimepicker/
minView: default 2
maxView: default 4
0 or 'hour' for the hour view (小时视图)
1 or 'day' for the day view 0-24h (日视图)
2 or 'month' for month view (the default) (月视图)
3 or 'year' for the 12-month overview (年视图)
4 or 'decade' for the 10-year overview. Useful for date-of-birth datetimepickers. (十年视图)
*/
let format = 'yyyy-mm-dd hh:ii:00'; // 默认选择分钟,故秒数为00,datetimepicker不支持秒数的选择
let minView = 0; // 默认最小视图为分钟

if (this.accuracy === 'hour') {
format = 'yyyy-mm-dd hh:00'; // 设置为小时,分钟和秒数需为00
minView = 1; // 最小视图为小时
} else if (this.accuracy === 'day') {
format = 'yyyy-mm-dd 00:00:00'; // 设置为天
minView = 2; // 最小视图为日期
}

$(this.el.nativeElement).find('input').datetimepicker({
language: 'zh-CN',
autoclose: true, // 选择日期后自动关闭
maxView: parseInt(this.maxView, 10) || 4, // 选择的最大视图,4为十年视图
startDate: this.startDate || '', // 最早可选日期,默认不限制
endDate: this.endDate || '', // 最晚可选日期,默认不限制
format, // 格式化
minView, // 选择的最小视图
})
.on('hide', ev => { // 这里需要注意,我们使用箭头函数() => {},则不会更改this的指向
this.value = $(ev.target).val(); // 更新值
this.change.emit({value: $(ev.target).val()}); // 触发回调
});
}

// 获取值的访问
get value(): any {
return this.model;
}

// 设置值,同时触发change回调
set value(v: any) {
if (v !== this.model) {
this.model = v;
this.onChange(v);
}
}

// 失焦时触发回调
onBlur() {
this.onTouched();
}

// 父组件的值变更时,更新model的值
writeValue(value: string): void{
if (value !== this.model) {
this.model = value;
}
}

// 表单ControlValueAccessor接口
registerOnChange(fn: (_: any) => {}): void {
this.onChange = fn;
}

// 表单ControlValueAccessor接口
registerOnTouched(fn: () => {}): void {
this.onTouched = fn;
}
}

抽象出class继承

我们可以把相同的方法抽象出来,通过继承的方式,这样就能在多个相似组件通用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import {forwardRef} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';

export function customInputAccessor(component){
return {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => component),
multi: true
};
}

export class CustomInputComponent implements ControlValueAccessor {
private model: any; // 控件的值
private onChange: (_: any) => void;
private onTouched: () => void;

// get accessor
get value(): any {
return this.model;
}

// set accessor including call the onchange callback
set value(v: any) {
if (v !== this.model) {
this.model = v;
this.onChange(v);
}
}

// Set touched on blur
onBlur() {
this.onTouched();
}

writeValue(value: string): void{
if (value !== this.model) {
this.model = value;
}
}

// Set the function to be called when the control receives a change event.
registerOnChange(fn: (_: any) => {}): void {
this.onChange = fn;
}

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

在datetimepick中继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {CustomInputComponent, customInputAccessor} from '../class/custom-input.class';

@Component({
selector: 'date-time-picker',
template: `<input type="text" class="form-control" [disabled]="disabled" [(ngModel)]="value" (blur)="onBlur()" />`,
providers: [customInputAccessor(DateTimePickerComponent)],
})
export class DateTimePickerComponent extends CustomInputComponent implements AfterViewInit {
@Input() accuracy: string; // 日期选择的精度,默认分钟,可选 (hour | day)
@Input() startDate: string; // 可选最早日期
@Input() endDate: string; // 可选最晚日期
@Input() maxView: string; // 最大视图
@Input() disabled: boolean = false; // 是否禁用

@Output() change = new EventEmitter<any>(); // input的change事件回调

private el; // 控件元素

constructor(el: ElementRef) {
super(); // 继承
this.el = el;
}
ngAfterViewInit() {
// 原本的内容
}
}

效果图:

image

结束语


这节我们讲了自定义表单相关的一些概念,以及自定义一个时间选择input表单的实现过程。
很多时候我们都需要对不同的input自行封装,所以也可以单独抽象出来Class方便继承,又或者封装成指令等方式都是可以的。
此处查看项目代码
此处查看页面效果

码生艰难,写文不易,给我家猪囤点猫粮了喵~

B站: 被删

查看Github有更多内容噢:https://github.com/godbasin
更欢迎来被删的前端游乐场边撸猫边学前端噢

如果你想要关注日常生活中的我,欢迎关注“牧羊的猪”公众号噢

作者:被删

出处:https://godbasin.github.io

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章目录
  1. 1. NgModel相关
    1. 1.1. NgModel
    2. 1.2. NgControl
    3. 1.3. ControlValueAccessor
    4. 1.4. DefaultValueAccessor
    5. 1.5. NG_VALUE_ACCESSOR
  2. 2. 时间选择控件
    1. 2.1. datetimepicker
    2. 2.2. 创建自定义input控件
    3. 2.3. 创建自定义时间选择控件
    4. 2.4. 抽象出class继承
  3. 3. 结束语