Angular结构型指令简写语法失效,完整模板正常问题排查
问题背景
为实现响应式表单校验消息展示,基于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

