Search code examples
angularangular-reactive-formsangular-formbuilderangular-signals

How to dynamically add additional formControls via template outlet?


So, I have the issue that I have a base component that has a form. But I would like to be able to add another field to it. Why? Because I have four pages, with a very similar structure, that differ only in one field at most. I've got a stackblitz that should get the idea across as well. The rendering part is working nice and well.

But to also give a general idea:

@Component({
  selector: 'app-base-form',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  template: `<form
  style="display: flex; flex-direction: column; gap: 1rem"
  [formGroup]="form"
>
  <input formControlName="search" type="text" placeholder="Search something" />

  <ng-template
    [ngTemplateOutletContext]="{ $implicit: form }"
    [ngTemplateOutlet]="seachOptionalField()!"
  ></ng-template>
</form>`,
})
export class BaseFormComponent {
  seachOptionalField = input<TemplateRef<any>>();
  fb = inject(UntypedFormBuilder);
  form = this.fb.group({
    search: this.fb.control(['']),
  });
}

The idea was initially to pass the form via the outlet context, but programmatically adding a control didn't really impact the original form. Also, there was the question on how to apply the rendered (to be used) form control.

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, BaseFormComponent],
  template: `
    <app-base-form [seachOptionalField]="searchOptionalField"></app-base-form>
    <ng-template #searchOptionalField let-form>
    <input type="text" placeholder="We just added this" />
    {{form.value | json}}
    </ng-template>
  `,
})
export class App {}

Solution

  • You need to provide a corresponding formControlName on the new controls, also wrap them on a form element inside the template.

    You can also add an extra parameter, which specifies the new form controls configuration on a object which we can destructure into our form group when initializing.

    Working example below with stackblitz!

    Full Code:

    main.ts

    import { Component, inject } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { CommonModule } from '@angular/common';
    import 'zone.js';
    import { BaseFormComponent } from './app/base-form/base-form.component';
    import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms';
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [CommonModule, BaseFormComponent, ReactiveFormsModule],
      template: `
        <app-base-form [seachOptionalField]="searchOptionalField" [extraFormParams]="extraFormParams"></app-base-form>
        <ng-template #searchOptionalField let-form>
          <form [formGroup]="form">
            <input type="text" placeholder="We just added this" formControlName="search2"/>
        </form>
        {{form.value | json}}
        </ng-template>
      `,
    })
    export class App {
      fb = inject(UntypedFormBuilder);
      extraFormParams = {
        search2: this.fb.control(['']),
      };
    }
    
    bootstrapApplication(App);
    

    base-form.ts

    import { CommonModule } from '@angular/common';
    import { Component, TemplateRef, inject, input } from '@angular/core';
    import {
      FormGroup,
      ReactiveFormsModule,
      UntypedFormBuilder,
    } from '@angular/forms';
    
    @Component({
      selector: 'app-base-form',
      standalone: true,
      imports: [ReactiveFormsModule, CommonModule],
      template: `<form
      style="display: flex; flex-direction: column; gap: 1rem"
      [formGroup]="form"
    >
      <input formControlName="search" type="text" placeholder="Search something" />
    
      <ng-template
        [ngTemplateOutletContext]="{ $implicit: form }"
        [ngTemplateOutlet]="seachOptionalField()!"
      ></ng-template>
    </form>`,
    })
    export class BaseFormComponent {
      seachOptionalField = input<TemplateRef<any>>();
      extraFormParams = input<Object>({});
      fb = inject(UntypedFormBuilder);
      form!: FormGroup;
    
      ngOnInit() {
        this.form = this.fb.group({
          search: this.fb.control(['']),
          ...this.extraFormParams(),
        });
      }
    }
    

    Stackblitz Demo