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.
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:
<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:
- 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)
- How can I update totals reactively whenever any input changes? (e.g., (input) handlers, valueChanges on reactive forms, RxJS)
- 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
- Recommended Data Model: FormArray + FormGroup
- Building the Reactive Form Model
- Template Binding Directives
- Reactive Totals with valueChanges
- Sum Computation Algorithm
- Performance Optimization
- Complete Example Implementation
- Conclusion
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.
Recommended Data Model: FormArray + FormGroup
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:
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:
<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:
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:
- Row Totals: Map tenants to sum of their bills array
- Column Totals: For each bill index, sum across all tenants’ bill values at that index
// 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:
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.