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

Time:02-06

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 >
        <span>Name: </span>
        <input type="text" formControlName="name" />
    </div>

    <div >
        <span>Price: </span>
        <input type="text" formControlName="price" />
    </div>

    <div >
        <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 >
      <span>Page Count: </span>
      <input type="text" formControlName="pageCount" />
    </div>

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

CodePudding user response:

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

  •  Tags:  
  • Related