I am building up from previous question that i asked on StackOverflow and am trying to figure out a very weird problem.
Problem is that parent form is not having its valid status updated, it seems like validation doesn't propagate all the way up breaking the whole delegation principle.
Screenshot with child form with all data
This is as expected and returns all true, as it should be.
Screenshot with child form with nested form invalid:
This should show both as false, yet as you can see parent is showing true, its as if delegation stopped at person form.
Easiest way to replicate this is:
- ng new test
- Up to you if you choose routing, non-css, it really doesn't matter
- ng add @angular/material
- ng g c person-form
- ng g c address-form
Update the imports of App Module
@NgModule({
declarations: [
AppComponent,
PersonFormComponent,
AddressFormComponent
],
imports: [
BrowserModule,
FormsModule, // ADD
ReactiveFormsModule, // ADD
AppRoutingModule,
BrowserAnimationsModule,
MatFormFieldModule, //ADD
MatInputModule, //ADD
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Make the following updates to Person component
HTML
<fieldset [formGroup]="form">
<mat-form-field>
<input matInput placeholder="First name" formControlName="firstName" (blur)="onTouched()" />
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Last name" formControlName="lastName" (blur)="onTouched()" />
</mat-form-field>
<ng-container formArrayName="addresses">
<ng-container *ngFor="let addressForm of addresses.controls; index as i">
<app-address-form [formControlName]="i"></app-address-form>
<button (click)="removeAddressAtIndex(i)">Remove Address</button>
</ng-container>
</ng-container>
</fieldset>
<button (click)="addAddress()">Add Address</button>
<h3>The person form is valid: </h3><h2>{{form.valid}}</h2>
TS
@Component({
selector: 'app-person-form',
templateUrl: './person-form.component.html',
styleUrls: ['./person-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => PersonFormComponent),
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => PersonFormComponent),
},
],
})
export class PersonFormComponent
implements ControlValueAccessor, OnDestroy, Validator {
form: FormGroup = this.fb.group({
firstName: [null, Validators.required],
lastName: [null, Validators.required],
addresses: this.fb.array([]),
});
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { }
get addresses() {
return this.form.controls['addresses'] as FormArray;
}
addAddress() {
this.addresses.push(this.fb.control(null));
this.cdr.detectChanges();
}
removeAddressAtIndex(i: number) {
this.addresses.removeAt(i);
this.cdr.detectChanges();
}
onTouched: Function = () => { };
onChangeSubs: Subscription[] = [];
onValidationChange: any = () => { };
registerOnValidatorChange?(fn: () => void): void {
this.onValidationChange = fn;
}
ngOnDestroy() {
for (let sub of this.onChangeSubs) {
sub.unsubscribe();
}
}
registerOnChange(onChange: any) {
const sub = this.form.valueChanges.subscribe(onChange);
this.onChangeSubs.push(sub);
}
registerOnTouched(onTouched: Function) {
this.onTouched = onTouched;
}
setDisabledState(disabled: boolean) {
if (disabled) {
this.form.disable();
} else {
this.form.enable();
}
}
writeValue(value: any) {
if (value) {
console.log(value);
this.form.setValue(value, { emitEvent: false });
}
}
validate(control: AbstractControl) {
if (this.form.valid) {
return null;
}
let errors: any = {};
Object.keys(this.form.controls).forEach((e: any) => {
errors = this.addControlErrors(errors, e);
});
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
const controlErrors = this.form.controls[controlName].errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
}
Make the following changes to address component
HTML
<fieldset [formGroup]="form">
<mat-form-field>
<input matInput placeholder="Address Line 1" formControlName="addressLine1" (blur)="onTouched()" />
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Address Line 2" formControlName="addressLine2" (blur)="onTouched()" />
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Zip Code" formControlName="zipCode" (blur)="onTouched()" />
</mat-form-field>
<mat-form-field>
<input matInput placeholder="City" formControlName="city" (blur)="onTouched()" />
</mat-form-field>
</fieldset>
TS
@Component({
selector: 'app-address-form',
templateUrl: './address-form.component.html',
styleUrls: ['./address-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => AddressFormComponent),
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => AddressFormComponent),
},
],
})
export class AddressFormComponent
implements ControlValueAccessor, OnDestroy, Validator {
form: FormGroup = this.fb.group({
addressLine1: [null, Validators.required],
addressLine2: [null, Validators.required],
zipCode: [null, Validators.required],
city: [null, Validators.required],
});
onTouched: Function = () => { };
onChangeSubs: Subscription[] = [];
onValidationChange: any = () => { };
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { }
registerOnValidatorChange?(fn: () => void): void {
this.onValidationChange = fn;
}
ngOnDestroy() {
for (let sub of this.onChangeSubs) {
sub.unsubscribe();
}
}
registerOnChange(onChange: any) {
const sub = this.form.valueChanges.subscribe(onChange);
this.onChangeSubs.push(sub);
}
registerOnTouched(onTouched: Function) {
this.onTouched = onTouched;
}
setDisabledState(disabled: boolean) {
if (disabled) {
this.form.disable();
} else {
this.form.enable();
}
}
writeValue(value: any) {
if (value) {
console.log(value);
this.form.setValue(value, { emitEvent: false });
}
}
validate(control: AbstractControl) {
if (this.form.valid) {
return null;
}
let errors: any = {};
Object.keys(this.form.controls).forEach((e: any) => {
errors = this.addControlErrors(errors, e);
});
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
const controlErrors = this.form.controls[controlName].errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
}
Finishing touch needs to be applied to App Component
HTML
<form [formGroup]="form">
<app-person-form formControlName="person"></app-person-form>
<hr />
<ng-container formArrayName="addresses">
<ng-container *ngFor="let addressForm of addresses.controls; index as i">
<app-address-form [formControlName]="i"></app-address-form>
<button (click)="removeAddress(i)">Remove Address</button>
</ng-container>
</ng-container>
</form>
<button (click)="addAddress()">Add Address</button>
<h3>The parent form is valid: </h3><h2>{{form.valid}}</h2>
TS
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
form: FormGroup = this.fb.group({
person: [null, Validators.required],
totalQuantity: [
0,
[Validators.required, Validators.min(0), Validators.max(100)],
],
addresses: this.fb.array([]),
});
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {}
get addresses() {
return this.form.controls['addresses'] as FormArray;
}
removeAddress(i: number) {
this.addresses.removeAt(i);
this.cdr.detectChanges();
}
addAddress() {
this.addresses.push(this.fb.control(null));
this.cdr.detectChanges();
}
}
This is doing my head in as i cannot figure out why the propagation has effectively stopped.
CodePudding user response:
Complementary my comment. You need replace your validate function in person.component:
validate(control: AbstractControl) {
if (this.form.valid) {
return null;
}
let errors: any = {};
Object.keys(this.form.controls).forEach((e: any) => {
//see that if e=='adresses' this.form.controls['adresses'].errors is null
if (e== 'addresses')
{
this.addresses.controls.forEach((x,i) => {
errors = this.addControlErrors(errors, e '.' i);
});
}
else
errors = this.addControlErrors(errors, e);
});
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
//see that you use this.form.get(controlName)
//NOT this.form.controls[controlName]
//this allow you pass as name some like 'addresses.0'
const controlErrors = this.form.get(controlName).errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
See the forked stackblitz (in leyend of the .html I wrote the "errors" of the control)