typescript - Hvordan takle å lage et komplekst skjema med mange tilpassede komponenter?
La oss si at den genererte HTML-en fra Angular2-appen min ser slik ut:
<app>
<form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm.value)">
<panel-component>
<mid-component>
<inner-component-with-inputs>
<input/>
<inner-component-with-inputs>
<mid-component>
</panel-component>
<panel-component>
<mid-component>
<inner-component-with-inputs>
<input/>
<inner-component-with-inputs>
<mid-component>
</panel-component>
<!-- many many many fields -->
<button type="submit">Submit</button>
</form>
</app>
Hvordan kan jeg sette opp min ytre <form> på en måte som gjør at jeg kan validere alle indre inputfelt ved innsending? Må jeg sende myForm gjennom @Input() helt ned fra panel-component til inner-component-with-inputs? Eller finnes det en annen måte?
I søknaden min har jeg et veldig stort skjema med flere paneler, underpaneler, faner, modaler osv., og jeg trenger å kunne validere alt på én gang når jeg sender inn.
Alle opplæringsvideoer og ressurser på internett snakker bare om skjemaer som går over én komponent/mal.
Svar #1
Et vanlig mønster du vil se gjennom hele Angular-kildekoden, når foreldrerelasjoner/barnrelasjoner er involvert, er at foreldret typen legger seg selv til som en provider for seg selv. Det dette gjør, er å la barnkomponenten injisere forelderen. Og det vil bare være én instans av foreldrekoden hele veien ned i komponenttreet på grunn av hierarkisk DI. Nedenfor er et eksempel på hvordan det kan se ut
export abstract class FormControlContainer {
abstract addControl(name: string, control: FormControl): void;
abstract removeControl(name: string): void;
}
export const formGroupContainerProvider: any = {
provide: FormControlContainer,
useExisting: forwardRef(() => NestedFormComponentsComponent)
};
@Component({
selector: 'nested-form-components',
template: `
...
`,
directives: [REACTIVE_FORM_DIRECTIVES, ChildComponent],
providers: [formGroupContainerProvider]
})
export class ParentComponent implements FormControlContainer {
form: FormGroup = new FormGroup({});
addControl(name: string, control: FormControl) {
this.form.addControl(name, control);
}
removeControl(name: string) {
this.form.removeControl(name);
}
}
Noen notater:
Vi bruker et grensesnitt/abstrakt foreldre (
FormControlContainer) av et par grunner- Det frigjør
ParentComponentfraChildComponent. Barnet trenger ikke å vite noe om den spesifikkeParentComponent. Alt det vet om erFormControlContainerog kontrakten det har.
- Det frigjør
- Vi eksponerer bare metoder på
ParentComponentsom ønsker det, gjennom grensesnittkontrakten. Vi annonserer kun
ParentComponentsomFormControlContainer, så det er sistnevnte vi vil injisere.Vi oppretter en leverandør i form av
formControlContainerProviderog legger deretter denne leverandøren tilParentComponent. På grunn av hierarkisk DI har nå alle barna tilgang til forelderen.Hvis du ikke er kjent med
forwardRef, er dette en flott artikkel
Nå i barnet/barna kan du bare gjøre
@Component({
selector: 'child-component',
template: `
...
`,
directives: [REACTIVE_FORM_DIRECTIVES]
})
export class ChildComponent implements OnDestroy {
firstName: FormControl;
lastName: FormControl;
constructor(private _parent: FormControlContainer) {
this.firstName = new FormControl('', Validators.required);
this.lastName = new FormControl('', Validators.required);
this._parent.addControl('firstName', this.firstName);
this._parent.addControl('lastName', this.lastName);
}
ngOnDestroy() {
this._parent.removeControl('firstName');
this._parent.removeControl('lastName');
}
}
Etter min mening er dette et mye bedre design enn å sende FormGroup gjennom @Input-er. Som nevnt tidligere, er dette et vanlig design gjennom hele Angular-koden, så jeg tror det er trygt å si at det er et akseptabelt mønster.
Hvis du ønsket å gjøre barnekomponentene mer gjenbrukbare, kunne du gjøre konstruktørparameteren @Optional().
Nedenfor er den komplette kilden jeg brukte for å teste eksemplene ovenfor
import {
Component, OnInit, ViewChildren, QueryList, OnDestroy, forwardRef, Injector
} from '@angular/core';
import {
FormControl,
FormGroup,
ControlContainer,
Validators,
FormGroupDirective,
REACTIVE_FORM_DIRECTIVES
} from '@angular/forms';
export abstract class FormControlContainer {
abstract addControl(name: string, control: FormControl): void;
abstract removeControl(name: string): void;
}
export const formGroupContainerProvider: any = {
provide: FormControlContainer,
useExisting: forwardRef(() => NestedFormComponentsComponent)
};
@Component({
selector: 'nested-form-components',
template: `
<form [formGroup]="form">
<child-component></child-component>
<div>
<button type="button" (click)="onSubmit()">Submit</button>
</div>
</form>
`,
directives: [REACTIVE_FORM_DIRECTIVES, forwardRef(() => ChildComponent)],
providers: [formGroupContainerProvider]
})
export class NestedFormComponentsComponent implements FormControlContainer {
form = new FormGroup({});
onSubmit(e) {
if (!this.form.valid) {
console.log('form is INVALID!')
if (this.form.hasError('required', ['firstName'])) {
console.log('First name is required.');
}
if (this.form.hasError('required', ['lastName'])) {
console.log('Last name is required.');
}
} else {
console.log('form is VALID!');
}
}
addControl(name: string, control: FormControl): void {
this.form.addControl(name, control);
}
removeControl(name: string): void {
this.form.removeControl(name);
}
}
@Component({
selector: 'child-component',
template: `
<div>
<label for="firstName">First name:</label>
<input id="firstName" [formControl]="firstName" type="text"/>
</div>
<div>
<label for="lastName">Last name:</label>
<input id="lastName" [formControl]="lastName" type="text"/>
</div>
`,
directives: [REACTIVE_FORM_DIRECTIVES]
})
export class ChildComponent implements OnDestroy {
firstName: FormControl;
lastName: FormControl;
constructor(private _parent: FormControlContainer) {
this.firstName = new FormControl('', Validators.required);
this.lastName = new FormControl('', Validators.required);
this._parent.addControl('firstName', this.firstName);
this._parent.addControl('lastName', this.lastName);
}
ngOnDestroy() {
this._parent.removeControl('firstName');
this._parent.removeControl('lastName');
}
}
