Búsqueda de sitios web

typescript - ¿Cómo abordar la creación de un formulario complejo con muchos componentes personalizados?


Digamos que mi HTML generado desde la aplicación Angular2 se ve así:

<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>

¿Cómo puedo configurar mi <form> exterior de manera que pueda validar todos los inputs internos al enviar? ¿Tengo que pasar myForm a través de @Input() desde panel-component hasta inner-component-with-inputs? ¿O hay otra manera?

En mi aplicación tengo un formulario muy grande con múltiples paneles, subpaneles, pestañas, modales, etc., y necesito poder validarlo todo de una vez al enviar.

Todos los tutoriales y recursos en internet hablan solo sobre formularios que abarcan un componente/plantilla.

Respuesta #1

Un patrón común que verás en todo el código fuente de Angular, cuando están involucradas relaciones padre/hijo, es que el tipo padre se agregue a sí mismo como proveedor. Lo que esto hace es permitir que el componente hijo inyecte al padre. Y solo habrá una instancia del componente padre en todo el árbol de componentes debido a la inyección de dependencias jerárquica. A continuación se muestra un ejemplo de cómo podría verse esto

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);
  }
}

Algunas notas:

  • Estamos usando una interfaz/padre abstracto (FormControlContainer) por un par de razones

    1. Se desacopla el ParentComponent del ChildComponent. El hijo no necesita saber nada sobre el ParentComponent específico. Todo lo que sabe es sobre el FormControlContainer y el contrato que tiene.
    2. Solo exponemos métodos en el ParentComponent que se deseen, a través del contrato de la interfaz.
  • Solo anunciamos ParentComponent como FormControlContainer, así que este último es lo que inyectaremos.

  • Creamos un proveedor en la forma de formControlContainerProvider y luego agregamos ese proveedor al ParentComponent. Debido a la DI jerárquica, ahora todos los hijos tienen acceso al padre.

  • Si no estás familiarizado con forwardRef, este es un gran artículo

Ahora en el/los niño(s) solo puedes hacer

@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');
  }
}

En mi opinión, este es un diseño mucho mejor que pasar el FormGroup a través de @Inputs. Como se mencionó anteriormente, este es un diseño común en todo el código fuente de Angular, por lo que creo que es seguro decir que es un patrón aceptable.

Si quisieras hacer que los componentes hijos fueran más reutilizables, podrías hacer que el parámetro del constructor @Optional().

A continuación se muestra el código fuente completo que utilicé para probar los ejemplos anteriores

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');
  }
}

Artículos relacionados