欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-18 08:22:02
前言 前端框架的强大无疑给开发者省去了不少烦恼,又因比较完善的ui库支撑,让部分后端开发者能够省去大量样式设计的时间成本,纵然如此,业务的多变性是框架本身无法预料的,很多的控件功能在...

前言

前端框架的强大无疑给开发者省去了不少烦恼,又因比较完善的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

总结

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