Angular封装表单控件及思想总结


Posted in Javascript onDecember 11, 2019

前言

前端框架的强大无疑给开发者省去了不少烦恼,又因比较完善的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 class="my-input">
 <input type="{{type}}" value="{{value}}" placeholder="{{placeholder}}">
</div>

通过formControlName的绑定方式将userName传入控件,控件通过writeValue方法接收并赋值到自身属性value,用于与原生input交互,此时我们手动输入内容为数字3,然后打印:

Angular封装表单控件及思想总结

可以看到表单没有获取到最新的值,这是因为目前位置表单获取组件的value还是初始值,我们也没有提供改变value的方法机制,修改html:

<div class="my-input">
 <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 ,查询打印:

Angular封装表单控件及思想总结

实现原生input基础属性

这个几乎是一条默认的规则,封装的控件至少实现原生input的基础属性功能,在此基础上再进行满足业务需求。

  1. type
  2. maxlength
  3. minlength
  4. placeholder
  5. ......

这里只讨论type为text和number的情况,radio等其它类型没必要深入。

我们不能直接使用maxlength进行与input绑定,至少写法不是很好,比较妥善的做法是动态的判断长度值,并且将正确的值设置到原生input属性中。

为此修改html:

<div class="my-input">
 <input
 type="{{type}}"
 #inputElement
 [(ngModel)]="actualValue"
 placeholder="{{placeholder}}"
 (ngModelChange)="modelChange($event)"
 >
</div>

注入 Renderer2,用于对原生元素操作

ngOnChanges(changes: SimpleChanges) {
 this.initAttributes(changes);
 }
initAttributes(changes: SimpleChanges) {
 for (const key in changes) {
  if (changes.hasOwnProperty(key)) {
  const element = changes[key];
  if (element) {
   this.render2.setProperty(this.inputElement.nativeElement, key, element.currentValue);
  }
  }
 }
 }

Validator

ngOnInit() {
 this.form = this.fb.group({
  userName: [2, [Validators.required, Validators.minLength(3)]]
 });
 }

经过打印测试,表单的状态正确 √

适当使用指令

假如此时需要对输入内容拦截处理,目前在不写input事件的情况下无法做到,假如针对一个type=number类型的输入框,设置最大值,超过这个值不会改变,原生input元素确实有max属性支撑验证,但是它无法改变value值,也就是说假如这个最大值不是必要验证属性,那么表单还是可以提交最新的超出值,用指令可以拦截处理。

import { Directive, ElementRef, HostListener, Renderer2, Input } from '@angular/core';

@Directive({
 selector: '[appInput]',
})
export class InputDirective {
 constructor(
 private el: ElementRef,
 private render: Renderer2
 ) {
 // 添加预设class
 render.addClass(this.el.nativeElement, 'my-input');
 }
 @HostListener('input') onInputChange() {
 const element = this.el.nativeElement;
 if (element.max && Number(element.value) >= Number(element.max)) {
  this.render.setProperty(element, 'value', element.max);
 }
 }
}
<div class="my-input">
 <input
 appInput
 #inputElement
 [(ngModel)]="actualValue"
 placeholder="{{placeholder}}"
 (ngModelChange)="modelChange($event)"
 >
</div>
<my-input formControlName="userName" [maxLength]="5" [type]="'number'" [max]="250"></my-input>

表单验证测试:

Angular封装表单控件及思想总结

form表单拿到的值还是输入的非法值,这是因为模型值与原生元素之间没有真正的做到统一一致,

指令中核心代码修改:

@Output() valueChange = new EventEmitter();
@HostListener('input') onInputChange() {
 const element = this.el.nativeElement;
 if (element.max && Number(element.value) >= Number(element.max)) {
  this.render.setProperty(element, 'value', element.max);
  this.valueChange.emit(element.value);
 }
}

在input 标签上添加事件监听 (valueChange)="onValueChange($event)"

onValueChange(event) {
 this.modelChange(event);
 }

 Angular封装表单控件及思想总结

表单获取的值与原生控件的value一致,一般自行封装原生控件还需要加入自己的样式,甚至有时候我们封装的主要目的就是美化样式,动态添加class示例:

@Directive({
 selector: '[appInput]',
 // tslint:disable-next-line: no-host-metadata-property
 host: {
 '[class.my-input-disabled]': 'disabled'
 }
})
export class InputDirective {
 constructor(
 private el: ElementRef,
 private render: Renderer2
 ) {
 // 添加预设class
 render.addClass(this.el.nativeElement, 'my-input');
 }
 @Input() @InputBoolean() disabled = false;
 @Output() valueChange = new EventEmitter();
 @HostListener('input') onInputChange() {
 const element = this.el.nativeElement;
 if (element.max && Number(element.value) >= Number(element.max)) {
  this.render.setProperty(element, 'value', element.max);
  this.valueChange.emit(element.value);
 }
 console.log(element.value);
 }
}

