I try to implement nested reactive forms in angular.
But when I call markAllAsTouched() and updateValueAndValidity() in the submit function the nested inputs are not touched and therefore the error message isn't displayed.
I tried a hack to touch the inputs when the outer form is touched with this code in the statusChanges subscription
if (!this.control.touched && this.parentForm.touched) {
this.control.markAsTouched();
this.control.updateValueAndValidity();
}
But this leads to every form been touched when I enter text in one of them and therefore displaying the error message to soon.
What am I doing wrong? Are reactive forms in angular meant to be used this way? I want to have a single button that touches and updates every nested form element in the outer form.
I provided a repo on github (https://github.com/to-cl/testform) where I recreated the error.
In appComponent there is a testForm that includes several custom input fields.
Template:
<form [formGroup]="testForm">
<app-myinput [parentForm]="testForm" label="Comment" type="text" formControlName="comment"></app-myinput>
<app-myinput [parentForm]="testForm" label="Name" type="text" formControlName="name"></app-myinput>
<app-myinput [parentForm]="testForm" label="Email" type="text" formControlName="email"></app-myinput>
<app-myinput [parentForm]="testForm" label="Password" type="text" formControlName="password"></app-myinput>
<button style="margin: 1em;" (click)="onSubmit()">Submit</button>
</form>
Component:
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'form-test';
testForm!: FormGroup;
constructor(private _formBuilder: FormBuilder) { }
ngOnInit(): void {
this.testForm = this._formBuilder.group({
comment: [],
name: [],
email: [],
password: [],
});
}
onSubmit() {
console.log(this.testForm.valid)
this.testForm.markAllAsTouched();
this.testForm.updateValueAndValidity();
}
}
The custom input fields are components on their own:
Template:
<form [formGroup]="form" style="padding: 1em;">
<label style="padding-right: 1em">{{label}}</label>
<input id="value" formControlName="value" type="text" [required]="required">
<small style="color: rgb(209, 67, 67)" *ngIf="control.touched && control.hasError('required')">
required
</small>
</form>
Component:
import { AfterViewInit, Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-myinput',
templateUrl: './myinput.component.html',
styleUrls: ['./myinput.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MyinputComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MyinputComponent),
multi: true
}
]
})
export class MyinputComponent implements ControlValueAccessor, AfterViewInit, OnDestroy {
form: FormGroup;
subscriptions: Subscription[] = [];
@Input()
label: string = '';
@Input()
formControlName = '';
@Input()
parentForm!: FormGroup;
@Input()
required: boolean = true;
get control() {
return this.form.controls['value'];
}
get value(): any {
return this.form.value;
}
set value(value: any) {
this.control.setValue(value)
this.onChange(value);
this.onTouched();
}
writeValue(value: string): void {
}
constructor(private formBuilder: FormBuilder) {
this.form = this.formBuilder.group({
value: ['', Validators.required]
});
this.subscriptions.push(
// any time the inner form changes update the parent of any change
this.form.valueChanges.subscribe(value => {
this.onChange(value.value);
this.onTouched();
this.manipulateValidators();
})
);
}
ngAfterViewInit(): void {
this.addStatusChangeSubscription();
this.addValueChangeSubscription();
}
ngOnDestroy(): void {
this.subscriptions.forEach(s => s.unsubscribe());
}
addStatusChangeSubscription() {
this.subscriptions.push(
this.parentForm.statusChanges.subscribe(() => {
if (this.parentForm.disabled && !this.form.disabled) {
this.form.disable();
}
if (this.parentForm.enabled && !this.form.enabled) {
this.form.enable();
}
if (!this.control.touched && this.parentForm.touched) {
this.control.markAsTouched();
this.control.updateValueAndValidity();
}
})
);
}
addValueChangeSubscription() {
this.subscriptions.push(
this.parentForm.valueChanges.subscribe(value => {
if (this.value.value !== value[this.formControlName]) {
this.form.patchValue({ value: value[this.formControlName] }, { emitEvent: false })
}
})
);
}
validate(_: FormControl) {
let formInvalid: any = {};
formInvalid[this.formControlName] = { valid: false };
return this.form.valid ? null : formInvalid;
}
onChange: any = () => {
/* Left blank intentionally */
};
onTouched: any = () => {
/* Left blank intentionally */
};
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
manipulateValidators(): void {
if (this.required && !this.form.controls['value'].hasValidator(Validators.required)) {
this.form.controls['value'].addValidators(Validators.required);
}
if (!this.required && this.form.controls['value'].hasValidator(Validators.required)) {
this.form.controls['value'].removeValidators(Validators.required);
}
}
}