因为项目原因又玩上了Angular2(v4.0+),《玩转Angular2》系列用于探索一些灵活或者新的用法。 本文简单介绍封装使用ngModol实现自定义表单控件的过程。
NgModel相关
NgModel NgModel
用于从作用域创建一个FormControl
实例,并将它绑定到一个表单控制元素。
[ngModel]
: 单向绑定,作用域变更将同步到UI木板
[(ngModel)]
: 双向绑定,UI模版的变更也将同步到作用域
NgModel
继承自NgControl
。
NgControl NgControl
是所有控制指令继承的基础类。它将一个FormControl
绑定到DOM元素。
FormControl
、FormGroup
和FormArray
,三者都用于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 require ('./assets/plugins/datepicker/bootstrap-datetimepicker.min.js' );require ('./assets/plugins/datepicker/bootstrap-datetimepicker.zh-CN.js' );
我们想要封装后的组件跟原生的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 };
这里简单讲讲几个概念:
我们自定义了一个访问控制服务,该服务包装为NG_VALUE_ACCESSOR
服务,主要用于控制ControlValueAccessor
相关的访问。
我们需要将自定义input控件提供给NG_VALUE_ACCESSOR
,以便通过自定义方式控制父组件的[(ngModel)]
以及其他表单相关的访问。
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 ] }) export class CustomInputComponent implements ControlValueAccessor { private innerValue : any = '' ; private onTouched : () => void = noop; private onChange : (_: any ) => void = noop; get value (): any { return this .innerValue ; }; set value (v: any ) { if (v !== this .innerValue ) { this .innerValue = v; this .onChange (v); } } onBlur ( ) { this .onTouched (); } writeValue (value: any ) { if (value !== this .innerValue ) { this .innerValue = value; } } registerOnChange (fn: any ) { this .onChange = fn; } 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 ], }) export class DateTimePickerComponent implements AfterViewInit , ControlValueAccessor { @Input () accuracy : string; @Input () startDate : string; @Input () endDate : string; @Input () maxView : string; @Input () disabled : boolean = false ; @Output () change = new EventEmitter <any>(); private el; private model : any; private onChange : (_: any ) => void ; private onTouched : () => void ; constructor (el: ElementRef ) { this .el = el; } ngAfterViewInit ( ) { let format = 'yyyy-mm-dd hh:ii:00' ; let minView = 0 ; if (this .accuracy === 'hour' ) { format = 'yyyy-mm-dd hh: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 , startDate : this .startDate || '' , endDate : this .endDate || '' , format, minView, }) .on ('hide' , ev => { this .value = $(ev.target ).val (); this .change .emit ({value : $(ev.target ).val ()}); }); } get value (): any { return this .model ; } set value (v: any ) { if (v !== this .model ) { this .model = v; this .onChange (v); } } onBlur ( ) { this .onTouched (); } writeValue (value : string): void { if (value !== this .model ) { this .model = value; } } registerOnChange (fn : (_: any ) => {}): void { this .onChange = fn; } 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 value (): any { return this .model ; } set value (v: any ) { if (v !== this .model ) { this .model = v; this .onChange (v); } } onBlur ( ) { this .onTouched (); } writeValue (value : string): void { if (value !== this .model ) { this .model = value; } } registerOnChange (fn : (_: any ) => {}): void { this .onChange = fn; } 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; @Input () startDate : string; @Input () endDate : string; @Input () maxView : string; @Input () disabled : boolean = false ; @Output () change = new EventEmitter <any>(); private el; constructor (el: ElementRef ) { super (); this .el = el; } ngAfterViewInit ( ) { } }
效果图:
结束语
这节我们讲了自定义表单相关的一些概念,以及自定义一个时间选择input表单的实现过程。 很多时候我们都需要对不同的input自行封装,所以也可以单独抽象出来Class方便继承,又或者封装成指令等方式都是可以的。此处查看项目代码 此处查看页面效果
查看Github有更多内容噢:https://github.com/godbasin
更欢迎来被删的前端游乐场 边撸猫边学前端噢
如果你想要关注日常生活中的我,欢迎关注“牧羊的猪”公众号噢