A deep dive into Angular’s FormArray container

Written by Kayode Adeniyi✏️ Handling dynamic, structured data is a common challenge in modern web applications. Whether building spreadsheets, surveys, or data grids, developers need forms that can adapt to user input. Angular’s FormArray is a powerful container tool designed for this purpose. FormArray makes it easy to create and manage dynamic rows and columns of input fields, providing a seamless way to build spreadsheet-like interfaces. In this guide, you’ll learn how to: Upload and parse CSV data into a structured format Dynamically generate form controls using Angular’s FormArray container Add validation rules to ensure data accuracy Enable users to download modified data as a CSV file By the end of this guide, you’ll have a functional pseudo-spreadsheet application and a strong understanding of how Angular’s reactive forms simplify complex, dynamic data handling. Let’s get started! Setting up the Angular project To get started, ensure you have Node.js and the Angular CLI installed. To create a new Angular project, run the following command: ng new dynamic-formarray-app During setup, enable routing (by running Yes) and choose your preferred CSS preprocessor. Once the project is created, navigate to the project folder and install the necessary dependencies, including Bootstrap for styling: npm install bootstrap Add Bootstrap to angular.json under the styles array: "styles": [ "node_modules/bootstrap/dist/css/bootstrap.min.css", "src/styles.css" ] Add PapaParse for robust CSV parsing: npm install papaparse Finally, generate a new component for the spreadsheet interface: ng generate component components/spreadsheet The Angular project is now set up and ready for development. Uploading and parsing CSV data To dynamically generate form controls, we first need to upload and parse a CSV file. Add a file input element to your template: Upload CSV File: In your component file (spreadsheet.component.ts), use Angular’s FormBuilder and PapaParse to process the uploaded file: import { Component, OnInit } from '@angular/core'; import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import * as Papa from 'papaparse'; @Component({ selector: 'app-spreadsheet', templateUrl: './spreadsheet.component.html', styleUrls: ['./spreadsheet.component.css'] }) export class SpreadsheetComponent implements OnInit { spreadsheetForm!: FormGroup; constructor(private fb: FormBuilder) {} ngOnInit(): void { this.spreadsheetForm = this.fb.group({ rows: this.fb.array([]) }); } get formArray(): FormArray { return this.spreadsheetForm.get('rows') as FormArray; } onFileUpload(event: Event): void { const file = (event.target as HTMLInputElement).files?.[0]; if (file) { Papa.parse(file, { complete: (result) => this.loadCsvData(result.data), skipEmptyLines: true }); } } loadCsvData(data: any[]): void { const rows = this.formArray; rows.clear(); data.forEach((row) => { const formRow = this.fb.array(row.map((value: string) => this.fb.control(value, Validators.required))); rows.push(formRow); }); } } The code snippet above achieves the following: File upload: The element captures the file and triggers the onFileUpload method Parsing CSV data: PapaParse converts the CSV file into a structured, two-dimensional array Mapping to FormArray: Each row in the CSV becomes a FormArray of FormControls, allowing Angular to manage the data reactively Rendering CSV data dynamically After parsing the data, the next step is rendering it dynamically in a grid that mimics a spreadsheet. Each row in the FormArray corresponds to a FormArray of cells, represented as FormControl instances. In the template (spreadsheet.component.html), use Angular’s structural directives to display rows and cells: This field is required. Here’s what’s happening in the code block above: Dynamic rows: *ngFor loops over the FormArray rows, creating a for each row Dynamic cells: Inside each row, another *ngFor loops through the cells, rendering an for each FormControl Validation feedback: Error messages appear dynamically if a cell is invalid and has been touched Adding validation rules Validation ensures that the input meets specific criteria. Angular supports built-in validators like Validators.required and allows for custom validation logic. Custom numeric validator Create a custom validator to ensure numeric input: function validateNumeric(): ValidatorFn { return (control: AbstractControl): { [key: string]: any } | null => { const value = control.value; return isNaN(value) || value.trim() === '' ? { numeric: true } : null; }; } Update the loadCsvData method to include this validator: loadCsvD

Jan 16, 2025 - 16:22
A deep dive into Angular’s FormArray container

Written by Kayode Adeniyi✏️

Handling dynamic, structured data is a common challenge in modern web applications. Whether building spreadsheets, surveys, or data grids, developers need forms that can adapt to user input. Angular’s FormArray is a powerful container tool designed for this purpose.

FormArray makes it easy to create and manage dynamic rows and columns of input fields, providing a seamless way to build spreadsheet-like interfaces. In this guide, you’ll learn how to:

  • Upload and parse CSV data into a structured format
  • Dynamically generate form controls using Angular’s FormArray container
  • Add validation rules to ensure data accuracy
  • Enable users to download modified data as a CSV file

By the end of this guide, you’ll have a functional pseudo-spreadsheet application and a strong understanding of how Angular’s reactive forms simplify complex, dynamic data handling.

Let’s get started!

Setting up the Angular project

To get started, ensure you have Node.js and the Angular CLI installed. To create a new Angular project, run the following command:

ng new dynamic-formarray-app

During setup, enable routing (by running Yes) and choose your preferred CSS preprocessor. Once the project is created, navigate to the project folder and install the necessary dependencies, including Bootstrap for styling:

npm install bootstrap

Add Bootstrap to angular.json under the styles array:

 "styles": [
  "node_modules/bootstrap/dist/css/bootstrap.min.css",
  "src/styles.css"
]

Add PapaParse for robust CSV parsing:

 npm install papaparse

