In my application, I have a need for a reusable nested form component, such as Address. I want my AddressComponent to deal with its own FormGroup, so that I don't need to pass it from the outside.
At Angular conference (video, presentation) Kara Erikson, a member of Angular Core team recommended to implement ControlValueAccessor for the nested forms, making the nested form effectively just a FormControl.
I also figured out that I need to implement Validator, so that the validity of my nested form can be seen by the main form.
In the end, I created the SubForm class that the nested form needs to extend:
export abstract class SubForm implements ControlValueAccessor, Validator {
form: FormGroup;
public onTouched(): void {
}
public writeValue(value: any): void {
if (value) {
this.form.patchValue(value, {emitEvent: false});
this.onTouched();
}
}
public registerOnChange(fn: (x: any) => void): void {
this.form.valueChanges.subscribe(fn);
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
isDisabled ? this.form.disable()
: this.form.enable();
}
validate(c: AbstractControl): ValidationErrors | null {
return this.form.valid ? null : {subformerror: 'Problems in subform!'};
}
registerOnValidatorChange(fn: () => void): void {
this.form.statusChanges.subscribe(fn);
}
}
If you want your component to be used as a nested form, you need to do the following:
@Component({
selector: 'app-address',
templateUrl: './address.component.html',
styleUrls: ['./address.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AddressComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AddressComponent),
multi: true
}
],
})
export class AddressComponent extends SubForm {
constructor(private fb: FormBuilder) {
super();
this.form = this.fb.group({
street: this.fb.control('', Validators.required),
city: this.fb.control('', Validators.required)
});
}
}
Everything works well unless I check the validity status of my subform from the template of my main form. In this case ExpressionChangedAfterItHasBeenCheckedError is produced, see ngIf statement (stackblitz code) :
<form action=""
[formGroup]="form"
class="main-form">
<h4>Upper form</h4>
<label>First name</label>
<input type="text"
formControlName="firstName">
<div *ngIf="form.controls['address'].valid">Hi</div>
<app-address formControlName="address"></app-address>
<p>Form:</p>
<pre>{{form.value|json}}</pre>
<p>Validity</p>
<pre>{{form.valid|json}}</pre>
</form>