Angular 大学提供了示例如何将嵌套表单与 ControlValueAccessor 结合使用。
他们创建一个单独的表单组件并将其用作子表单:
<div [formGroup]="form">
... other form controls
<address-form formControlName="address" legend="Address"></address-form>
</div>
如果我在父窗体上调用 markAllAsTouched 方法,我希望将嵌套窗体的所有字段标记为触摸。
<form [formGroup]="form">
<app-address-form formControlName="address"></app-address-form>
</form>
<button (click)="markAsTouched()">Mark as touched</button>
export class AppComponent {
public form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = fb.group({ address: fb.control({ city: '' }) });
}
public markAsTouched() {
this.form.markAllAsTouched();
}
}
可以使用这种方法吗?
我在 StackBlitz 上构建了一个 简化版本。
单击按钮时实际发生的情况是将应用程序地址表单标记为已触摸:
<app-address-form formcontrolname="address" class="ng-pristine ng-valid ng-touched">
...
</app-address-form>
但我想将其传播到子表单及其字段。
我找到了以下将 markAsTouched 传播到子控件的解决方案。
以下递归函数有帮助:
const updateFormControlTree = (abstractControl: AbstractControl): void => {
const forEachChildControl = (
control: AbstractControl,
callbackFunction: (abstractControl: AbstractControl) => void): void => {
const childControls = (control as AbstractControl & { controls?: [] }).controls;
if (!childControls) {
return;
}
if (typeof childControls === 'object') {
const extractedChildControls: AbstractControl[] =
Object.values(childControls);
extractedChildControls.forEach((childControl) => {
callbackFunction(childControl);
});
}
};
forEachChildControl(abstractControl, (control: AbstractControl) =>
updateFormControlTree(control)
);
abstractControl.markAsTouched();
};
我必须使用上面的函数覆盖控件的markAsTouched。为此,我需要 FormControl 而不是 formControlName 属性:
<app-address-form [formControl]="addressControl"></app-address-form>
这就是我可以重写该函数的方法:
export class AddressFormComponent implements ControlValueAccessor, Validator, OnInit
{
@Input() formControl!: FormControl;
public ngOnInit(): void {
this.formControl.markAsTouched = () => updateFormControlTree(this.form);
}
...
}
最后要做的是将我的控件标记为从父窗体中触摸:
export class AppComponent {
public addressControl = new FormControl();
public form: FormGroup = this.fb.group({
address: this.addressControl,
});
constructor(private fb: FormBuilder) {
}
markAsTouched() {
this.addressControl.markAsTouched();
}
}
我在 StackBlitz 上构建了一个 工作示例。
附注上述解决方案的灵感来自于 Angular Framework 源代码中的更新值和有效性的方法。这个方法是私有的。不过如果能公开就好了。
可能有更好的方法,但是,您可以通过使用
Input
来实现这一点
父组件ts
export class AppComponent {
public form: FormGroup;
setAsTouched: boolean = false;
constructor(private fb: FormBuilder) {
this.form = fb.group({ address: fb.control({ city: '' }) });
}
public markAsTouched() {
this.setAsTouched = true;
}
}
父模板组件
<h1>Form</h1>
<form [formGroup]="form">
<app-address-form
formControlName="address"
[mark]="setAsTouched"
></app-address-form>
</form>
<button (click)="markAsTouched()">Mark as touched</button>
子组件 ts
...
...
export class AddressFormComponent implements ControlValueAccessor, Validator {
@Input() set mark(isTouched: boolean) {
if (isTouched) {
this.form.markAllAsTouched();
}
}
...
...
...
}
实际上,解释链接的嵌套表单组的管理有点混乱,因为您确实没有有嵌套表单组。你有一个带有一个控件的 FormGroup(控件的值是一个对象,但你只有一个控件)
看到在你的例子中你写的是:
this.form = fb.group({ address: fb.control({ city: '' }) });
嵌套的 formGroup 应该是这样的:
this.form = fb.group({ address: fb.group({ city: '' }) });
但是这个formGroup不能像Angular大学所说的那样进行管理。方法很简单:使用组件并将 formGroup 作为 Input(*)
传递这就是因为只触碰了“控制”的原因
解决方案:您可以使用模板引用变量并传递给您的函数
<form [formGroup]="form">
<app-address-form #address formControlName="address"></app-address-form>
</form>
<button (click)="markAsTouched(address)">Mark as touched</button>
public markAsTouched(address:any) {
this.form.markAllAsTouched();
//see that you can access to the form of the "adress" simply
//using adress.form
address.form.markAllAsTouched()
}
(*) 组件(不是从 ControlValueAccesor 实现)只是
@Component({
selector: 'app-address-form',
template: `
<div [formGroup]="form">
<label>City</label>
<input formControlName="city" (blur)="onTouched()">
</div>
`
})
export class AddressFormComponent {
form:FormGroup
@Input('form') set _(value){
this.form=value as FormGroup
}
}
你用as
<app-address-form [form]=form.get('address')><app-address-form>
这在 Angular 18+ 中可以使用 formControl.events observable 实现。
parentControl.events
.pipe(
filter(e => e instanceof TouchedChangeEvent),
filter(e => e.touched),
tap(() => childControl.markAllAsTouched())
).subscribe()