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用Date对象处理时间实现思路及代码
Jan 31 Javascript
js中对象的声明方式以及数组的一些用法示例
Dec 11 Javascript
js实现从中间开始往上下展开网页窗口的方法
Mar 02 Javascript
Hammer.js+轮播原理实现简洁的滑屏功能
Feb 02 Javascript
AngularJS表达式讲解及示例代码
Aug 16 Javascript
js获取form表单所有数据的简单方法
Aug 18 Javascript
JS取模、取商及取整运算方法示例
Oct 13 Javascript
Bootstrap的class样式小结
Dec 01 Javascript
ES6中的rest参数与扩展运算符详解
Jul 18 Javascript
JavaScript实现三级联动菜单效果
Aug 16 Javascript
使用watch监听路由变化和watch监听对象的实例
Feb 24 Javascript
vue中使用vee-validator完成表单校验方案
Nov 01 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
解析阿里云ubuntu12.04环境下配置Apache+PHP+PHPmyadmin+MYsql
2013/06/26 PHP
对比分析php中Cookie与Session的异同
2016/02/19 PHP
PHP CURL post数据报错 failed creating formpost data
2016/10/16 PHP
thinkphp jquery实现图片上传和预览效果
2020/07/22 PHP
php 根据URL下载远程图片、压缩包、pdf等文件到本地
2019/07/26 PHP
用jquery实现下拉菜单效果的代码
2010/07/25 Javascript
情人节专属 纯js脚本1k大小的3D玫瑰效果
2012/02/11 Javascript
轻松学习jQuery插件EasyUI EasyUI创建菜单与按钮
2015/11/30 Javascript
Javascript中的Prototype到底是什么
2016/02/16 Javascript
很棒的一组js图片轮播特效
2017/01/12 Javascript
bootstrap滚动监控器使用方法解析
2017/01/13 Javascript
vue实现简单实时汇率计算功能
2017/01/15 Javascript
vuex进阶知识点巩固
2018/05/20 Javascript
详解React中setState回调函数
2018/06/14 Javascript
泛谈JS逻辑判断选择器 || &amp;&amp;
2019/05/24 Javascript
vue发送websocket请求和http post请求的实例代码
2019/07/11 Javascript
微信小程序去除左上角返回键的实现方法
2020/03/06 Javascript
jQuery实现视频展示效果
2020/05/30 jQuery
[09:22]2014DOTA2西雅图国际邀请赛 主赛事第二日TOPPLAY
2014/07/21 DOTA
Python3基于sax解析xml操作示例
2018/05/22 Python
python实现人民币大写转换
2018/06/20 Python
Python爬虫之正则表达式的使用教程详解
2018/10/25 Python
Python shutil模块用法实例分析
2019/10/02 Python
浅谈pytorch、cuda、python的版本对齐问题
2020/01/15 Python
Python使用循环神经网络解决文本分类问题的方法详解
2020/01/16 Python
踩坑:pytorch中eval模式下结果远差于train模式介绍
2020/06/23 Python
python suds访问webservice服务实现
2020/06/26 Python
日本最大的药妆连锁店:Matsukiyo松本清药妆店
2017/11/23 全球购物
Farfetch巴西官网:奢侈品牌时尚购物平台
2020/10/19 全球购物
英语系本科生个人求职信
2013/09/21 职场文书
小学国庆节活动方案
2014/02/11 职场文书
伊琍体标语
2014/06/25 职场文书
高中诗歌鉴赏教学反思
2016/02/16 职场文书
Win11怎样将锁屏账户头像图片改成动画视频
2021/11/21 数码科技
微信小程序中使用vant框架的具体步骤
2022/02/18 Javascript
画错魏国疆域啦!《派对咖孔明》动画因作画失误于官网致歉
2022/04/07 日漫