How to Create a Flexible Angular Array Input Component

I have been working on developing reusable form input components using Angular's reactive forms.

However, I am currently encountering challenges with my FormArray input component.

To overcome some issues, I had to resort to using double-type casting:

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

Without wrapping the input HTML within a container like this:

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

I was facing the following 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).

Although my current solution is functional, I believe there must be a more efficient way to handle this.

As part of my approach, I have included two components: a form and an input.

** Components Overview **

// 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;
  }
}

Using Angular version 17

Answer №1

Utilizing the ControlContainer DI allows for accessing the parent FormGroup by:

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

Subsequently, the formGroup can then be used to retrieve the formArray.

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

Following that, the formArrayName and formGroup need to be configured within their respective DIV elements.

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

By completing these steps, your component should function correctly.

Complete 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);

View Stackblitz Demo


For enhanced reusability, you can provide the array name as an @Input as demonstrated in the following example

View Stackblitz Demo

Answer №2

When working with Angular, a FormArray can consist of FormControls or FormGroups.

In the latest version of Angular (17), you can define the following:

formArrayOfControls!:FormArray<FormControl>
formArrayOfGroups!:FormArray<FormGroup>

This allows you to iterate through the form controls and form groups.

With the introduction of the new @for, you can use:

@for (control of formArrayOfControls.controls;let i=$index;track i)
{
  // ensure you use "formControl"
  <input [formControl]="control"/>
}

//and 
@for (group of formArrayOfGroups.controls;track $index)
{
  <div [formGroup]="group">
     <!--you can use formControlName here, for example-->
     <input formControlName="control"/>
  </div>
 }

If you prefer, you can still use the old *ngFor syntax:

<div *ngFor="let control of formArray.controls">
  <input [formControl]="control">
</div>

//and
<div *ngFor="let group of formArray.controls" [formGroup]="group">
  <input formControlName="control">
</div>

To pass the FormArray itself to your component, simply do the following:

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

Then, you can pass it like this:

<app-array-input [formArray]="formArray"></app-array-input>

For a demo, check out the Naren forked stackblitz

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Error Encountered in Webpack Development Server: Module 'ipaddr.js' Not Found

I encountered an error when running the webpack dev server, as shown below: After trying to run it with different methods using both local and global packages, I am currently using the following versions: "webpack": "^2.5.1", "webpack-dev-server": " ...

Implement Bootstrap into Asp.Net Core 6 for the scaffolded Identity _Layout.cshtml

In my development environment using VS2022 Preview, I have set up a Single Page Application with Angular and the default project provided by VS2022. To customize the appearance of the Identity pages, I decided to override Bootstrap with a style from Bootsw ...

What is causing elements like divs, paragraphs, or text not to display within an ion-item after an ion-input is added?

I am currently working on validating a simple form and everything seems to be functioning properly. However, I have encountered an issue with displaying text messages within an ionic 3 list item. The ion-item consists of an ion-input element. When I place ...

Repetitive cycling through an array

My array consists of classes: const transferClasses = [ { id: "c5d91430-aaab-ed11-8daf-85953743f5cc", name: "Class1", isTransfer: false, children: [], }, { id: "775cb75d-aaab-ed11-8daf-85953743f5cc", ...

Overriding a shared module service in Angular from a separate module: A step-by-step guide

I am working with various modules such as SchoolModule, UniversityModule, and SharedModule The SharedModule includes a BaseService that both the SchoolModule and UniversityModule providers are utilizing as an extension When loading the SchoolModule, I ne ...

What should be the datatype of props in a TypeScript functional HOC?

My expertise lies in creating functional HOCs to seamlessly integrate queries into components, catering to both functional and class-based components. Here is the code snippet I recently developed: const LISTS_QUERY = gql` query List { list { ...

Steps for customizing the text representation of an object:

In my reactive form component, I have an input control attached to a formGroup. Let's consider this object: {Id: 1, Text: "some text here..."}. Just like a select or dropdown menu, I want to display the 'Text' part in the input field but sub ...

In Angular 2 Type Script service, make sure to include the @angular/core module for proper functionality as the 'require' method may not

I am encountering an issue with a service I am using. Whenever I try to start the page, I receive an error message. Here is the screenshot of the error: https://i.sstatic.net/WMzfU.png The compiled .js file contains the following code: reuired('@ang ...

Plugin for managing network connectivity in Ionic framework

In order to check if internet and id connection are available, I need to make a server request. I have implemented the Ionic Native Network Plugin following their official documentation. Here is my code snippet: import { Component } from '@angular/c ...

What is the best way to change a blob into a base64 format using Node.js with TypeScript?

When making an internal call to a MicroService in Node.js with TypeScript, I am receiving a blob image as the response. My goal is to convert this blob image into Base64 format so that I can use it to display it within an EJS image tag. I attempted to ach ...

Neglecting the error message for type assignment in the Typescript compiler

Presented here is a scenario I am facing: const customer = new Customer(); let customerViewModel = new CustomerLayoutViewModel(); customerViewModel = customer; Despite both Customer and CustomerLayoutViewModel being identical at the moment, there is no ...

The content security policy is preventing a connection to the signalr hub

Currently, I am developing an application using electron that incorporates React and Typescript. One of the features I am integrating is a SignalR hub for chat functionality. However, when attempting to connect to my SignalR server, I encounter the followi ...

Is the 'case' in a switch statement not treated as a typeguard by Typescript?

Here is a simplified version of the code I am working with: type Todo = { id: string; text: string; }; type Action = | { type: 'DELETE'; payload: string } | { type: 'CREATE'; payload: Todo } function reducer(state: Todo[], ...

methods for array filtering in typescript

How do I filter an array in TypeScript? I attempted the following findAllPersonsNotVisited():Observable<Person[]> { var rightNow = new Date(); var res = rightNow.toISOString().slice(0,10).replace(/-/g,"-"); return this.db.list(& ...

Navigating through a node tree and making changes to its configuration and content

Here's the input I have. Some nodes have downlines with multiple other nodes nested inside. data = [ { "user_id": "1", "username": "johndoe001", "amount": "0.00", "down ...

What is the best way to access a class's private static property in order to utilize it for testing purposes?

Hello, I am currently a beginner in TypeScript and unit testing, so this inquiry may seem elementary to those more experienced. I am attempting to perform unit testing on the code provided below using sinon. Specifically, I am interested in testing whethe ...

Guidelines for accessing a specific object or index from a dropdown list filled with objects stored in an array

Here is a question for beginners. Please be kind. I have created a select field in an HTML component using Angular, populated from an array of objects. My goal is to retrieve the selection using a method. However, I am facing an issue where I cannot use ...

Is there a surefire method to ensure that ChromeDriver in Protractor consistently uses the stable version?

Every time Chrome releases an update, I encounter a recurring issue. Allow me to paint the picture: All browsers are at version 83 of Chrome Chrome announces that version 84 is on its way, but it has not been released yet. A new ChromeDriver 84 is rolled ...

Implementing a custom type within a generic function

Struggling with a particular problem, I am trying to figure out whether it is possible to pass a custom type or if only native TypeScript types (such as string and number) can be passed into a generic type implementation for an interface: type coordinates ...

Error: Trying to access property '2' of a null value

I’ve been working on a project using Next.js with TypeScript, focusing on encryption and decryption. Specifically, I’m utilizing the 'crypto' module of Node.js (@types/nodejs). However, I encountered an error while attempting to employ the &a ...