溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Angular封裝表單控件及思想總結(jié)

發(fā)布時(shí)間:2020-09-26 05:29:53 來源:腳本之家 閱讀:169 作者:何棄療 欄目:web開發(fā)

前言

前端框架的強(qiáng)大無疑給開發(fā)者省去了不少煩惱,又因比較完善的UI庫(kù)支撐,讓部分后端開發(fā)者能夠省去大量樣式設(shè)計(jì)的時(shí)間成本,縱然如此,業(yè)務(wù)的多變性是框架本身無法預(yù)料的,很多的控件功能在實(shí)際開發(fā)中總是不夠完善和靈活,所以需要開發(fā)者結(jié)合業(yè)務(wù)需求進(jìn)行再次封裝這些UI控件/組件。

表單控件

常規(guī)組件只需要根據(jù)官方指引,寫好數(shù)據(jù)傳輸?shù)姆绞胶陀嗛喖纯扇我馐褂?,表單控件有點(diǎn)特殊,按照常規(guī)方式寫出來的組件使用在表單中,綁定ngModel或者formControlName,隨之而來的是一個(gè)報(bào)錯(cuò):

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

只有實(shí)現(xiàn)了這個(gè)接口才可以完成像普通表單元素那樣使用和驗(yàn)證。

interface ControlValueAccessor {
 writeValue(obj: any): void
 registerOnChange(fn: any): void
 registerOnTouched(fn: any): void
 setDisabledState(isDisabled: boolean)?: void
}

你的控件必須包含上述方法;此外,控件內(nèi)部要有value的get實(shí)現(xiàn),以及最好有個(gè)與value等值的別名變量(想不明白別急,看代碼);一個(gè)簡(jiǎn)單的input控件封裝應(yīng)該類似這樣:

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;
 }

}

其實(shí)封裝工作只完成一半,組件裝飾器元數(shù)據(jù)完整:

@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表單中使用不會(huì)報(bào)錯(cuò);表單內(nèi)放置一個(gè)查詢按鈕,用來輸出表單狀態(tài):

<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);
 }

封裝控件內(nèi)部:

<div class="my-input">
 <input type="{{type}}" value="{{value}}" placeholder="{{placeholder}}">
</div>

通過formControlName的綁定方式將userName傳入控件,控件通過writeValue方法接收并賦值到自身屬性value,用于與原生input交互,此時(shí)我們手動(dòng)輸入內(nèi)容為數(shù)字3,然后打?。?/p>

Angular封裝表單控件及思想總結(jié)

可以看到表單沒有獲取到最新的值,這是因?yàn)槟壳拔恢帽韱潍@取組件的value還是初始值,我們也沒有提供改變value的方法機(jī)制,修改html:

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

這里稍微解釋input綁定數(shù)據(jù)與觸發(fā)的更新方法可以選擇原生的value和input進(jìn)行更新,也可以選擇ng提供的ngModel和ngModelChange事件更新控件,區(qū)別在于使用原生input的輸入事件,要使用到事件對(duì)象展開找到元素的value屬性值;而使用ng官方框架自帶的事件,事件對(duì)象$event就是最新的value值。

新增set value方法:

set value(data) {
 this.actualValue = data;
 // 通知表單value更新
 this.onChange(data);
}
registerOnChange(fn: any) {
 // 注冊(cè)表單的value改變通知方法
 this.onChange = fn;
}
modelChange(event) {
 this.value = event;
}

輸入 3 ,查詢打印:

Angular封裝表單控件及思想總結(jié)

實(shí)現(xiàn)原生input基礎(chǔ)屬性

這個(gè)幾乎是一條默認(rèn)的規(guī)則,封裝的控件至少實(shí)現(xiàn)原生input的基礎(chǔ)屬性功能,在此基礎(chǔ)上再進(jìn)行滿足業(yè)務(wù)需求。

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

這里只討論type為text和number的情況,radio等其它類型沒必要深入。

我們不能直接使用maxlength進(jìn)行與input綁定,至少寫法不是很好,比較妥善的做法是動(dòng)態(tài)的判斷長(zhǎng)度值,并且將正確的值設(shè)置到原生input屬性中。

為此修改html:

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

注入 Renderer2,用于對(duì)原生元素操作

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)]]
 });
 }

經(jīng)過打印測(cè)試,表單的狀態(tài)正確 √

適當(dāng)使用指令

假如此時(shí)需要對(duì)輸入內(nèi)容攔截處理,目前在不寫input事件的情況下無法做到,假如針對(duì)一個(gè)type=number類型的輸入框,設(shè)置最大值,超過這個(gè)值不會(huì)改變,原生input元素確實(shí)有max屬性支撐驗(yàn)證,但是它無法改變value值,也就是說假如這個(gè)最大值不是必要驗(yàn)證屬性,那么表單還是可以提交最新的超出值,用指令可以攔截處理。

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

@Directive({
 selector: '[appInput]',
})
export class InputDirective {
 constructor(
 private el: ElementRef,
 private render: Renderer2
 ) {
 // 添加預(yù)設(shè)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>

表單驗(yàn)證測(cè)試:

Angular封裝表單控件及思想總結(jié)

form表單拿到的值還是輸入的非法值,這是因?yàn)槟P椭蹬c原生元素之間沒有真正的做到統(tǒng)一一致,

指令中核心代碼修改:

@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 標(biāo)簽上添加事件監(jiān)聽 (valueChange)="onValueChange($event)"

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

 Angular封裝表單控件及思想總結(jié)

表單獲取的值與原生控件的value一致,一般自行封裝原生控件還需要加入自己的樣式,甚至有時(shí)候我們封裝的主要目的就是美化樣式,動(dòng)態(tài)添加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
 ) {
 // 添加預(yù)設(shè)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);
 }
}

結(jié)尾:總結(jié)下封裝表單控件的原則:

1.原生控件支持的屬性機(jī)制理論上需要全部保留實(shí)現(xiàn)(特別針對(duì)某業(yè)務(wù)封裝除外);

2.不涉及復(fù)雜的數(shù)據(jù)處理、判斷等邏輯的優(yōu)先使用指令處理,例如本例中input的大多數(shù)功能都可以不做封裝,原生標(biāo)簽input已經(jīng)很完善;

3.get和set方法必須體現(xiàn),且要保持模型數(shù)據(jù)與原生元素的value一致,外部操作可以更改組件屬性,是否需要監(jiān)聽屬性變化作出相應(yīng)處理根據(jù)空間類型和業(yè)務(wù)進(jìn)行斟酌;

4.一定要使用form表單提交功能去驗(yàn)證,原生form 配合name和label

總結(jié)

以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)億速云的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI