How to tackle creating complex form with lots of custom components?

后端 未结 2 873
再見小時候
再見小時候 2020-12-14 04:31

Let\'s say that my generated html from angular2 app looks like this:


&l
2条回答
  •  臣服心动
    2020-12-14 05:14

    A common pattern you will see throughout the Angular source code, when parent/child relationships are involved, is the parent type adding itself as a provider to itself. What this does is allow child component to inject the parent. And there will on only be one instance of the parent component all the way down the component tree because of hierarchical DI. Below is an example of what that might look like

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

    Some notes:

    • We're using an interface/abstract parent (FormControlContainer) for a couple reasons

      1. It decouples the ParentComponent from the ChildComponent. The child doesn't need to know anything about the specific ParentComponent. All it knows about is the FormControlContainer and the contract that is has.
      2. We only expose methods on the ParentComponent that want, through the interface contract.
    • We only advertise ParentComponent as FormControlContainer, so the latter is what we will inject.

    • We create a provider in the form of the formControlContainerProvider and then add that provider to the ParentComponent. Because of hierarchical DI, now all the children have access to the parent.

    • If you are unfamiliar with forwardRef, this is a great article

    Now in the child(ren) you can just do

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

    IMO, this is a much better design than passing the FormGroup through @Inputs. As stated earlier, this is a common design throughout the Angular source, so I think it's safe to say that it's an acceptable pattern.

    If you wanted to make the child components more reusable, you could make the constructor parameter @Optional().

    Below is the complete source I used to test the above examples

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

提交回复
热议问题