Web

Angular editable table with dynamic rows and columns

Build an Angular editable table using Reactive Forms (FormArray). Model dynamic tenants and bill types and recompute row and column totals instantly on input.

1 answer 1 view

How can I calculate and update the sum of each column and each row in an editable Angular table with dynamic rows and columns (bill types and tenants loaded from an API) so totals update immediately when any cell input changes?

Context:

  • The table is an editable grid (like Excel) for monthly bills. Column headers (billTypes) and row headers (tenants) are retrieved from an API and may vary.
  • Each cell contains an for the bill amount (one input per tenant per bill type).
  • The example HTML template I’m using:
html
<table>
  <tr>
    <th></th>
    <th *ngFor="let billType of this.data.billTypes">{{ billType }}</th>
  </tr>
  <tr *ngFor="let tenant of this.data.tenants">
    <td>{{ tenant }}</td>
    <td *ngFor="let billType of this.billTypes">
      <input id="{{tenant + billType}}" value="0" type="number">
    </td>
  </tr>
  <tr>
    <td>Total</td>
    <td id="{{billType + 'total'}}" *ngFor="let billType of this.data.billTypes">0</td>
  </tr>
</table>

Questions:

  1. What is the recommended Angular data structure and binding approach to represent this grid so I can compute row and column sums efficiently? (e.g., FormArray/FormGroup, a 2D array or object map, ngModel)
  2. How can I update totals reactively whenever any input changes? (e.g., (input) handlers, valueChanges on reactive forms, RxJS)
  3. Can you provide a concise example or algorithm to compute column sums and row sums for arbitrary numbers of tenants and bill types?

Building an Angular editable table with dynamic rows and columns that updates row and column totals reactively is best achieved using Angular Reactive Forms with FormArray and FormGroup. This approach provides a robust data model where each cell is a bound FormControl, and changes trigger automatic recomputation of totals through valueChanges subscriptions. Below is the definitive implementation guide.

Contents

Why Template-Driven Forms Fall Short

Template-driven forms lack programmatic control for dynamic structures like bill grids. Using [(ngModel)] in your table inputs creates several challenges:

  • No built-in way to track all controls in a dynamic grid
  • Manual change detection required for each input
  • Difficult to implement reactive recalculations at scale
  • Poor performance with large datasets due to excessive change detection cycles

The Angular official documentation confirms that Reactive Forms provide superior control for dynamic forms through its FormControl, FormGroup, and FormArray primitives.

For dynamic bill tables, use this structure:

  • Root FormGroup: Contains the entire form
  • FormArray Tenants: Holds each tenant row
  • Nested FormGroup: Per tenant (contains name + bills FormArray)
  • FormArray Bills: Per tenant (holds bill amount FormControls)

This model mirrors your grid structure and enables automatic value tracking. Each cell becomes a FormControl bound to a specific FormArray index, allowing programmatic access for sum calculations.

Building the Reactive Form Model

Initialize the form after loading your data:

typescript
import { FormGroup, FormBuilder, FormArray, FormControl } from '@angular/forms';

constructor(private fb: FormBuilder) {}

loadData() {
  this.api.getTenantsAndBills().subscribe(data => {
    this.billTypes = data.billTypes;
    this.form = this.fb.group({
      tenants: this.fb.array([])
    });

    // Create form groups for each tenant
    data.tenants.forEach(tenant => {
      const tenantGroup = this.fb.group({
        name: tenant,
        bills: this.fb.array(data.billTypes.map(() => new FormControl(0)))
      });
      this.tenants.push(tenantGroup);
    });

    this.calculateTotals(); // Initial calculation
  });
}

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

Template Binding Directives

Bind your template to the reactive form structure:

