Angular template inside reactive form

Viewed 365

In our application I noticed lots of duplicate code in the HTML of our forms, most input elements have the same structure so I want to create a generic input component that we can use instead to keep the files clean.

The problem I currently have is that there is 1 form that has 2 nested formGroups inside, example:

this.addressForm = this.fb.group({

  postalAddress: this.fb.group({
    street: ["", [Validators.required]],
  }),

  visitorAddress: this.fb.group({
    street: [""],
  })
});

This leads to my new component's HTML to also have duplicate code due to some forms requiring a formGroupName.

<div [formGroup]="form" *ngIf="form && controlPath && controlName">
<div *ngIf="groupName" [formGroupName]="groupName">
    <mat-form-field class="w-full">
        <mat-label>{{ label }}</mat-label>
        <input type="text" matInput [formControlName]="controlName" *ngIf="type === 'text'">
        <input type="number" matInput [formControlName]="controlName" *ngIf="type === 'number'">
        <mat-error *ngFor="let message of form.get(controlPath)?.errors?.['messages']">{{ message | i18n }}</mat-error>
    </mat-form-field>
</div>
<div *ngIf="!groupName">
    <mat-form-field class="w-full">
        <mat-label>{{ label }}</mat-label>
        <input type="text" matInput [formControlName]="controlName" *ngIf="type === 'text'">
        <input type="number" matInput [formControlName]="controlName" *ngIf="type === 'number'">
        <mat-error *ngFor="let message of form.get(controlPath)?.errors?.['messages']">{{ message | i18n }}</mat-error>
    </mat-form-field>
</div>

The code above works fine but as mentioned, I would like to get rid of the duplicate code. I figured this would be a good case for a ng-template but it appears when using a template the nested controls can no longer find the surrounding FormGroup.

Example

<div [formGroup]="form" *ngIf="form && controlPath && controlName">
<div *ngIf="groupName" [formGroupName]="groupName">
    <ng-content *ngTemplateOutlet="content"></ng-content>
</div>
<div *ngIf="!groupName">
    <ng-content *ngTemplateOutlet="content"></ng-content>
</div>

<ng-template #content>
    <mat-form-field class="w-full">
        <mat-label>{{ label }}</mat-label>
        <input type="text" matInput [formControlName]="controlName" *ngIf="type === 'text'">
        <input type="number" matInput [formControlName]="controlName" *ngIf="type === 'number'">
        <mat-error *ngFor="let message of form.get(controlPath)?.errors?.['messages']">{{ message | i18n }}</mat-error>
    </mat-form-field>
</ng-template>

Error: enter image description here

Has anyone encountered such a situation and if so, what would be a good way to go about this?

UPDATE

After Garbage Collectors answer I refactored my code to the following:

<div [formGroup]="form" *ngIf="form && controlPath && controlName">
<div *ngIf="groupName" [formGroupName]="groupName">
    <ng-container [ngTemplateOutlet]="inputTemplate" [ngTemplateOutletContext]="{ templateForm: form, templateFormGroup: groupName, templateControlName: controlName }"></ng-container>
</div>
<div *ngIf="!groupName">
    <ng-container [ngTemplateOutlet]="inputTemplate" [ngTemplateOutletContext]="{ templateForm: form, templateControlName: controlName }"></ng-container>
</div>

<ng-template #inputTemplate let-form="templateForm" let-groupName="templateFormGroup" let-controlName="templateControlName">
    <div [formGroup]="form" [formGroupName]="groupName">
        <mat-form-field class="w-full">
            <mat-label>{{ label }}</mat-label>
            <input type="text" matInput [formControlName]="controlName" *ngIf="type === 'text'">
            <input type="number" matInput [formControlName]="controlName" *ngIf="type === 'number'">
            <mat-error *ngIf="form.get(controlPath)?.errors?.['required']">{{ 'error.required' | i18n }}</mat-error>
            <mat-error *ngIf="form.get(controlPath)?.errors?.['email']">{{ 'error.email' | i18n }}</mat-error>
            <mat-error *ngIf="form.get(controlPath)?.errors?.['invalid']">{{ 'error.form.invalid' | i18n }}</mat-error>
            <mat-error *ngIf="form.get(controlPath)?.errors?.['minlength']">{{ minLengthError }}</mat-error>
            <mat-error *ngIf="form.get(controlPath)?.errors?.['pattern']">{{ patternError }}</mat-error>
            <mat-error *ngFor="let message of form.get(controlPath)?.errors?.['messages']">{{ message | i18n }}</mat-error>
        </mat-form-field>
    </div>
</ng-template>

Though the template now has all variables correctly, it cannot find the control path for non-nested forms. I outputted controlName in the template as a test and it is correct. I expect the error occurring due to the formGroupName being null for non-nested forms. Error: enter image description here

1 Answers

As far as I remember, every isolated piece of code displaying form controls should have a reference to the form where those controls were defined.

Actions

  • Use ng-container instead of ng-content to include your templates
  • Pass form as a parameter to your template
  • Assign form parameter to [formGroup] attribute inside your template

Template

<ng-template #demoTemplate let-form="demoForm">
  <div [formGroup]="form">
    <!-- Insert the rest of your template here -->
  </div>
</ng-template>

Main component using template

<div [formGroup]="form" *ngIf="form">
  <ng-container 
    [ngTemplateOutlet]="demoTemplate" 
    [ngTemplateOutletContext]="{ demoForm: form }">
  </ng-container>
</div>

Notice that attribute formGroup is added in both places, in the template and in the parent.

Related