Angular Dynamic Forms
A FormArray models a changing collection of controls. Track each control’s identity when rendering it so adding or removing a line does not attach one control’s state to another row.
How it works
The desk can add a second requested tool and remove the last line while retaining at least one control. Each input has a distinct label connection. A real collection editor needs a deliberate policy for maximum rows, duplicates and how row removal affects errors or focus. Keeping those rules in component operations is easier to test than mutating the controls array directly.
Run the code
Use a separate empty project folder with a compatible Node version: ^22.22.3, ^24.15.0 or a supported 26+ runtime. The review uses Node 24.22.1, Angular 22.2.4, CLI/build 22.2.5 and TypeScript 6.0.3. Node 22.15.1 is too old for Angular 22. Save every download at its displayed path; browsers may keep only the basename, so recreate the src folder. Run npm install, npm run build and npm start, then open the loopback address printed by the server. Run npm test for the supplied component tests. These are local teaching projects, not Bhylu publishing files.
The small loopback server serves the compiled app and fixed, read-only tool API fixtures. It is not a database, authentication or lending service. Angular examples do not run in Bhylu’s current HTML or TypeScript playground. No Apache, MySQL or PHP configuration change is needed.
Example
import { Component } from '@angular/core';
import { FormArray, FormControl, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'bh-dynamic-forms',
standalone: true,
imports: [ReactiveFormsModule],
template: `<button type="button" (click)="add()">Add tool line</button
><button type="button" (click)="remove()">Remove last line</button>
@for (control of tools.controls; track control; let index = $index) {
<label [for]="'tool-' + index">Tool code</label
><input [id]="'tool-' + index" [formControl]="control" />
}
<p data-result>{{ tools.length }} {{ tools.length === 1 ? 'line' : 'lines' }}</p>`,
})
export class App {
tools = new FormArray([new FormControl('DR-4', { nonNullable: true })]);
add(): void {
if (this.tools.length < 5)
this.tools.push(new FormControl('CL-2', { nonNullable: true }));
}
remove(): void {
if (this.tools.length > 1) this.tools.removeAt(this.tools.length - 1);
}
}
Expected result
1 line
Adding a line produces two controls; removing returns to one, and another removal keeps one. The tests check the rendered input count as well as the summary. The upper bound prevents this teaching fixture from accumulating unbounded controls.