html
<table>
  <tr>
    <th></th>
    <th *ngFor="let billType of billTypes">{{ billType }}</th>
  </tr>
  
  <ng-container formArrayName="tenants">
    <tr *ngFor="let tenant of tenants.controls; let i = index" [formGroupName]="i">
      <td>{{ tenant.value.name }}</td>
      <ng-container formArrayName="bills">
        <td *ngFor="let billControl of tenant.controls.bills.controls; let j = index">
          <input 
            type="number" 
            [formControlName]="j"
            (input)="calculateTotals()"
          >
        </td>
      </ng-container>
    </tr>
  </ng-container>
  
  <tr>
    <td>Total</td>
    <td *ngFor="let total of columnTotals">{{ total }}</td>
  </tr>
</table>

Use formArrayName, formGroupName, and formControlName for two-way binding. This Angular pattern ensures all changes are tracked automatically.

Reactive Totals with valueChanges

Subscribe to form changes to recalculate totals reactively:

typescript
columnTotals: number[] = [];
rowTotals: number[] = [];

ngOnInit() {
  this.form.valueChanges.pipe(
    debounceTime(100), // Optional: For performance
    distinctUntilChanged()
  ).subscribe(() => {
    this.calculateTotals();
  });
}

calculateTotals() {
  const rawValues = this.form.value.tenants;
  
  // Calculate row sums
  this.rowTotals = rawValues.map(tenant => 
    tenant.bills.reduce((sum, val) => sum + Number(val), 0)
  );
  
  // Calculate column sums
  this.columnTotals = this.billTypes.map((_, colIndex) => 
    rawValues.reduce((sum, tenant) => 
      sum + Number(tenant.bills[colIndex]), 0
    )
  );
}

The Angular University FormArray guide recommends this pattern for handling dynamic grids, as valueChanges emits whenever any nested control changes.

Sum Computation Algorithm

For arbitrary tenant/bill counts, use this efficient approach:

  1. Row Totals: Map tenants to sum of their bills array
  2. Column Totals: For each bill index, sum across all tenants’ bill values at that index
typescript
// Row totals calculation
rowTotals = this.form.value.tenants.map(tenant => 
  tenant.bills.reduce((acc, val) => acc + +val, 0)
);

// Column totals calculation
columnTotals = this.form.value.tenants.reduce((acc, tenant) => {
  tenant.bills.forEach((bill, index) => {
    acc[index] = (acc[index] || 0) + +bill;
  });
  return acc;
}, []);

This algorithm handles any grid size with O(n*m) complexity, where n is tenants and m is bill types.

Performance Optimization

For large datasets:

  • Add debounceTime(100) to valueChanges pipe
  • Use distinctUntilChanged() to prevent unnecessary calculations
  • Consider OnPush change detection in your component
  • Calculate only changed rows/columns instead of entire grid

Complete Example Implementation

Here’s a minimal working component:

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

@Component({...})
export class BillTableComponent implements OnInit {
  form: FormGroup;
  billTypes: string[] = [];
  columnTotals: number[] = [];

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.loadData();
  }

  loadData() {
    // Simulate API call
    this.billTypes = ['Electricity', 'Water', 'Gas'];
    this.form = this.fb.group({
      tenants: this.fb.array([
        this.createTenantRow('Alice'),
        this.createTenantRow('Bob')
      ])
    });
    this.calculateTotals();
  }

  createTenantRow(name: string) {
    return this.fb.group({
      name: name,
      bills: this.fb.array(this.billTypes.map(() => new FormControl(0)))
    });
  }

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

  calculateTotals() {
    const rawValues = this.form.value.tenants;
    this.columnTotals = this.billTypes.map((_, colIndex) => 
      rawValues.reduce((sum, tenant) => 
        sum + Number(tenant.bills[colIndex]), 0
      )
    );
  }
}

Conclusion

Implementing an Angular editable table with dynamic totals requires Angular Reactive Forms with nested FormArray structures. This approach provides automatic value tracking, reactive calculations through valueChanges, and efficient sum updates. The recommended pattern uses FormGroup for tenants, FormArray for bills, and simple reduce operations for totals. For optimal performance, consider debouncing calculations and using OnPush change detection in large tables.

Sources

Authors
Verified by moderation
Moderation
Angular editable table with dynamic rows and columns