前端框架的强大无疑给开发者省去了不少烦恼,又因比较完善的UI库支撑,让部分后端开发者能够省去大量样式设计的时间成本,纵然如此,业务的多变性是框架本身无法预料的,很多的控件功能在实际开发中总是不够完善和灵活,所以需要开发者结合业务需求进行再次封装这些UI控件/组件。
表单控件
常规组件只需要根据官方指引,写好数据传输的方式和订阅即可任意使用,表单控件有点特殊,按照常规方式写出来的组件使用在表单中,绑定ngModel或者formControlName,随之而来的是一个报错:
RROR Error: No value accessor for form control with name: 'userName'
ControlValueAccessor
Defines an interface that acts as a bridge between the Angular forms API and a native element in the DOM
只有实现了这个接口才可以完成像普通表单元素那样使用和验证。
interface ControlValueAccessor { writeValue(obj: any): void registerOnChange(fn: any): void registerOnTouched(fn: any): void setDisabledState(isDisabled: boolean)?: void }
你的控件必须包含上述方法;此外,控件内部要有value的get实现,以及最好有个与value等值的别名变量(想不明白别急,看代码);一个简单的input控件封装应该类似这样:
export class MyInputComponent implements OnInit, ControlValueAccessor { value: string | number; @Input() disabled: boolean; @Input() placeholder: string; @Input() type = 'text'; constructor() { } ngOnInit() { } writeValue(data: any) { this.value = data; } registerOnChange(fn: any) { } registerOnTouched(fn: any) { } setDisabledState(disabled: boolean) { this.disabled = disabled; } }
其实封装工作只完成一半,组件装饰器元数据完整:
@Component({ // tslint:disable-next-line: component-selector selector: 'my-input', templateUrl: './my-input.component.html', styleUrls: ['./my-input.component.scss'], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MyInputComponent), multi: true }] })
至此,控件在form表单中使用不会报错;表单内放置一个查询按钮,用来输出表单状态:
<form nz-form [formGroup]="form" (ngSubmit)="submit(form)"> <div nz-row nzFlex [nzGutter]="8"> <div nz-col [nzSpan]="6"> <nz-form-item> <nz-form-label [nzSpan]="10">userName</nz-form-label> <nz-form-control [nzSpan]="14"> <my-input formControlName="userName"></my-input> </nz-form-control> </nz-form-item> </div> </div> <button nz-button type="submit" [nzType]="'primary'">查询</button> </form>
ngOnInit() { this.form = this.fb.group({ userName: [2] }); } submit(form: FormGroup) { console.log(form); }
封装控件内部:
<div> <input type="{{type}}" value="{{value}}" placeholder="{{placeholder}}"> </div>
通过formControlName的绑定方式将userName传入控件,控件通过writeValue方法接收并赋值到自身属性value,用于与原生input交互,此时我们手动输入内容为数字3,然后打印:
可以看到表单没有获取到最新的值,这是因为目前位置表单获取组件的value还是初始值,我们也没有提供改变value的方法机制,修改html:
<div> <input type="{{type}}" [ngModel]="actualValue" placeholder="{{placeholder}}" (ngModelChange)="modelChange($event)"> </div>
这里稍微解释input绑定数据与触发的更新方法可以选择原生的value和input进行更新,也可以选择ng提供的ngModel和ngModelChange事件更新控件,区别在于使用原生input的输入事件,要使用到事件对象展开找到元素的value属性值;而使用ng官方框架自带的事件,事件对象$event就是最新的value值。
新增set value方法:
set value(data) { this.actualValue = data; // 通知表单value更新 this.onChange(data); } registerOnChange(fn: any) { // 注册表单的value改变通知方法 this.onChange = fn; } modelChange(event) { this.value = event; }
输入 3 ,查询打印:
实现原生input基础属性
这个几乎是一条默认的规则,封装的控件至少实现原生input的基础属性功能,在此基础上再进行满足业务需求。
type
maxlength
minlength
placeholder
......
这里只讨论type为text和number的情况,radio等其它类型没必要深入。
我们不能直接使用maxlength进行与input绑定,至少写法不是很好,比较妥善的做法是动态的判断长度值,并且将正确的值设置到原生input属性中。
为此修改html:
<div> <input type="{{type}}" #inputElement [(ngModel)]="actualValue" placeholder="{{placeholder}}" (ngModelChange)="modelChange($event)" > </div>
注入 Renderer2,用于对原生元素操作