您好,登錄后才能下訂單哦!
前言
前端框架的強大無疑給開發者省去了不少煩惱,又因比較完善的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,然后打印:
可以看到表單沒有獲取到最新的值,這是因為目前位置表單獲取組件的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 ,查詢打印:
實現原生input基礎屬性
這個幾乎是一條默認的規則,封裝的控件至少實現原生input的基礎屬性功能,在此基礎上再進行滿足業務需求。
這里只討論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>
表單驗證測試:
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); }
表單獲取的值與原生控件的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
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。