Issue
I am figuring out how to refactor a complex form into smaller forms.
Essentially there is a parent form that will have a an array of child components which contain their owner forms.
Parent TS
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
form: FormGroup = this.fb.group({
mainAddress: [null, Validators.required],
addresses: this.fb.array([]),
});
constructor(private fb: FormBuilder) {}
get addresses() {
return this.form.controls['addresses'] as FormArray;
}
removeAddress(i: number) {
this.addresses.removeAt(i);
}
addAddress() {
this.addresses.push(
this.fb.control({
address: [null, Validators.required],
})
);
}
}
<form [formGroup]="form">
<app-address-form formControlName="mainAddress"></app-address-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>
Child Address TS
@Component({
selector: 'app-address-form',
templateUrl: './address-form.component.html',
styleUrls: ['./address-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: AddressFormComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: AddressFormComponent,
},
],
})
export class AddressFormComponent
implements ControlValueAccessor, OnDestroy, Validator
{
@Input()
legend: string;
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[] = [];
constructor(private fb: FormBuilder) {}
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 = {};
errors = this.addControlErrors(errors, 'addressLine1');
errors = this.addControlErrors(errors, 'addressLine2');
errors = this.addControlErrors(errors, 'zipCode');
errors = this.addControlErrors(errors, 'city');
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
const controlErrors = this.form.controls[controlName].errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
}
Child Address 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>
Error i get is :
Error: NG01002: Must supply a value for form control with name: 'addressLine1'
Please keep in mind i am trying to avoid duplication of code by separating out as much as i can (DRY principle).
Appreciate any feedback
Solution
You has an array of FormControls (*) (each FormControl "store" a complex object with properties: addressLine1,addressLine2,zipCode and city, but the FormArray is still a FormArray of FormControls)
So, your function addAddress should be like
addAddress() {
this.addresses.push(this.fb.control(null))
}
(*)Exists two approach when we work with nested forms. One is your approach: create a custom formControl that store a complex object and the "sub-formGroup" is created inside the component. The another is create the "sub-formGroup" in main component and pass the formGroup to a component (in this last case you not create a custom form control that implements ControlValueAccessor else a simple component with an @Input
Update about function validate in person.component and adress.component
In person.component you can crete a function validate like
control:AbstractControl //<--if you want to see the error
//in .html you can use
//{{control.errors|json}}
validate(control: AbstractControl) {
if (!this.control) this.control = control; //<--this line is only if
//we can "get" the control
if (this.form.valid)
return null;
//if only give a generic error
//return {error:'form not fullfilled'}
let errors: any = null;
Object.keys(this.form.controls).forEach((e: any) => {
//see that this.adresses is a FormArray, the formArray has no errors
//else his elements
if (e == 'addresses') {
this.addresses.controls.forEach((x,i) => {
const err = x.errors;
if (err) {
errors = errors || {};
errors[e+'.'+i] = err;
}
});
} else {
const err = this.form.get(e).errors;
if (err) {
errors = errors || {};
errors[e] = err;
}
}
});
return errors;
}
}
and when you add the adress use updateValueAndValidity
addAddress() {
this.addresses.push(this.fb.control(null));
this.cdr.detectChanges();
this.form.updateValueAndValidity();
}
The function validate to the adress.form like
validate(control: AbstractControl) {
if (!this.control) this.control = control;
if (this.form.valid) return null;
//if only give a generic error
//return {error:'form not fullfilled'}
let errors: any = null;
Object.keys(this.form.controls).forEach((e: any) => {
const err = this.form.get(e).errors;
if (err) {
errors = errors || {};
errors[e] = err;
}
});
return errors;
}
Your forked stackblitz
Answered By - Eliseo
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.