Angular基于ControlValueAccessor的自定义表单控件双向绑定失效问题
问题原因&解决方案
核心问题点
- 自定义控件的
onChange回调未传递输入值:ControlValueAccessor要求onChange需要把最新控件值作为参数传出,原代码仅触发无参onChange(),父表单无法接收最新值,导致绑定失效 - 输入事件未同步内部
value变量:input绑定了[value]="value",但输入时未更新this.value,视图和内部状态不同步 - intl-telInput初始化时机错误:原代码在constructor中操作DOM,此时组件DOM未渲染,可能无法获取到input元素
- 响应式表单中同时使用
formControlName和[(ngModel)]属于不推荐写法,高版本Angular会抛出警告
修改后的代码
1. 自定义控件模板
<input type="tel" id="myPhone" (blur)="onTouched()" (input)="handleInput($event)" [disabled]="disabled" [value]="value">
2. 自定义控件TS文件
import * as $ from 'jquery'; import { Component, forwardRef, AfterViewInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; declare var intlTelInput: any; @Component({ selector: 'international-phone-control', templateUrl: './international-phone-control.component.html', styleUrls: ['./international-phone-control.component.css'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InternationalPhoneControlComponent), multi: true } ] }) export class InternationalPhoneControlComponent implements ControlValueAccessor, AfterViewInit { value: string = ''; onChange: (val: string) => void = () => {}; onTouched: () => void = () => {}; disabled: boolean = false; private itiInstance: any; ngAfterViewInit() { const input = document.querySelector("#myPhone") as HTMLInputElement; this.itiInstance = intlTelInput(input, { initialCountry: "auto", geoIpLookup: (callback: any) => { $.get('https://ipinfo.io?token=0454c4d5e06c30', () => {}, "jsonp").always(resp => { const countryCode = resp?.country ?? "us"; callback(countryCode); }); }, utilsScript: "https://cdn.jsdelivr.net/npm/intl-tel-input@17.0.3/build/js/utils.js" }); } handleInput(event: Event) { // 如需获取带国际区号的完整号码,使用itiInstance.getNumber(),否则直接取input的value const currentValue = this.itiInstance ? this.itiInstance.getNumber() : (event.target as HTMLInputElement).value; this.value = currentValue; this.onChange(currentValue); } writeValue(value: string): void { this.value = value ?? ''; if (this.itiInstance) { this.itiInstance.setNumber(this.value); } } registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } }
3. 表单组件模板(移除冗余ngModel)
<form [formGroup]="myForm"> <h1>运行结果:</h1> <h2>控件值:{{myForm.get('phone')?.value}}</h2> <h2>是否被触碰:{{myForm.get('phone')?.touched}}</h2> <h2>是否被修改:{{myForm.get('phone')?.dirty}}</h2> <h2>是否报错:{{myForm.get('phone')?.hasError('required')}}</h2> <input type="text" formControlName="userName" class="form-control"/> <br/> <br/> <international-phone-control formControlName="phone" class="form-control"></international-phone-control> <p class="text-danger" *ngIf="myForm.get('phone')?.dirty && myForm.get('phone')?.hasError('required')"> 手机号为必填项 </p> </form>
内容的提问来源于stack exchange,提问作者mnu-nasir
相关产品推荐
相关产品推荐

