Reputation: 8434
I've created a simple nested form that depends on the child component rather than putting everything into one component. I've implemented a very basic ControlValueAccessor to enable it to work in such fashion, however what I noticed is that validation doesn't get captured at parent component. This means a faulty form can be submitted even though its not valid.
How do i get the validation of child forms to propagate up to the parent form.
Below is the code.
Parent HTML
<form [formGroup]="parentForm" (ngSubmit)="submitData()">
<h3>Parent</h3>
<br>
<ng-container formArrayName="children">
<ng-container *ngFor="let c of children?.controls; index as j">
<app-child [formControlName]="j"></app-child>
</ng-container>
</ng-container>
<button mat-raised-button type="button" (click)="addChild()">Add Activity</button>
<button mat-raised-button type="submit" color="warn" [disabled]="!parentForm.valid">Submit</button>
</form>
<pre>
{{parentForm.valid}} // this is always true because its not getting validator state from children
</pre>
<pre>
{{parentForm.value | json}}
</pre>
Parent TS
export class ParentComponent implements OnInit {
parentForm: FormGroup;
constructor(private fb: FormBuilder) {
}
ngOnInit(): void {
this.createForm();
}
private createForm() {
this.parentForm = this.fb.group({
children: this.fb.array([])
});
}
get children(): FormArray {
return this.parentForm.get("children") as FormArray;
}
addChild() {
const tempChild = new FormControl({
description: null
});
this.children.push(tempChild);
}
submitData() {
console.info(JSON.stringify(this.parentForm.value));
}
}
Child HTML
<form [formGroup]="newChildForm">
<tr>
<td>
<mat-form-field>
<mat-label>Description</mat-label>
<input type="text" matInput formControlName="description" required>
</mat-form-field>
</td>
</tr>
</form>
Child TS
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ChildComponent),
multi: true
}
]
})
export class ChildComponent implements ControlValueAccessor, OnInit {
newChildForm: FormGroup;
constructor(private fb: FormBuilder) {
this.createForm();
}
private createForm() {
this.newChildForm = this.fb.group({
description: [null, Validators.required],
});
}
onTouched: () => void = () => { };
writeValue(value: any) {
if (value) {
this.newChildForm.setValue(value, { emitEvent: true });
}
}
registerOnChange(fn: (v: any) => void) {
this.newChildForm.valueChanges.subscribe((val) => {
fn(val);
});
}
setDisabledState(disabled: boolean) {
disabled ? this.newChildForm.disable() : this.newChildForm.enable();
}
registerOnTouched(fn: () => void) {
this.onTouched = fn;
}
ngOnInit(): void { }
}
EDIT 2: nested approach
Stackblitz Link with Validator and ChangeDetection
Upvotes: 7
Views: 7390
Reputation: 6716
You can achieve that by implementing Validator interface in your child component, which requires to write your validate
method within child component, to be checked every time the value changes, and it could be like the following in your case:
@Component({
selector: "app-child",
templateUrl: "./child.component.html",
styleUrls: ["./child.component.css"],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ChildComponent),
multi: true,
},
{
// >>>>>> 1- The NG_VALIDATORS should be provided here <<<<<
provide: NG_VALIDATORS,
useExisting: ChildComponent,
multi: true,
},
],
})
export class ChildComponent implements ControlValueAccessor, Validator, OnInit {
newChildForm: FormGroup;
onChange: any = () => {};
onValidationChange: any = () => {};
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {
this.createForm();
}
private createForm() {
this.newChildForm = this.fb.group({
description: [null, Validators.required],
children: this.fb.array([]),
});
}
onTouched: () => void = () => {};
writeValue(value: any) {
if (value) {
this.newChildForm.setValue(value, { emitEvent: true });
}
}
registerOnChange(fn: (v: any) => void) {
this.onChange = fn;
}
setDisabledState(disabled: boolean) {
disabled ? this.newChildForm.disable() : this.newChildForm.enable();
}
registerOnTouched(fn: () => void) {
this.onTouched = fn;
}
ngOnInit(): void {
this.newChildForm.valueChanges.subscribe((val) => {
this.onChange(val);
// >>>> 4- call registerOnValidatorChange after every changes to check validation of the form again <<<<
this.onValidationChange();
});
}
// >>>> 2- validate method should be added here <<<<
validate(): ValidationErrors | null {
if (this.newChildForm?.invalid) {
return { invalid: true };
} else {
return null;
}
}
// >>>> 3- add registerOnValidatorChange to call it after every changes <<<<
registerOnValidatorChange?(fn: () => void): void {
this.onValidationChange = fn;
}
get children(): FormArray {
return this.newChildForm.get("children") as FormArray;
}
addChild() {
const tempChild = new FormControl({
description: null,
children: [],
});
this.children.push(tempChild);
this.cdr.detectChanges();
}
}
Upvotes: 8