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

Angular结构型指令简写语法失效,完整模板正常问题排查

Angular 18结构型指令*语法简写调用报错问题解决

问题背景

为实现响应式表单校验消息展示,基于Angular 18开发了结构型指令appValidationMessages,使用完整<ng-template>调用时功能正常,但使用*语法简写调用时抛出错误:

ERROR Error: Control with name function inputValueFn() {
    producerAccessed(node);
    if (node.value === REQUIRED_UNSET_VALUE) {
      throw new RuntimeError(-950, ngDevMode && "Input is required but no value is available yet.");
    }
    return node.value;
  } not found in form group

错误原因

1. 指令代码的字符串插值错误

原指令的throw语句中,直接使用了this.appValidationMessagesControlName(InputSignal对象)而非调用其getter方法this.appValidationMessagesControlName(),导致错误信息输出的是信号对象的toString结果(即函数代码)。

2. *语法的输入绑定规则未遵守

Angular的*语法会将宿主元素转换为<ng-template>,并将表达式绑定到与指令selector同名的输入属性。原指令未给appValidationMessagesOf设置selector别名,因此*appValidationMessages="let valMessages of loginForm"无法将loginForm绑定到appValidationMessagesOf;同时简写调用中appValidationMessagesControlName的绑定方式不符合*语法的参数传递规则,导致控件名未被正确赋值。

修复方案

步骤1:修正指令的错误信息插值

将throw语句中的this.appValidationMessagesControlName改为this.appValidationMessagesControlName(),确保输出正确的控件名字符串。

步骤2:配置输入属性别名以支持*语法

给appValidationMessagesOf设置与指令selector同名的别名,让*语法的of loginForm能正确绑定到该输入;同时给appValidationMessagesControlName设置别名controlName,支持在*语法表达式中传递控件名参数。

步骤3:添加内存泄漏防护

使用takeUntilDestroyed自动取消订阅控件的statusChanges,避免内存泄漏。

修复后的完整指令代码

import { Directive, effect, input, InputSignal, TemplateRef, ViewContainerRef, DestroyRef } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Directive({
  selector: '[appValidationMessages]',
  standalone: true
})
export class ValidationMessagesDirective {
  constructor(
    private templateReference: TemplateRef<{
      $implicit: { key: string; message: string }
    }>,
    private viewContainerReference: ViewContainerRef,
    private destroyRef: DestroyRef
  ) {
    effect(() => {
      const formGroup = this.appValidationMessagesOf();
      const controlName = this.appValidationMessagesControlName();
      
      if (!controlName) {
        throw new Error('Control name must be provided');
      }

      const controlReference = formGroup.get(controlName);
      if (!controlReference) {
        // 修正:调用信号getter获取实际控件名
        throw new Error(`Control with name ${controlName} not found in form group`);
      }
      this.control = controlReference;

      // 使用takeUntilDestroyed自动取消订阅,防止内存泄漏
      this.control.statusChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
        this.doValidation();
      });
    });
  }

  // 设置别名,支持*语法的of绑定
  public appValidationMessagesOf: InputSignal<FormGroup> = input.required<FormGroup>({ alias: 'appValidationMessages' });
  // 设置别名,支持*语法的controlName参数传递
  public appValidationMessagesControlName: InputSignal<string> = input<string>('', { alias: 'controlName' });

  private control!: AbstractControl<any, any>;

  private doValidation(): void {
    if (!this.control || !this.control.touched || !this.control.errors) {
      this.viewContainerReference.clear();
      return;
    }
    this.viewContainerReference.clear();
    for (const key in this.control.errors) {
      let error = 'This field is invalid';
      switch (key) {
        case 'required':
          error = 'This field is required';
          break;
        case 'email':
          error = 'This field must be a valid email address';
          break;
        case 'minlength':
          error = `This field must be at least ${this.control.errors['minlength'].requiredLength} characters long`;
          break;
        case 'emailIsTaken':
          error = 'This email is already taken';
          break;
      }
      const viewData = { $implicit: { key: key, message: error } };
      this.viewContainerReference.createEmbeddedView(this.templateReference, viewData);
    }
  }
}

正确的*语法调用方式

<section>
  <header>Validation messages shorthand</header>
  <p class="text-danger" *appValidationMessages="let valMessages of loginForm; controlName: 'email'">
    {{valMessages.message}}
  </p>
</section>

说明

  • 完整<ng-template>的调用方式仍可正常使用,无需修改。
  • *语法中,of loginForm对应绑定到appValidationMessagesOf,controlName: 'email'对应绑定到appValidationMessagesControlName。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 07:39:53