Dynamic Forms built by a form generator have quite a history in Angular. Such form generators enable us to build a form at runtime using metadata, such as field names and validation rules. A typical use cases are situations where the user defines types or extensions to existing types.
With Reactive Forms, building such form generators was quite easy. In this article, I show how we can achieve the same with Angular's new Signal Forms (experimental).
📂 Source Code (🔀 branch dynamic-forms
)
Consumer's Perspective
Before we look at the implementation of the forms generator, let's discuss the consumer's perspective: They just provide an array with FieldDef
objects that provide metadata describing the form:
@Component([...])
export class FlightEditComponent {
[...]
meta: FieldDef[] = [
{ name: 'id', label: 'Id', required: true },
{
name: 'from',
label: 'From',
required: true,
minLength: 3,
maxLength: 20,
},
{ name: 'to', label: 'To', required: true, minLength: 3, maxLength: 20 },
{ name: 'date', label: 'Date', required: true, type: 'datetime-local' },
{ name: 'delayed', label: 'Delayed', type: 'checkbox' },
];
// Let's assume we don't know the structure of the entity
// upfront but we have fitting meta data
entity = [...] as WritableSignal<unknown>;
dynamicForm = form(this.entity, toSchema(this.meta));
[...]
}
A function toSchema
converts this metadata into a schema used by Signal Forms. The form function takes the entity to display and the schema. The entity can be represented by a Signal<unknown>
as we don't know about the shape of user-defined objects at compile-time.
To render the form, the template delegates to a component app-dynamic-form
:
<form [...]>
<app-dynamic-form [metaInfo]="meta" [dynamicForm]="dynamicForm" />
[...]
</form>
This results in the following form at runtime:
Implementation
The implementation shown here is minimal to make it easier to convey the idea. Basically, we need a type FieldDef, a function toSchema, and a component for rendering the form. The type and the function are shown in the following listening:
import { maxLength, minLength, required, Schema, schema } from "@angular/forms/signals";
export interface FieldDef {
name: string;
label: string;
required?: boolean;
minLength?: number;
maxLength?: number;
type?: string;
}
export function toSchema(meta: FieldDef[]): Schema<unknown> {
return schema<unknown>((path) => {
for (const fieldDef of meta) {
const prop = fieldDef.name;
const fieldPath = (path as any)[prop];
if (!fieldPath) {
continue;
}
if (fieldDef.required) {
required(fieldPath);
}
if (typeof fieldDef.minLength !== 'undefined') {
minLength(fieldPath, fieldDef.minLength);
}
if (typeof fieldDef.maxLength !== 'undefined') {
maxLength(fieldPath, fieldDef.maxLength);
}
}
});
}
This function creates a schema<unknown>
by iterating the meta data and describing the respective field by seting up validators. As the field names are not known at compile-time, we need to access their properties via index notation (brackets).
The app-dynamic-form
component gets the metadata and the entity passed. It iterates the metadata and renders respective fields:
@for(fieldDef of metaInfo(); track fieldDef.name) {
@let field = $any(dynamicForm())[fieldDef.name];
<div class="form-group">
<label>
{{ fieldDef.label }}
<input
[type]="fieldDef.type"
[control]="field"
[class.form-control]="fieldDef.type !== 'checkbox'"
/>
</label>
<app-validation-errors
[errors]="field().errors()"
></app-validation-errors>
</div>
}
More: Modern Angular Workshop
Subscribe to our Modern Angular Workshop to stay up to date!
English Version | German Version
Conclusion
This example demonstrates how Signal Forms can be used to build a simple dynamic form generator. By leveraging metadata such as field names and validation rules, a form can be created at runtime without knowing the shape of the underlying object at compile time.
The implementation is intentionally kept minimal to clearly illustrate the core idea:
- Metadata is converted into a schema,
- the schema is bound to a signal-based form,
- and a generic component handles the rendering of the fields.
In real-world scenarios, this approach can easily be extended to support:
- Arrays and form groups,
- Additional validators,
- or more complex UI components.
Signal Forms thus provide a flexible and powerful foundation for building dynamic forms.