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

    1. Det frigjør ParentComponent fra ChildComponent. Barnet trenger ikke å vite noe om den spesifikke ParentComponent. Alt det vet om er FormControlContainer og kontrakten det har.
    2. Vi eksponerer bare metoder på ParentComponent som ønsker det, gjennom grensesnittkontrakten.
  • Vi annonserer kun ParentComponent som FormControlContainer, så det er sistnevnte vi vil injisere.

  • Vi oppretter en leverandør i form av formControlContainerProvider og legger deretter denne leverandøren til ParentComponent. 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');
  }
}

Relaterte artikler