Finally, generate a new component for the spreadsheet interface:

ng generate component components/spreadsheet

The Angular project is now set up and ready for development.

Uploading and parsing CSV data

To dynamically generate form controls, we first need to upload and parse a CSV file. Add a file input element to your template:

<div class="mb-3">
  <label for="csvFile" class="form-label">Upload CSV File:</label>
  <input
    type="file"
    id="csvFile"
    class="form-control"
    accept=".csv"
    (change)="onFileUpload($event)"
  />
</div>

In your component file (spreadsheet.component.ts), use Angular’s FormBuilder and PapaParse to process the uploaded file:

import { Component, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import * as Papa from 'papaparse';

@Component({
  selector: 'app-spreadsheet',
  templateUrl: './spreadsheet.component.html',
  styleUrls: ['./spreadsheet.component.css']
})
export class SpreadsheetComponent implements OnInit {
  spreadsheetForm!: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {
    this.spreadsheetForm = this.fb.group({ rows: this.fb.array([]) });
  }

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

  onFileUpload(event: Event): void {
    const file = (event.target as HTMLInputElement).files?.[0];
    if (file) {
      Papa.parse(file, {
        complete: (result) => this.loadCsvData(result.data),
        skipEmptyLines: true
      });
    }
  }

  loadCsvData(data: any[]): void {
    const rows = this.formArray;
    rows.clear();
    data.forEach((row) => {
      const formRow = this.fb.array(row.map((value: string) => this.fb.control(value, Validators.required)));
      rows.push(formRow);
    });
  }
}

The code snippet above achieves the following:

  • File upload: The element captures the file and triggers the onFileUpload method
  • Parsing CSV data: PapaParse converts the CSV file into a structured, two-dimensional array
  • Mapping to FormArray: Each row in the CSV becomes a FormArray of FormControls, allowing Angular to manage the data reactively

Rendering CSV data dynamically

After parsing the data, the next step is rendering it dynamically in a grid that mimics a spreadsheet. Each row in the FormArray corresponds to a FormArray of cells, represented as FormControl instances.

In the template (spreadsheet.component.html), use Angular’s structural directives to display rows and cells:

<form [formGroup]="spreadsheetForm">
  <div *ngFor="let row of formArray.controls; let i = index" class="row mb-2">
    <div *ngFor="let cell of (row as FormArray).controls; let j = index" class="col">
      <input
        type="text"
        [formControl]="cell"
        class="form-control"
        [ngClass]="{ 'is-invalid': cell.invalid && cell.touched }"
        placeholder="Cell {{ i + 1 }}, {{ j + 1 }}"
      />
      <div *ngIf="cell.invalid && cell.touched" class="invalid-feedback">
        <span *ngIf="cell.hasError('required')">This field is required.</span>
      </div>
    </div>
  </div>
</form>

Here’s what’s happening in the code block above:

  • Dynamic rows: *ngFor loops over the FormArray rows, creating a
    for each row
  • Dynamic cells: Inside each row, another *ngFor loops through the cells, rendering an for each FormControl
  • Validation feedback: Error messages appear dynamically if a cell is invalid and has been touched

Adding validation rules

Validation ensures that the input meets specific criteria. Angular supports built-in validators like Validators.required and allows for custom validation logic.

Custom numeric validator

Create a custom validator to ensure numeric input:

function validateNumeric(): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } | null => {
    const value = control.value;
    return isNaN(value) || value.trim() === '' ? { numeric: true } : null;
  };
}

Update the loadCsvData method to include this validator:

loadCsvData(data: any[]): void {
  const rows = this.formArray;
  rows.clear();
  data.forEach((row) => {
    const formRow = this.fb.array(
      row.map((value: string) => this.fb.control(value, [Validators.required, validateNumeric()]))
    );
    rows.push(formRow);
  });
}

Exporting modified data

Once the user modifies the form, allow them to download the updated data as a CSV file using the Blob API.

Here is the code for the CSV export:

downloadCsv(): void {
  const headers = ['Column 1', 'Column 2', 'Column 3'];
  const rows = this.formArray.controls.map((row) =>
    (row as FormArray).controls.map((control) => control.value)
  );

  const csvArray = [headers, ...rows];
  const csvData = csvArray.map((row) => row.join(',')).join('\n');

  const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
  const url = window.URL.createObjectURL(blob);

  const link = document.createElement('a');
  link.href = url;
  link.setAttribute('download', 'modified-data.csv');
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  window.URL.revokeObjectURL(url);
}

Adding the download button

Finally, we’ll include a button in the template to trigger the download:

<button type="button" class="btn btn-secondary" (click)="downloadCsv()">
  Download Modified CSV
</button>

And that’s it! We’ve successfully built a fully functional pseudo-spreadsheet application capable of dynamically generating form controls, validating user inputs, and exporting modified data — all powered by Angular’s FormArray.

Conclusion

By following this guide, you learned how to:

  • Parse CSV files and bind the data dynamically to Angular’s FormArray
  • Validate user input using both built-in and custom validators
  • Export modified data back into a CSV format

This solution is highly adaptable, making it suitable for various real-world scenarios like data grids, surveys, or interactive spreadsheets.

By mastering Angular’s FormArray, you can build flexible, dynamic form applications that meet real-world needs, such as data grids, spreadsheets, and surveys. Now you have the tools to simplify complex form handling with Angular. Happy coding!

Experience your Angular apps exactly how a user does

Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket.

Angular LogRocket Demo

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Angular apps — start monitoring for free.