You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 21:12:01