Search code examples
angulartypescript

Creating a reusable Angular ArrayInputComponent


I am working on creating reusable form input components using Angular's reactive forms.

For my FormArray input component, I am facing challenges.

I found I needed to use double-type casting:

get arrayGroup(): FormGroup {
  return this.formArray as AbstractControl as FormGroup;
}

Because unless I wrapped the input HTML in something like this:

<div [formGroup]="arrayGroup"></div>

I got this error:

NG01053: formGroupName must be used with a parent formGroup directive. You'll want to add a formGroup directive and pass it to an existing FormGroup instance (you can create one in your class).

My solution works and does the job, but there must be a better way.

I have shared two components, a form, and an input.

** The components **

// FILENAME: forms.component.ts

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
  FormArray,
  FormBuilder,
  FormGroup,
  ReactiveFormsModule,
} from '@angular/forms';

import { ArrayInputComponent } from './array-input/array-input.component';

@Component({
  selector: 'app-forms',
  standalone: true,
  imports: [ArrayInputComponent, CommonModule, ReactiveFormsModule],
  template: `
    <div>
      <h1>Forms</h1>
      <form [formGroup]="form" (ngSubmit)="onSubmit()">
        <app-array-input [formArray]="formArray"></app-array-input>
        <button>Submit</button>
      </form>
    </div>
  `,
})
export class FormsComponent implements OnInit {
  form: FormGroup = this.fb.group({});

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {
    this.form.addControl('formArray', this.fb.array([]));
  }

  get formArray() {
    return this.form.get('formArray') as FormArray;
  }

  onSubmit() {
    console.log('Form valid: ', this.form.valid);
    console.log('Form values: ', this.form.value);
  }
}
// FILENAME: ./array-input/array-input.component

import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import {
  AbstractControl,
  FormArray,
  FormBuilder,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';

@Component({
  selector: 'app-array-input',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <div [formGroup]="arrayGroup">
      <h1>Array Input</h1>
      <button type="button" (click)="add()">Add</button>
      <ng-container *ngFor="let item of this.formArray.controls; let i = index">
        <div [formGroupName]="i">
          <input full placeholder="name" formControlName="name" />
          <input placeholder="relation" formControlName="relation" />
        </div>
      </ng-container>
    </div>
  `,
})
export class ArrayInputComponent {
  @Input() formArray!: FormArray;

  constructor(private fb: FormBuilder) {}

  add() {
    const item = this.fb.group({
      name: ['', [Validators.required]],
      relation: [],
    });

    this.formArray.push(item);
  }

  get arrayGroup(): FormGroup {
    return this.formArray as AbstractControl as FormGroup;
  }
}

Angular version 17


Solution

  • The ControlContainer DI can be used to fetch the parent FormGroup using

    this.formGroup = this.controlContainer!.control as FormGroup;
    

    Then we can use this formGroup to get the formArray.

      get formArray(): FormArray {
        return this.formGroup.get('formArray') as FormArray;
      }
    

    After this, we configure the formArrayName and formGroup which have their own separate DIV.

     <div [formGroup]="formGroup">
        <div formArrayName="formArray">
          <h1>Array Input</h1>
          <button type="button" (click)="add()">Add</button>
          <ng-container *ngFor="let item of this.formArray.controls; let i = index">
            <div [formGroupName]="i">
              <input full placeholder="name" formControlName="name" />
              <input placeholder="relation" formControlName="relation" />
            </div>
          </ng-container>
        </div>
      </div>
    

    After this your component, should work fine.

    Full Code:

    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    
    import { CommonModule } from '@angular/common';
    import { Component, Input } from '@angular/core';
    import {
      AbstractControl,
      ControlContainer,
      FormArray,
      FormBuilder,
      FormGroup,
      FormGroupName,
      NgForm,
      ReactiveFormsModule,
      Validators,
    } from '@angular/forms';
    
    @Component({
      selector: 'app-array-input',
      standalone: true,
      imports: [CommonModule, ReactiveFormsModule],
      template: `
      <div [formGroup]="formGroup">
        <div formArrayName="formArray">
          <h1>Array Input</h1>
          <button type="button" (click)="add()">Add</button>
          <ng-container *ngFor="let item of this.formArray.controls; let i = index">
            <div [formGroupName]="i">
              <input full placeholder="name" formControlName="name" />
              <input placeholder="relation" formControlName="relation" />
            </div>
          </ng-container>
        </div>
      </div>
      `,
    })
    export class ArrayInputComponent {
      formGroup: FormGroup = new FormGroup({});
    
      constructor(
        private fb: FormBuilder,
        private controlContainer: ControlContainer
      ) {}
    
      ngOnInit() {
        console.log(this.controlContainer.control);
        this.formGroup = this.controlContainer!.control as FormGroup;
      }
    
      add() {
        const item = this.fb.group({
          name: ['', [Validators.required]],
          relation: [],
        });
    
        this.formArray.push(item);
      }
    
      get formArray(): FormArray {
        return this.formGroup.get('formArray') as FormArray;
      }
    }
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [ArrayInputComponent, CommonModule, ReactiveFormsModule],
      template: `
        <div>
          <h1>Forms</h1>
          <form [formGroup]="form" (ngSubmit)="onSubmit()">
            <app-array-input></app-array-input>
            <button>Submit</button>
          </form>
        </div>
      `,
    })
    export class App {
      form: FormGroup = this.fb.group({});
    
      constructor(private fb: FormBuilder) {}
    
      ngOnInit(): void {
        this.form.addControl('formArray', this.fb.array([]));
      }
    
      get formArray() {
        return this.form.get('formArray') as FormArray;
      }
    
      onSubmit() {
        console.log('Form valid: ', this.form.valid);
        console.log('Form values: ', this.form.value);
      }
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo


    If you want something even more reusable, you can give the array name as an @Input working example below

    Stackblitz Demo