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

Angular独立组件实现ControlValueAccessor时触发writeValue空指针错误

问题:Angular独立组件实现ControlValueAccessor时出现"Cannot read properties of null (reading 'writeValue')"错误

错误信息

ERROR TypeError: Cannot read properties of null (reading 'writeValue')
    at Yo (forms.mjs:3236:21)
    at e.addControl (forms.mjs:5173:7)
    at e._setUpControl (forms.mjs:5751:41)
    at e.ngOnChanges (forms.mjs:5698:30)
    at e.Y0 (core.mjs:3136:10)
    at qp (core.mjs:4199:10)
    at bb (core.mjs:4226:5)
    at Im (core.mjs:4183:9)
    at js (core.mjs:4138:5)
    at mv (core.mjs:12111:9)

当前实现代码

Google Place Autocomplete独立组件(TS)

import { Component, ElementRef, forwardRef, OnInit, ViewChild, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
import { GoogleMapsLoaderService } from 'src/app/_services/google-maps-loader.service';

@Component({
  selector: 'app-google-place-autocomplete',
  templateUrl: './google-place-autocomplete.component.html',
  styleUrls: ['./google-place-autocomplete.component.scss'],
  standalone: true,
  imports: [
    CommonModule,
    ReactiveFormsModule
  ],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => GooglePlaceAutocompleteComponent),
      multi: true,
    }
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GooglePlaceAutocompleteComponent implements ControlValueAccessor, OnInit {
  @ViewChild('autocomplete', { static: false }) autocompleteRef!: ElementRef;

  value: string = '';
  disabled = false;
  onChange = (value: any) => { };
  onTouched = () => { };

  constructor(private googleMapsLoader: GoogleMapsLoaderService) { }

  async ngOnInit() {
    await this.googleMapsLoader.load();
  }

  writeValue(value: string): void {
    this.value = value;
    this.updateComponentValue();
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
    if (this.autocompleteRef?.nativeElement) {
      this.autocompleteRef.nativeElement.disabled = isDisabled;
    }
  }

  onPlaceSelected(event: any) {
    const place = event.detail?.place;
    this.value = place?.formattedAddress || '';
    this.onChange(this.value);
    this.onTouched();
  }

  private updateComponentValue(): void {
    if (this.autocompleteRef?.nativeElement) {
      if (typeof this.autocompleteRef.nativeElement.setValue === 'function') {
        this.autocompleteRef.nativeElement.setValue(this.value);
      } else {
        this.autocompleteRef.nativeElement.value = this.value;
      }
    }
  }
}

HTML模板

<gmpx-place-autocomplete 
  #autocomplete 
  style="width: 100%" 
  [value]="value"
  (gmpx-placechange)="onPlaceSelected($event)">
</gmpx-place-autocomplete>

表单组件中的使用代码

@Component({
  selector: 'app-location-form',
  standalone: true,
  imports: [
    CommonModule,
    ReactiveFormsModule,
    GooglePlaceAutocompleteComponent
  ],
  template: `
    <form [formGroup]="locationForm" (ngSubmit)="onSubmit()">
      <app-google-place-autocomplete formControlName="address"></app-google-place-autocomplete>
      <button type="submit">Save</button>
    </form>
  `
})
export class LocationFormComponent implements OnInit {
  locationForm = this.fb.group({
    address: ['', Validators.required]
  });

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {}

  onSubmit(): void {
    if (this.locationForm.valid) {
      console.log(this.locationForm.value);
    }
  }
}

错误原因

  1. 手动注册NG_VALUE_ACCESSOR的解析问题:在独立组件中手动配置NG_VALUE_ACCESSOR提供者时,forwardRef可能出现解析异常,导致Angular无法正确获取ControlValueAccessor实例,进而尝试对null调用writeValue方法。
  2. 异步依赖的时序冲突:Google Maps脚本是异步加载的,writeValue可能在脚本加载完成、自定义元素初始化前被调用,导致值同步逻辑无法正常执行。

修复方案

1. 使用provideNgValueAccessor简化注册

Angular 14+提供了provideNgValueAccessor辅助函数,自动处理forwardRef和多提供者配置,彻底避免手动注册的潜在问题。

2. 优化值同步的时序逻辑

标记Google Maps脚本加载状态,确保仅在脚本加载完成后才同步值到自定义元素;同时在脚本加载完成后重新触发一次值同步,避免初始值丢失。

修改后的组件代码

import { Component, ElementRef, OnInit, ViewChild, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ControlValueAccessor, provideNgValueAccessor } from '@angular/forms';
import { GoogleMapsLoaderService } from 'src/app/_services/google-maps-loader.service';

@Component({
  selector: 'app-google-place-autocomplete',
  templateUrl: './google-place-autocomplete.component.html',
  styleUrls: ['./google-place-autocomplete.component.scss'],
  standalone: true,
  imports: [CommonModule],
  providers: [provideNgValueAccessor(GooglePlaceAutocompleteComponent)],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GooglePlaceAutocompleteComponent implements ControlValueAccessor, OnInit {
  @ViewChild('autocomplete', { static: false }) autocompleteRef!: ElementRef;

  value: string = '';
  disabled = false;
  onChange = (value: any) => { };
  onTouched = () => { };
  private mapsLoaded = false;

  constructor(private googleMapsLoader: GoogleMapsLoaderService) { }

  async ngOnInit() {
    await this.googleMapsLoader.load();
    this.mapsLoaded = true;
    this.updateComponentValue(); // 脚本加载完成后同步初始值
  }

  writeValue(value: string): void {
    this.value = value;
    if (this.mapsLoaded) {
      this.updateComponentValue();
    }
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
    if (this.mapsLoaded && this.autocompleteRef?.nativeElement) {
      this.autocompleteRef.nativeElement.disabled = isDisabled;
    }
  }

  onPlaceSelected(event: any) {
    const place = event.detail?.place;
    this.value = place?.formattedAddress || '';
    this.onChange(this.value);
    this.onTouched();
  }

  private updateComponentValue(): void {
    if (this.autocompleteRef?.nativeElement) {
      const element = this.autocompleteRef.nativeElement;
      if (typeof element.setValue === 'function') {
        element.setValue(this.value);
      } else {
        element.value = this.value;
      }
    }
  }
}

独立组件实现ControlValueAccessor的已知问题

  • 手动注册的风险:手动配置NG_VALUE_ACCESSOR时,forwardRef在独立组件中可能出现解析问题,推荐使用provideNgValueAccessor辅助函数(Angular 14+)替代。
  • 异步依赖的时序问题:如果组件依赖异步加载的资源(如第三方脚本),需要明确标记加载状态,避免在资源就绪前操作未初始化的元素。
  • DI上下文一致性:独立组件的依赖注入上下文与模块组件一致,但需确保所有依赖均通过imports或providers正确引入,避免注入失败。

内容的提问来源于stack exchange,提问作者AlejoDev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 11:10:53