结尾:总结下封装表单控件的原则:

1.原生控件支持的属性机制理论上需要全部保留实现(特别针对某业务封装除外);

2.不涉及复杂的数据处理、判断等逻辑的优先使用指令处理,例如本例中input的大多数功能都可以不做封装,原生标签input已经很完善;

3.get和set方法必须体现,且要保持模型数据与原生元素的value一致,外部操作可以更改组件属性,是否需要监听属性变化作出相应处理根据空间类型和业务进行斟酌;

4.一定要使用form表单提交功能去验证,原生form 配合name和label

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
在你的网页中嵌入外部网页的方法
Apr 02 Javascript
js获取div高度的代码
Aug 09 Javascript
javascript 全选与全取消功能的实现代码
Dec 23 Javascript
怎么选择Javascript框架(Javascript Framework)
Nov 22 Javascript
对new functionName()定义一个函数的理解
May 22 Javascript
推荐一个自己用的封装好的javascript插件
Jan 29 Javascript
JS日程管理插件FullCalendar中文说明文档
Feb 06 Javascript
基于JavaScript实现全选、不选和反选效果
Feb 15 Javascript
JS获取鼠标位置距浏览器窗口距离的方法示例
Apr 11 Javascript
使用webpack4编译并压缩ES6代码的方法示例
Apr 24 Javascript
Vue3.x源码调试的实现方法
Oct 13 Javascript
何时/使用 Vue3 render 函数的教程详解
Jul 25 Javascript
小程序接口的promise化的实现方法
Dec 11 #Javascript
js中Function引用类型常见有用的方法和属性详解
Dec 11 #Javascript
jQuery实现验证用户登录
Dec 10 #jQuery
这样回答继承可能面试官更满意
Dec 10 #Javascript
jquery实现弹窗(系统提示框)效果
Dec 10 #jQuery
微信小程序 this.triggerEvent()的具体使用
Dec 10 #Javascript
jQuery实现消息弹出框效果
Dec 10 #jQuery
You might like
DC动漫人物排行
2020/03/03 欧美动漫
php 读取文件乱码问题
2010/02/20 PHP
jQuery判断元素是否是隐藏的代码
2011/04/24 Javascript
拖动table标题实现改变td的大小(css+js代码)
2013/04/16 Javascript
JS函数重载的解决方案
2014/05/13 Javascript
jQuery中remove()方法用法实例
2014/12/25 Javascript
浅谈JavaScript function函数种类
2014/12/29 Javascript
jQuery实现列表的全选功能
2015/03/18 Javascript
EasyUi中的Combogrid 实现分页和动态搜索远程数据
2016/04/01 Javascript
js实现按钮控制带有停顿效果的图片滚动
2016/08/30 Javascript
详解数组Array.sort()排序的方法
2020/05/09 Javascript
Javascript封装id、class与元素选择器方法示例
2017/03/13 Javascript
微信sdk实现禁止微信分享(使用原生php实现)
2019/11/15 Javascript
javascript实现简易的计算器
2020/01/17 Javascript
微信小程序自定义联系人弹窗
2020/05/26 Javascript
vue项目页面嵌入代码块vue-prism-editor的实现
2020/10/30 Javascript
[01:06:30]DOTA2-DPC中国联赛定级赛 Phoenix vs DLG BO3第二场 1月9日
2021/03/11 DOTA
python求斐波那契数列示例分享
2014/02/14 Python
Python中使用glob和rmtree删除目录子目录及所有文件的例子
2014/11/21 Python
使用基于Python的Tornado框架的HTTP客户端的教程
2015/04/24 Python
ubuntu16.04制作vim和python3的开发环境
2018/09/23 Python
浅谈python脚本设置运行参数的方法
2018/12/03 Python
Tensorflow读取并输出已保存模型的权重数值方式
2020/01/04 Python
通过python检测字符串的字母
2020/02/18 Python
python语言的优势是什么
2020/06/17 Python
Casadei卡萨蒂官网:意大利奢侈鞋履品牌
2017/10/28 全球购物
印度尼西亚电子产品购物网站:Kliknklik
2018/06/05 全球购物
全球性的女装店:storets
2019/06/12 全球购物
大唐面试试题(CPU,UNIX等等)
2012/01/11 面试题
鲜果饮品店创业计划书
2014/01/21 职场文书
运动会通讯稿300字
2014/02/02 职场文书
《云房子》教学反思
2014/04/20 职场文书
学生会生活部工作总结2015
2015/03/31 职场文书
2015年志愿者服务工作总结
2015/04/20 职场文书
夏洛特的网观后感
2015/06/15 职场文书
司法廉洁教育心得体会
2016/01/20 职场文书