Storybook中Formly验证触发NG0100错误,输入框无法聚焦
问题描述
我正在为表单字段自定义Formly包装器,想要在Storybook中展示该包装器。参考示例添加了中间组件StoryBookFormlyComponent,通过Storybook args配置包装器属性,代码如下:
import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { Component, Input } from '@angular/core'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormlyModule, type FormlyFieldConfig } from '@ngx-formly/core'; import { FormlyBootstrapModule } from '@ngx-formly/bootstrap'; import { FormComponentsModule } from '../lib/form-components/form-components.module'; import { FieldWrapperComponent } from '../lib/form-components/field-wrapper/field-wrapper.component'; @Component({ selector: 'storybook-formly', // eslint-disable-line @angular-eslint/component-selector template: ` <form [formGroup]="form"> <formly-form [model]="model" [fields]="fields" [options]="options" [form]="form"></formly-form> </form> ` }) export class StoryBookFormlyComponent { @Input() label: string; @Input() placeholder: string; @Input() description: string; @Input() required: boolean; @Input() inputType: string; form = new FormGroup({}); model = { example: '' }; options = {}; get fields(): FormlyFieldConfig[] { return [ { key: 'example', wrappers: [FieldWrapperComponent], type: this.inputType, props: { label: this.label, placeholder: this.placeholder, description: this.description, required: this.required } } ]; } } const meta: Meta<StoryBookFormlyComponent> = { title: 'Atoms/Forms/Standard Inputs', component: StoryBookFormlyComponent, tags: ['autodocs'], decorators: [ moduleMetadata({ imports: [ ReactiveFormsModule, FormComponentsModule, FormlyBootstrapModule, FormlyModule.forRoot({ validationMessages: [{ name: 'required', message: 'This field is required' }] }) ] }) ], render: (args: StoryBookFormlyComponent) => ({ props: args }), argTypes: {} }; export default meta; type Story = StoryObj<StoryBookFormlyComponent>; export const Primary: Story = { args: { label: 'Example Label 2', placeholder: 'My placeholder', description: 'Some description', required: true, inputType: 'input' } };
运行Storybook时组件显示正常,控件可管理属性,但点击输入框会立即触发错误:
ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'ng-untouched': 'true'. Current value: 'false'. Expression location: StoryBookFormlyComponent component.
此时验证触发并显示错误,但输入框失去焦点,无法重新聚焦或输入内容。原无Storybook args的示例无此问题,当前环境为Angular 16、Storybook 7.1.0,包装器组件代码如下:
import { Component } from '@angular/core'; import { FieldWrapper } from '@ngx-formly/core'; @Component({ selector: 'component-library-field-wrapper', template: ` <ng-template #labelTemplate> <label *ngIf="props.label && props.hideLabel !== true" [attr.for]="id" class="form-label"> {{ props.label }} <span *ngIf="props.required && props.hideRequiredMarker !== true" aria-hidden="true" class="required"> (Required)</span> </label> </ng-template> <div class="mb-3" [class.form-floating]="props.labelPosition === 'floating'" [class.has-error]="showError"> <ng-container *ngIf="props.labelPosition !== 'floating'"> <ng-container [ngTemplateOutlet]="labelTemplate"></ng-container> </ng-container> <div *ngIf="props.description" class="form-text">{{ props.description }}</div> <div *ngIf="showError" class="invalid-feedback"> <formly-validation-message [field]="field"></formly-validation-message> </div> <ng-template #fieldComponent></ng-template> <ng-container *ngIf="props.labelPosition === 'floating'"> <ng-container [ngTemplateOutlet]="labelTemplate"></ng-container> </ng-container> </div> ` }) export class FieldWrapperComponent extends FieldWrapper {}
解决方案
这个错误源于Angular变更检测机制检测到组件初始化后表达式值发生了变化,结合Formly与Storybook的场景,可通过以下方式修复:
方法1:延迟初始化Formly字段配置
将fields从getter改为在ngOnInit中初始化,避免每次变更检测都重新生成字段配置对象:
import { Component, Input, OnInit } from '@angular/core'; // 保留其他导入内容 @Component({ selector: 'storybook-formly', template: ` <form [formGroup]="form"> <formly-form [model]="model" [fields]="fields" [options]="options" [form]="form"></formly-form> </form> ` }) export class StoryBookFormlyComponent implements OnInit { @Input() label: string; @Input() placeholder: string; @Input() description: string; @Input() required: boolean; @Input() inputType: string; form = new FormGroup({}); model = { example: '' }; options = {}; fields: FormlyFieldConfig[]; ngOnInit(): void { this.fields = [ { key: 'example', wrappers: [FieldWrapperComponent], type: this.inputType, props: { label: this.label, placeholder: this.placeholder, description: this.description, required: this.required } } ]; } }
方法2:手动触发变更检测
若需保留getter写法,可注入ChangeDetectorRef并在视图初始化后手动触发变更检测:
import { Component, Input, ChangeDetectorRef, AfterViewInit } from '@angular/core'; // 保留其他导入内容 @Component({ selector: 'storybook-formly', template: ` <form [formGroup]="form"> <formly-form [model]="model" [fields]="fields" [options]="options" [form]="form"></formly-form> </form> ` }) export class StoryBookFormlyComponent implements AfterViewInit { @Input() label: string; @Input() placeholder: string; @Input() description: string; @Input() required: boolean; @Input() inputType: string; form = new FormGroup({}); model = { example: '' }; options = {}; constructor(private cdr: ChangeDetectorRef) {} ngAfterViewInit(): void { this.cdr.detectChanges(); } get fields(): FormlyFieldConfig[] { return [ { key: 'example', wrappers: [FieldWrapperComponent], type: this.inputType, props: { label: this.label, placeholder: this.placeholder, description: this.description, required: this.required } } ]; } }
方法3:调整包装器组件的错误判断逻辑
在包装器模板中,直接使用表单控件的状态组合判断错误,替代showError属性,减少变更检测冲突:
<div class="mb-3" [class.form-floating]="props.labelPosition === 'floating'" [class.has-error]="field.formControl.touched && field.formControl.invalid"> <!-- 其余模板内容保持不变 --> </div>
关键原因说明
原代码中使用get fields()会在每次变更检测时重新生成FormlyFieldConfig数组,导致Formly内部重复初始化字段控件,进而触发控件状态(如ng-untouched)在变更检测周期内发生变化,触发Angular严格模式错误。将字段配置改为初始化时生成,可避免重复创建配置对象,解决状态变更冲突。
内容的提问来源于stack exchange,提问作者richard
相关产品推荐
相关产品推荐

