Angular响应式表单操作模态框后预加载数据丢失报NG0100错误
问题场景
项目侧边导航栏的某个导航链接会携带输入数据,调用存在异常的业务组件:
- 首次从侧边导航进入视图时,表单预加载数据可正常展示,组件运行正常
- 点击按钮触发模态框、关闭模态框后,输入框数据暂时显示正常
- 跳转侧边栏其他页面再返回当前页时,表单所有预加载数据全部消失,控制台抛出NG0100 表达式变更后被检查错误
- 手动添加
ngAfterViewChecked生命周期强制触发变更检测后,控制台报错消失,但预加载数据丢失的问题仍然存在
相关代码
业务组件TypeScript代码
@Input() data: MyData; myModal: BsModalRef; editForm: FormGroup; ngOnInit(){ this.editForm = this.formBuilder.group({ inOne: [this.data.one, [Validators.required]], inTwo: [this.data.two, [Validators.required]] }); } showModal() { const config: ModalOptions = { initialState: { message: 'Some dynamic tip message' } }; this.myModal = this.modalService.show(MyModalComponent, config); }
业务组件HTML模板代码
<form [formGroup]="editForm" (ngSubmit)="editForm.valid && save()"> <h3>SIMPLE MODE</h3> <app-text-input [formControl]="editForm.controls['inOne']" [type]="'number'"> </app-text-input> <app-text-input [formControl]="editForm.controls['inTwo']" [type]="'number'"> </app-text-input> <button class="btn btn-info btn-sm" (click)="showModal()">Tip</button> </form>
自定义通用输入组件app-text-input代码
<div class="form-group"> <input [class.is-invalid]='ngControl.touched && ngControl.invalid' type={{type}} class="form-control" [formControl]="ngControl.control" placeholder={{label}}" > <!-- some validators --> </div>
控制台报错信息
core.mjs:6485 ERROR Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value for 'ng-valid': 'true'. Current value: 'false'. at throwErrorIfNoChangesMode (core.mjs:6733:1) at bindingUpdated (core.mjs:12710:1) at checkStylingProperty (core.mjs:16451:1) at ɵɵclassProp (core.mjs:16359:1) at NgControlStatus_HostBindings (forms.mjs:1367:1) at processHostBindingOpCodes (core.mjs:9251:1) at refreshView (core.mjs:9530:1) at refreshComponent (core.mjs:10655:1) at refreshChildComponents (core.mjs:9280:1) at refreshView (core.mjs:9534:1)
已尝试的无效修复
ngAfterViewChecked(): void { this.changeDetectorRef.detectChanges(); }
根本原因
- 核心问题在自定义
app-text-input组件的实现不符合Angular表单规范:组件没有正确注入NgControl、也没有实现自定义表单控件需要的ControlValueAccessor接口,直接在模板里硬绑定ngControl.control属于非法访问表单控件内部引用的写法,本身就存在变更检测不稳定的隐患。 - 调用ngx-bootstrap的模态框服务时,模态框会启动独立的变更检测上下文,此时非法绑定的表单控件引用会被模态框的上下文污染;关闭模态框后,控件状态引用没有被正确释放,Angular在变更检测校验阶段发现单周期内
ng-valid状态从true突变为false,直接抛出NG0100错误。 - 之前添加的
detectChanges()只是强制Angular接受了突变的状态,没有修复引用污染的根因。当路由跳走再返回时,业务组件会被重建,此时被污染的表单控件引用无法正确接收初始化传入的data参数值,控件初始值为空,必填校验直接失败,最终表现为预加载数据丢失。 - 自定义输入组件模板里
placeholder={{label}}"末尾多写了一个双引号,属于语法小错误,也会干扰模板解析稳定性。
修复方案
重构自定义
app-text-input组件,按规范接入Angular表单体系- 首先在组件类的构造函数里注入
NgControl,并指定valueAccessor指向当前组件实例:
import { NgControl, ControlValueAccessor } from '@angular/forms'; // ... 其他导入 @Component({ selector: 'app-text-input', templateUrl: './text-input.component.html' // 注意不要在providers里重复添加NG_VALUE_ACCESSOR,否则会造成循环依赖 }) export class TextInputComponent implements ControlValueAccessor { @Input() type: string = 'text'; @Input() label: string = ''; value: any; onChange = (value: any) => {}; onTouched = () => {}; constructor(@Self() public ngControl: NgControl) { this.ngControl.valueAccessor = this; } writeValue(value: any): void { this.value = value; } registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouched = fn; } }- 修正组件模板,移除非法的
[formControl]="ngControl.control"绑定,修复多余的引号语法错误:
<div class="form-group"> <input [class.is-invalid]='ngControl.touched && ngControl.invalid' [type]="type" class="form-control" [value]="value" (input)="onChange($any($event.target).value)" (blur)="onTouched()" [placeholder]="label" > <!-- some validators --> </div>- 首先在组件类的构造函数里注入
移除之前在业务组件里加的
ngAfterViewChecked中强制调用detectChanges()的代码,这种写法不仅会掩盖真实的变更检测异常,还会造成不必要的性能损耗。监听模态框隐藏事件,在模态框关闭后主动标记组件状态为脏,清理模态框带来的变更检测上下文残留:
showModal() { const config: ModalOptions = { initialState: { message: 'Some dynamic tip message' } }; this.myModal = this.modalService.show(MyModalComponent, config); this.myModal.onHidden?.subscribe(() => { this.changeDetectorRef.markForCheck(); }); }(可选优化)将表单初始化逻辑调整到
ngOnChanges生命周期中,监听输入参数data的变化,每次路由携带新参数进入组件时都能正确同步值到表单,避免路由复用场景下的数据不同步问题:ngOnChanges(changes: SimpleChanges) { if (changes['data'] && this.data) { if (!this.editForm) { this.editForm = this.formBuilder.group({ inOne: [this.data.one, [Validators.required]], inTwo: [this.data.two, [Validators.required]] }); } else { this.editForm.patchValue({ inOne: this.data.one, inTwo: this.data.two }); } } }
内容的提问来源于stack exchange,提问作者Mortrus
相关产品推荐
相关产品推荐

