Forms are essential to most web applications, whether for registration, data input, or surveys. While Angular’s Reactive Forms module is ideal for creating static forms, many applications require forms that can adapt dynamically based on user interactions or external data.
In this article, we'll dive into building dynamic forms using Angular 19's standalone components, offering a modular approach that eliminates the need for traditional Angular modules. While the accompanying GitHub repository includes Tailwind CSS for styling the forms, this article will focus solely on the dynamic form functionality. Tailwind styles and configurations are intentionally excluded from the examples to maintain a clear focus on the core topic.
What Are Dynamic Forms?
Dynamic forms allow you to define the structure of the form (fields, validators, layout, etc.) at runtime instead of at compile time. This is particularly useful in scenarios like:
- Multi-step forms with conditional steps.
- Forms generated from API responses.
- User-configurable forms that allow adding or removing fields dynamically.
Benefits of Using Standalone Components
Standalone components simplify Angular development by removing the dependency on NgModule
. You can declare component dependencies (e.g., Reactive Forms, routing) directly within the component, leading to:
- Reduced boilerplate.
- Better modularity.
- Faster development.
Angular’s FormArray
and FormGroup
make it easy to manage such forms, offering flexibility to add, remove, or modify controls dynamically.
Step-by-Step Guide to Building Dynamic Forms
1. Install Angular and Create a new Project
Before we dive into advanced examples, let’s start by creating a brand new application.
npm install @angular/cli
Now create a new Angular Application. For the stylesheet format you can choose SCSS
and for SSR
select No
.
ng new dynamic-forms-sample-app
2. Building the Dynamic Form
To create a form dynamically, we use FormGroup
and FormArray
from Angular's Reactive Forms. Here’s the full implementation:
Component Code
import { Component } from "@angular/core";
import {
FormBuilder,
FormGroup,
FormArray,
Validators,
ReactiveFormsModule,
} from "@angular/forms";
@Component({
selector: "app-root",
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent {
dynamicForm: FormGroup; // Main form group
constructor(private fb: FormBuilder) {
this.dynamicForm = this.fb.group({
name: [""], // Simple input field
email: [""], // Another input field
fields: this.fb.array([]), // Dynamic fields will be stored here
});
}
// Getter to access the FormArray for dynamic fields
get fields(): FormArray {
return this.dynamicForm.get("fields") as FormArray;
}
/**
* Adds a new field to the dynamic form.
*/
addField() {
const fieldGroup = this.fb.group({
label: [""], // Label for the field
value: [""], // Value of the field
});
this.fields.push(fieldGroup);
}
/**
* Removes a field from the dynamic form at a specific index.
* @param index Index of the field to be removed.
*/
removeField(index: number) {
this.fields.removeAt(index);
}
/**
* Submits the form and logs its current value to the console.
*/
submitForm() {
console.log(this.dynamicForm.value);
}
}
Explanation of Methods
addField()
This method creates a new form group with two controls:label
andvalue
. The new form group is added to thefields
array. This allows the user to add new fields to the form dynamically.removeField(index: number)
This method removes a form group from thefields
array at the specified index. It’s useful when a user wants to delete a field they no longer need.submitForm()
This method collects the current state of the form and logs it. In a real-world application, you would send this data to a server or use it to update the UI.
Template Code
Open the app.component.html
, remove everything and add the following template. The template dynamically renders form controls and provides buttons for adding and removing fields.
<form [formGroup]="dynamicForm" (ngSubmit)="submitForm()">
<div>
<label>Name:</label>
<input formControlName="name" />
</div>
<div>
<label>Email:</label>
<input formControlName="email" />
</div>
<div formArrayName="fields">
@for(field of fields.controls; let i = $index; track field) {
<div [formGroupName]="i">
<label>
Label:
<input formControlName="label" />
</label>
<label>
Value:
<input formControlName="value" />
</label>
<button type="button" (click)="removeField(i)">Remove</button>
</div>
}
</div>
<button type="button" (click)="addField()">Add Field</button>
<button type="submit">Submit</button>
</form>
Template Breakdown
Static Fields (
Name
andEmail
)
These fields are always present and useformControlName
for binding.Dynamic Fields (
fields
)
- The
@for
loops over theFormArray
to render each dynamic field. - Each field has
formControlName
bindings for itslabel
andvalue
.
-
Buttons
- The "Add Field" button calls
addField()
to add a new dynamic field. - Each dynamic field has a "Remove" button that calls
removeField()
.
- The "Add Field" button calls
Building a Dynamic Form from API Data
In many applications, the structure of a form comes from an external source, such as a configuration stored on a server.
Fetching Form Configurations
Let’s assume the following JSON is returned by an API:
{
"fields": [
{ "label": "Username", "type": "text", "required": true },
{ "label": "Age", "type": "number", "required": false },
{
"label": "Gender",
"type": "select",
"options": ["Male", "Female"],
"required": true
}
]
}
Rendering Dynamic Fields
Here’s how to dynamically generate the form based on this configuration:
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
@Component({
selector: "app-dynamic-api-form",
templateUrl: "./dynamic-api-form.component.html",
})
export class DynamicApiFormComponent implements OnInit {
dynamicForm!: FormGroup; // The main reactive form instance
formConfig: any; // The configuration object fetched from the API
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.fetchFormConfig().then((config) => {
this.formConfig = config;
this.buildForm(config.fields); // Build the form based on the configuration
});
}
/**
* Simulates fetching form configuration from an API.
* In a real application, this would be an HTTP request.
*/
async fetchFormConfig() {
// Simulate API call
return {
fields: [
{ label: "Username", type: "text", required: true },
{ label: "Age", type: "number", required: false },
{
label: "Gender",
type: "select",
options: ["Male", "Female"],
required: true,
},
],
};
}
/**
* Dynamically creates the form controls based on the fetched configuration.
*/
buildForm(fields: any[]) {
const controls: any = {};
fields.forEach((field) => {
const validators = field.required ? [Validators.required] : [];
controls[field.label] = ["", validators];
});
this.dynamicForm = this.fb.group(controls);
}
/**
* Handles form submission, logging the form value to the console.
*/
submitForm() {
console.log(this.dynamicForm.value);
}
}
Code Breakdown
Properties
dynamicForm
- Holds the main
FormGroup
instance for the reactive form. - It is dynamically built based on the API response.
-
formConfig
- Stores the configuration object fetched from the API.
- Defines the fields, their types, validation rules, and options (if applicable).
Methods
-
ngOnInit()
- Lifecycle hook that runs after the component initializes.
- Calls
fetchFormConfig()
to fetch form configuration and set up the form.
fetchFormConfig()
- Simulates an API call to retrieve form configuration. In a production setting, replace this mock with an actual HTTP request to fetch the configuration.
Example Configuration:
{
fields: [
{ label: "Name", type: "text", required: true },
{ label: "Age", type: "number", required: true },
{
label: "Gender",
type: "select",
required: true,
options: ["Male", "Female", "Other"],
},
];
}
buildForm()
- Dynamically constructs the
FormGroup
using the fetched configuration. - For each field in the configuration:
- Adds a
FormControl
to theFormGroup
. - Assigns validators (like
Validators.required
) if the field is marked as required.
- Adds a
Example Output (FormGroup Structure):
{
Name: ['', [Validators.required]],
Age: ['', [Validators.required]],
Gender: ['', [Validators.required]]
}
submitForm()
- Triggered when the user submits the form.
- If the form is valid:
- Logs the form values to the console.
- If invalid:
- Logs an error message.
Example Output (Form Value):
{
Name: 'John Doe',
Age: 30,
Gender: 'Male'
}
How It Works Together
- Initialization
- When the component is loaded,
ngOnInit()
triggersfetchFormConfig()
to simulate fetching a form structure.
- Form Construction
-
buildForm()
uses the fetched configuration to create a reactive form dynamically.
- User Interaction
- The form is displayed using the associated HTML.
- Users can input values or select options based on the field types.
- Validation
- The form controls enforce validation rules (e.g., required fields).
- Invalid fields show error messages when touched.
-
Submission
- On submit,
submitForm()
checks the validity and handles the form values appropriately.
- On submit,
This breakdown ensures clarity on the purpose and functionality of each method and property in the TypeScript code.
Template:
@if(dynamicForm) {
<form [formGroup]="dynamicForm" (ngSubmit)="submitForm()">
@for(field of formConfig.fields; track field) {
<label>{{ field.label }}</label>
@switch(field.type) {
@case('text') {
<input *ngSwitchCase="'text'" [formControlName]="field.label" />
}
@case('number') {
<input
*ngSwitchCase="'number'"
[formControlName]="field.label"
type="number"
/>
}
@case('select') {
<select *ngSwitchCase="'select'" [formControlName]="field.label">
<option *ngFor="let option of field.options" [value]="option">
{{ option }}
</option>
</select>
}
}
}
<button type="submit">Submit</button>
</form>
}
HTML Structure Breakdown
@if(dynamicForm)
Ensures the form renders only after it has been initialized with the fetched configuration.@for(field of formConfig.fields; track field)
Iterates over thefields
array from the configuration to dynamically render form controls.@switch(field.type)
Dynamically selects the type of form control to render based on thetype
property in the field configuration (e.g.,text
,number
, orselect
).Input Field Types
-
Text Fields (
@case('text')
) Renders an<input>
element for text fields. -
Number Fields (
@case('number')
) Renders an<input>
element withtype="number"
. -
Select Fields (
@case('select')
) Renders a<select>
element, dynamically populating its options from theoptions
array in the field configuration.
- Validation Feedback
-
@if(dynamicForm.get(field.label)?.invalid && dynamicForm.get(field.label)?.touched && dynamicForm.get(field.label)?.hasError('required'))
Displays validation error messages only if the field is invalid and has been interacted with.
-
Submit Button
-
[disabled]="dynamicForm.invalid"
Disables the submit button until all required fields are valid.
-
This template ensures the dynamic form is fully responsive to the fetched configuration while providing real-time validation feedback for required fields.
Dynamic Validators
You can also modify validators dynamically based on user input or conditions.
Example:
onRoleChange(role: string) {
const emailControl = this.dynamicForm.get('email');
if (role === 'admin') {
emailControl?.setValidators([Validators.required, Validators.email]);
} else {
emailControl?.clearValidators();
}
emailControl?.updateValueAndValidity();
}
Conclusion
Dynamic forms in Angular offer a flexible way to build highly interactive and scalable user interfaces. By leveraging FormArray
, FormGroup
, and API-driven configurations, you can create forms that adapt to user needs while maintaining robustness and performance. You can download the repo (with Tailwind) from this repo: https://github.com/sonukapoor/dynamic-forms-sample-app
Use these techniques to build smarter forms that empower your users and simplify your codebase. Happy coding!
Top comments (1)
Good and simple explanation
I would like to see a sample on nested drop down that would be a perfect one to see how the events of selecting elements and updating the controls can be handled in the dynamic forms