我已经为这个问题苦苦挣扎了几个星期,但无法弄清楚是什么原因造成的。 (角度版本:16.1.4)
我用BehaviorSubject填充这个表单组件,当打印到控制台时,表单和数据库返回所有正确的值,但是,对于html模板中首先出现的任何字段,表单都会抛出以下错误。
ERROR Error: Cannot find control with name: 'empNum'
at _throwError (forms.mjs:3150:11)
at setUpControl (forms.mjs:2933:13)
at FormGroupDirective.addControl (forms.mjs:4782:9)
at FormControlName._setUpControl (forms.mjs:5370:43)
at FormControlName.ngOnChanges (forms.mjs:5315:18)
at FormControlName.rememberChangeHistoryAndInvokeOnChangesHook (core.mjs:2849:14)
at callHookInternal (core.mjs:3841:14)
at callHook (core.mjs:3872:9)
at callHooks (core.mjs:3823:17)
at executeInitAndCheckHooks (core.mjs:3773:9)
ERROR TypeError: Cannot read properties of null (reading 'invalid')
at HistoryFormComponent_Template (history-form.component.ts:19:15)
at ReactiveLViewConsumer.runInContext (core.mjs:10414:13)
at executeTemplate (core.mjs:10954:22)
at refreshView (core.mjs:12475:13)
at detectChangesInView (core.mjs:12639:9)
at detectChangesInComponent (core.mjs:12615:5)
at detectChangesInChildComponents (core.mjs:12653:9)
at refreshView (core.mjs:12525:13)
at detectChangesInView (core.mjs:12639:9)
at detectChangesInComponent (core.mjs:12615:5)
ERROR TypeError: Cannot read properties of null (reading 'invalid')
at HistoryFormComponent_Template (history-form.component.ts:19:15)
at ReactiveLViewConsumer.runInContext (core.mjs:10414:13)
at executeTemplate (core.mjs:10954:22)
at refreshView (core.mjs:12475:13)
at detectChangesInView (core.mjs:12639:9)
at detectChangesInComponent (core.mjs:12615:5)
at detectChangesInChildComponents (core.mjs:12653:9)
at refreshView (core.mjs:12525:13)
at detectChangesInView (core.mjs:12639:9)
at detectChangesInComponent (core.mjs:12615:5)
我可以切换 html 模板中字段的顺序,但表单中的第一个字段总是抛出此错误。当它不是第一个字段时,“empNum”控件将正确设置,并且初始值将按预期填充。我的预感是正在发生一些奇怪的异步行为,但我不知道如果是这种情况如何解决这个问题。
相同的代码结构正用于数据库中其他集合上的其他表单,并且在这些表单上一切都按预期工作。
这是该组件的完整代码:
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup, FormBuilder, Validators, FormControl } from '@angular/forms';
import { BehaviorSubject } from 'rxjs';
import { History } from '../history';
import { ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-history-form',
template: `
<!-- FORM -->
<form [formGroup]="historyForm" class="history-form" autocomplete="off" (ngSubmit)="submitForm()">
<div class="form-floating mb-3">
<input class="form-control" type="number" id="empNum" formControlName="empNum" placeholder="Employee Number">
<label for="empNum">Employee Number</label>
</div>
<div *ngIf="empNum.invalid && (empNum.dirty || empNum.touched)" class="alert alert-danger">
<div *ngIf="empNum.errors?.['required']">
Employee number is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="text" id="jobNum" formControlName="jobNum" placeholder="Job Number" required>
<label for="jobNum">Job Number</label>
</div>
<div *ngIf="jobNum.invalid && (jobNum.dirty || jobNum.touched)" class="alert alert-danger">
<div *ngIf="jobNum.errors?.['required']">
Job number is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="text" id="partNum" formControlName="partNum" placeholder="Part Number" required>
<label for="partNum">Part Number</label>
</div>
<div *ngIf="partNum.invalid && (partNum.dirty || partNum.touched)" class="alert alert-danger">
<div *ngIf="partNum.errors?.['required']">
Part number is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="text" id="machArea" formControlName="machArea" placeholder="Machine / Area" required>
<label for="machArea">Machine / Area</label>
</div>
<div *ngIf="machArea.invalid && (machArea.dirty || machArea.touched)" class="alert alert-danger">
<div *ngIf="machArea.errors?.['required']">
Machine / Area is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="text" id="action" formControlName="action" placeholder="Action" required>
<label for="action">Action</label>
</div>
<div *ngIf="action.invalid && (action.dirty || action.touched)" class="alert alert-danger">
<div *ngIf="action.errors?.['required']">
Action is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="number" id="tTime" formControlName="tTime" placeholder="Total Time" required>
<label for="tTime">Total Time</label>
</div>
<div *ngIf="tTime.invalid && (tTime.dirty || tTime.touched)" class="alert alert-danger">
<div *ngIf="tTime.errors?.['required']">
Total Time is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="number" id="qtyReq" formControlName="qtyReq" placeholder="Quantity Required" required>
<label for="qtyReq">Quantity Required</label>
</div>
<div *ngIf="qtyReq.invalid && (qtyReq.dirty || qtyReq.touched)" class="alert alert-danger">
<div *ngIf="qtyReq.errors?.['required']">
Quantity is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="number" id="qtyConf" formControlName="qtyConf" placeholder="Quantity Conforming" required>
<label for="qtyConf">Quantity Conforming</label>
</div>
<div *ngIf="qtyConf.invalid && (qtyConf.dirty || qtyConf.touched)" class="alert alert-danger">
<div *ngIf="qtyConf.errors?.['required']">
Quantity Conforming is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="number" id="qtyNonConf" formControlName="qtyNonConf" placeholder="Quantity Non-Conforming" required>
<label for="qtyNonConf">Quantity Non-Conforming</label>
</div>
<div *ngIf="qtyNonConf.invalid && (qtyNonConf.dirty || qtyNonConf.touched)" class="alert alert-danger">
<div *ngIf="qtyNonConf.errors?.['required']">
Quantity Non-conforming is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="number" id="opNum" formControlName="opNum" placeholder="Operation Number" required>
<label for="opNum">Operation Number</label>
</div>
<div *ngIf="opNum.invalid && (opNum.dirty || opNum.touched)" class="alert alert-danger">
<div *ngIf="opNum.errors?.['required']">
Operation Number is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="number" id="machCost" formControlName="machCost" placeholder="Machine Cost" required>
<label for="machCost">Machine Cost</label>
</div>
<div *ngIf="machCost.invalid && (machCost.dirty || machCost.touched)" class="alert alert-danger">
<div *ngIf="machCost.errors?.['required']">
Machine Cost is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="number" id="laborCost" formControlName="laborCost" placeholder="Labor Cost" required>
<label for="laborCost">Labor Cost</label>
</div>
<div *ngIf="laborCost.invalid && (laborCost.dirty || laborCost.touched)" class="alert alert-danger">
<div *ngIf="laborCost.errors?.['required']">
Labor Cost is required.
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control" type="number" id="totalCost" formControlName="totalCost" placeholder="Total Cost" required>
<label for="totalCost">Total Cost</label>
</div>
<div *ngIf="totalCost.invalid && (totalCost.dirty || totalCost.touched)" class="alert alert-danger">
<div *ngIf="totalCost.errors?.['required']">
Total Cost is required.
</div>
</div>
<!-- DATE TIME PICKER -->
<div class="form-floating mb-3">
<input
type="datetime-local"
class="form-control"
formControlName="dateTime"
placeholder="Select Date and Time"
id="dateTime"
/>
<label for="dateTime">Select Date and Time</label>
</div>
<div *ngIf="dateTime.invalid && (totalCost.dirty || totalCost.touched)" class="alert alert-danger">
<div *ngIf="totalCost.errors?.['required']">
Date is required.
</div>
</div>
<button class="btn btn-primary" type="submit" [disabled]="historyForm.invalid">Submit</button>
</form>
`,
styles: [
// `.history-form {
// max-width: 560px;
// margin-left: auto;
// margin-right: auto;
// }`
]
})
export class HistoryFormComponent implements OnInit {
@Input()
initialState: BehaviorSubject<History> = new BehaviorSubject(<History>{});
@Output()
formValuesChanged = new EventEmitter<History>();
@Output()
formSubmitted = new EventEmitter<History>();
historyForm: FormGroup = new FormGroup({});
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {}
get empNum() { return this.historyForm.get('empNum')!; }
get jobNum() { return this.historyForm.get('jobNum')!; }
get partNum() { return this.historyForm.get('partNum')!; }
get machArea() { return this.historyForm.get('machArea')!; }
get action() { return this.historyForm.get('action')!; }
get tTime() { return this.historyForm.get('tTime')!; }
get qtyReq() { return this.historyForm.get('qtyReq')!; }
get qtyConf() { return this.historyForm.get('qtyConf')!; }
get qtyNonConf() { return this.historyForm.get('qtyNonConf')!; }
get opNum() { return this.historyForm.get('opNum')!; }
get machCost() { return this.historyForm.get('machCost')!; }
get laborCost() { return this.historyForm.get('laborCost')!; }
get totalCost() { return this.historyForm.get('totalCost')!; }
get dateTime() { return this.historyForm.get('dateTime')!; }
// get dateTime() {
// const dateTime = String(this.historyForm.get('dateTime')!);
// return (new Date(dateTime).toISOString);
// }
ngOnInit() {
this.initialState.subscribe(history => {
this.historyForm = this.fb.group({
empNum: [history.empNum, [Validators.required]],
jobNum: [history.jobNum, [Validators.required]],
partNum: [history.partNum, [Validators.required]],
machArea: [history.machArea, [Validators.required]],
action: [history.action, [Validators.required]],
tTime: [history.tTime, [Validators.required]],
qtyReq: [history.qtyReq, [Validators.required]],
qtyConf: [history.qtyConf, [Validators.required]],
qtyNonConf: [history.qtyNonConf, [Validators.required]],
opNum: [history.opNum, [Validators.required]],
machCost: [history.machCost, [Validators.required]],
laborCost: [history.laborCost, [Validators.required]],
totalCost: [history.totalCost, [Validators.required]],
// to get correct initial dateTime format, must convert to JS Date object, then to ISO String,
// then slice off the timezone portion.
// pushing back to database: + ".000+00:00" to the date string.
dateTime: [new Date(history.dateTime).toISOString().slice(0, 19), [Validators.required]],
});
// Manually trigger change detection
this.cdr.detectChanges();
// console.log(this.historyForm.value);
// console.log(this.empNum.value, this.jobNum.value, this.opNum.value)
// const empNumState = this.historyForm.get('empNum')
// console.log(empNumState);
});
this.historyForm.valueChanges.subscribe((val) => { this.formValuesChanged.emit(val); });
}
submitForm() {
console.log(this.historyForm.value);
this.formSubmitted.emit(this.historyForm.value);
}
}
非常感谢任何可能的修复或提示。我是 Angular 的新手,所以我确信我可能遗漏了一些东西。
我尝试在 ngOnInit() 中手动触发更改检测,但没有成功。我还注释掉了 html 中的所有验证检查,这也没有改变任何内容。
通过在 FormBuilder 构造函数中初始化空 FormGroup 控件解决了此问题。
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {
this.historyForm = this.fb.group({
empNum: ["", [Validators.required]],
jobNum: ["", [Validators.required]],
partNum: ["", [Validators.required]],
machArea: ["", [Validators.required]],
action: ["", [Validators.required]],
tTime: ["", [Validators.required]],
qtyReq: ["", [Validators.required]],
qtyConf: ["", [Validators.required]],
qtyNonConf: ["", [Validators.required]],
opNum: ["", [Validators.required]],
machCost: ["", [Validators.required]],
laborCost: ["", [Validators.required]],
totalCost: ["", [Validators.required]],
dateTime: ["", [Validators.required]],
});
}
感谢永顺指出这一点!