Search code examples
javascriptangularformsangular2-templateangular-template

Angular 11 how to extend a Form from a base component using a child component


My app has many Fees. All Fees share the same attributes except a few here and there. Say I have this structure:

// Base Fee
interface IFee {
    id: string;
    name: string;
    price: string;
    date: string;
}

interface IWebFee extends IFee {
    url: string;
    links: number;
}

interface IBookFee extends IFee {
    pageCount: number;
    bookTitle: string;
}

So lets say I wanted to create a Form to edit a BookFee. Content projection wont work since there wont be any context. So I tried creating an embedded view... but I still cant access the parent FormGroup to append controls to it. Here is what I have (which throws an error for missing control because I cant access the FormGroup from the BaseFeeFormComponent):

base-fee-form.component.ts

@Component({
    selector: 'app-base-fee-form',
    ...
    providers: [
    {
        provide: ControlContainer,
        useFactory: (comp: BaseFeeFormComponent) => comp.ngForm,
        deps: [BaseFeeFormComponent],
    },
  ],
})
export class BaseFeeFormComponent implements AfterContentInit {
    @ContentChild('feeChild') templateChild: TemplateRef<any>;
    @ViewChild('mountRef', { read: ViewContainerRef }) vcRef: ViewContainerRef;
    @ViewChild('ngForm') ngForm: FormGroupDirective;
    form: FormGroup;

    constructor(protected _fb: FormBuilder) {
        this.form = this._fb.group({
            name: [],
            price: [],
            date: [],
        });
    }

    ngAfterContentInit() {
        setTimeout(() => this.vc.createEmbeddedView(this.templateChild));
    }
}

base-fee-form.component.html

<form [formGroup]="form" #ngForm="ngForm">
    <div class="control-group">
        <span>Name: </span>
        <input type="text" formControlName="name" />
    </div>

    <div class="control-group">
        <span>Price: </span>
        <input type="text" formControlName="price" />
    </div>

    <div class="control-group">
        <span>Date: </span>
        <input type="date" formControlName="date" />
    </div>

    <div #mountRef></div>
</form>

book-fee-form.component.ts

@Component({
  selector: 'app-book-fee-form',
  templateUrl: './book-fee-form.component.html',
  styleUrls: ['./book-fee-form.component.css'],
  encapsulation: ViewEncapsulation.None,
})
export class BookFeeFormComponent {
  constructor(
      // private _formDirective: FormGroupDirective,
      private _fb: FormBuilder
    ) {
      // this._formDirective.form.addControl('pageCount', this._fb.control(0));
      // this._formDirective.form.addControl('bookTitle', this._fb.control(null));
  }

  ngOnInit() {}
}

book-fee-form.component.html

<app-base-fee-form>
  <ng-template #feeChild>
    <div class="control-group">
      <span>Page Count: </span>
      <input type="text" formControlName="pageCount" />
    </div>

    <div class="control-group">
      <span>Book Title: </span>
      <input type="text" formControlName="bookTitle" />
    </div>
  </ng-template>
</app-base-fee-form>

How do I access the parent NgForm to append the needed controls to the existing FormGroup? Rather, is there an easier way to do this? I'm trying to avoid creating components for each form that share nearly identical templates and functions.

I've created a Stackblitz to show my problem: StackBlitz


Solution

  • One solution would be to represent the controls as objects, and generate them dynamically

    export type control = {
      label: string;
      type: string;
      formControlName: string;
    };
    

    base-fee-form.component.html

    <form [formGroup]="form" #ngForm="ngForm">
      <ng-container *ngFor="let control of controls">
        <div class="control-group">
          <span>{{ control.label }}: </span>
          <input
            type="{{ control.type }}"
            formControlName="{{ control.formControlName }}"
          />
        </div>
      </ng-container>
    </form>
    

    Define your common controls in the base component

    export class BaseFeeFormComponent {
      controls: control[] = [
        { label: 'Name', type: 'text', formControlName: 'name' },
        { label: 'Price', type: 'text', formControlName: 'price' },
        { label: 'Date', type: 'date', formControlName: 'date' },
      ];
      form: FormGroup;
    
      constructor(protected _fb: FormBuilder) {
        this.form = this._fb.group({
          name: [],
          price: [],
          date: [null],
        });
      }
    }
    

    Then extend the base component to add new controls

    export class BookFeeFormComponent extends BaseFeeFormComponent {
      constructor(protected _fb: FormBuilder) {
        super(_fb);
        this.controls = this.controls.concat([
          { label: 'Page Count', type: 'text', formControlName: 'pageCount' },
          { label: 'Book Title', type: 'text', formControlName: 'bookTitle' },
        ]);
        this.form = this._fb.group({
          ...this.form.controls,
          pageCount: [],
          bookTitle: [],
        });
      }
    }
    

    You could make custom html for each component, or just point the child components to the base html

    @Component({
      selector: 'app-book-fee-form',
      templateUrl: '../base-fee-form/base-fee-form.component.html',
    ...
    

    Stackblitz: https://stackblitz.com/edit/angular-ivy-rwm5jw?file=src/app/book-fee-form/book-fee-form.component.ts