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 相关文章推荐
VUEJS实战之修复错误并且美化时间(2)
Jun 13 Javascript
js基于cookie记录来宾姓名的方法
Jul 19 Javascript
jquery实现拖动效果
Aug 10 Javascript
js基本算法:冒泡排序,二分查找的简单实例
Oct 08 Javascript
js实现弹窗居中的简单实例
Oct 09 Javascript
前端js实现文件的断点续传 后端PHP文件接收
Oct 14 Javascript
利用javascript实现的三种图片放大镜效果实例(附源码)
Jan 23 Javascript
推荐三款不错的图片压缩上传插件(webuploader、localResizeIMG4、LUploader)
Apr 21 Javascript
vue.js组件之间传递数据的方法
Jul 10 Javascript
js用类封装pop弹窗组件
Oct 08 Javascript
javascript触发模拟鼠标点击事件
Jun 26 Javascript
微信小程序使用npm包的方法步骤
Aug 13 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开发框架的对比
2013/07/05 PHP
PHP预定义变量9大超全局数组用法详解
2016/04/23 PHP
PHP面向对象程序设计组合模式与装饰模式详解
2016/12/02 PHP
php支付宝APP支付功能
2020/07/29 PHP
JQuery勾选指定name的复选框集合并显示的方法
2015/05/18 Javascript
jQuery结合AJAX之在页面滚动时从服务器加载数据
2015/06/30 Javascript
AngularJs 60分钟入门基础教程
2016/04/03 Javascript
AngularJS基础 ng-if 指令用法
2016/08/01 Javascript
Dropzone.js实现文件拖拽上传功能(附源码下载)
2016/11/22 Javascript
vue计算属性时v-for处理数组时遇到的一个bug问题
2018/01/21 Javascript
viewer.js一个强大的基于jQuery的图像查看插件(支持旋转、缩放)
2020/04/01 jQuery
[01:11:15]VGJ.S vs Secret 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
实现python版本的按任意键继续/退出
2016/09/26 Python
Python 循环语句之 while,for语句详解
2018/04/23 Python
python去掉 unicode 字符串前面的u方法
2018/10/21 Python
老生常谈python中的重载
2018/11/11 Python
python分块读取大数据,避免内存不足的方法
2018/12/10 Python
python实现生成Word、docx文件的方法分析
2019/08/30 Python
详解Django3中直接添加Websockets方式
2020/02/12 Python
Python3 socket即时通讯脚本实现代码实例(threading多线程)
2020/06/01 Python
Python通过kerberos安全认证操作kafka方式
2020/06/06 Python
安装Anaconda3及使用Jupyter的方法
2020/10/27 Python
详解Html5微信支付爬坑之路
2018/07/24 HTML / CSS
极度干燥澳大利亚官方网站:Superdry澳大利亚
2019/03/28 全球购物
美国购买体育、音乐会和剧院门票网站:SelectATicket
2019/09/08 全球购物
采购主管岗位职责
2014/02/01 职场文书
法人代表任命书范本
2014/06/05 职场文书
物业保安岗位职责
2014/07/02 职场文书
意外伤害赔偿协议书
2014/09/16 职场文书
企业领导班子四风对照检查材料
2014/09/27 职场文书
门市房租房协议书
2014/12/04 职场文书
高中升旗仪式主持词
2015/07/03 职场文书
获奖感言一句话
2015/07/31 职场文书
Oracle更换为MySQL遇到的问题及解决
2021/05/21 Oracle
详解Redis基本命令与使用场景
2021/06/01 Redis
React中的Context应用场景分析
2021/06/11 Javascript