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 相关文章推荐
JS、jquery实现几分钟前、几小时前、几天前等时间差显示效果的代码实例分享
Apr 11 Javascript
jQuery响应enter键的实现思路
Apr 18 Javascript
javascript中不提供sleep功能如何实现这个功能
May 27 Javascript
js中的事件捕捉模型与冒泡模型实例分析
Jan 10 Javascript
JavaSacript中charCodeAt()方法的使用详解
Jun 05 Javascript
jQuery实现本地预览上传图片功能
Jan 08 Javascript
全面解析JS字符串和正则表达式中的match、replace、exec等函数
Jul 01 Javascript
JavaScript触发onScroll事件的函数节流详解
Dec 14 Javascript
Vue中使用webpack别名的方法实例详解
Jun 19 Javascript
Vue.js 利用v-for中的index值实现隔行变色
Aug 01 Javascript
Element-UI踩坑之Pagination组件的使用
Oct 29 Javascript
Angular+ionic实现折叠展开效果的示例代码
Jul 29 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
打造超酷的PHP数据饼图效果实现代码
2011/11/23 PHP
php删除字符串末尾子字符,删除开始字符,删除两端字符(实现代码)
2013/06/27 PHP
PHP错误和异长常处理总结
2014/03/06 PHP
php+mysqli数据库连接的两种方式
2015/01/28 PHP
PHP实现生成数据字典功能示例
2018/05/24 PHP
javascript编程起步(第七课)
2007/01/10 Javascript
jQuery EasyUI API 中文文档 - TimeSpinner时间微调器
2011/10/23 Javascript
JavaScript实现url地址自动检测并添加URL链接示例代码
2013/11/12 Javascript
jquery实现邮箱自动补全功能示例分享
2014/02/17 Javascript
jQuery 如何先创建、再修改、后添加DOM元素
2014/05/20 Javascript
JavaScript数组常用操作技巧汇总
2014/11/17 Javascript
javascript插件开发的一些感想和心得
2016/02/28 Javascript
JavaScript编写页面半透明遮罩效果的简单示例
2016/05/09 Javascript
Three.js学习之文字形状及自定义形状
2016/08/01 Javascript
Actionscript与javascript交互实例程序(修改)
2016/09/22 Javascript
react-router4 嵌套路由的使用方法
2017/07/24 Javascript
jQuery实现的页面遮罩层功能示例【测试可用】
2017/10/14 jQuery
JavaScript实现简单的文本逐字打印效果示例
2018/04/12 Javascript
JS点击动态添加标签、删除指定标签的代码
2018/04/18 Javascript
简单了解Vue + ElementUI后台管理模板
2020/04/07 Javascript
js+canvas实现五子棋小游戏
2020/08/02 Javascript
js+for循环实现字符串自动转义的代码(把后面的字符替换前面的字符)
2020/12/24 Javascript
[48:31]DOTA2-DPC中国联赛 正赛 Dynasty vs XG BO3 第一场 2月2日
2021/03/11 DOTA
使用python编写脚本获取手机当前应用apk的信息
2014/07/21 Python
对Django项目中的ORM映射与模糊查询的使用详解
2019/07/18 Python
python写程序统计词频的方法
2019/07/29 Python
pytorch 修改预训练model实例
2020/01/18 Python
最新PyCharm从安装到PyCharm永久激活再到PyCharm官方中文汉化详细教程
2020/11/17 Python
日本面向世界,国际级的免税在线购物商城:DOKODEMO
2017/02/01 全球购物
Foot Locker英国官网:美国知名运动产品零售商
2019/02/21 全球购物
大学辅导员事迹材料
2014/02/05 职场文书
优秀共产党员先进事迹材料
2014/05/06 职场文书
2014年纠风工作总结
2014/12/08 职场文书
2014年电信员工工作总结
2014/12/19 职场文书
巾帼建功标兵先进事迹材料
2016/02/29 职场文书
导游词之藏龙百瀑景区
2019/12/30 职场文书