DEV Community

Sonu Kapoor for This is Angular

Posted on

Dynamic Forms in Angular 19: Creating Flexible and Scalable User Interfaces with Standalone Components

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.

Image description


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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation of Methods

  1. addField()

    This method creates a new form group with two controls: label and value. The new form group is added to the fields array. This allows the user to add new fields to the form dynamically.

  2. removeField(index: number)

    This method removes a form group from the fields array at the specified index. It’s useful when a user wants to delete a field they no longer need.

  3. 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>
Enter fullscreen mode Exit fullscreen mode

Template Breakdown

  1. Static Fields (Name and Email)

    These fields are always present and use formControlName for binding.

  2. Dynamic Fields (fields)

  • The @for loops over the FormArray to render each dynamic field.
  • Each field has formControlName bindings for its label and value.
  1. Buttons
    • The "Add Field" button calls addField() to add a new dynamic field.
    • Each dynamic field has a "Remove" button that calls removeField().

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
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Code Breakdown

Properties

  1. dynamicForm
  • Holds the main FormGroup instance for the reactive form.
  • It is dynamically built based on the API response.
  1. formConfig
    • Stores the configuration object fetched from the API.
    • Defines the fields, their types, validation rules, and options (if applicable).

Methods

  1. ngOnInit()
    • Lifecycle hook that runs after the component initializes.
    • Calls fetchFormConfig() to fetch form configuration and set up the form.

  1. 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"],
       },
     ];
   }
Enter fullscreen mode Exit fullscreen mode

  1. buildForm()
  • Dynamically constructs the FormGroup using the fetched configuration.
  • For each field in the configuration:
    • Adds a FormControl to the FormGroup.
    • Assigns validators (like Validators.required) if the field is marked as required.

Example Output (FormGroup Structure):

   {
     Name: ['', [Validators.required]],
     Age: ['', [Validators.required]],
     Gender: ['', [Validators.required]]
   }
Enter fullscreen mode Exit fullscreen mode

  1. 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'
   }
Enter fullscreen mode Exit fullscreen mode

How It Works Together

  1. Initialization
  • When the component is loaded, ngOnInit() triggers fetchFormConfig() to simulate fetching a form structure.
  1. Form Construction
  • buildForm() uses the fetched configuration to create a reactive form dynamically.
  1. User Interaction
  • The form is displayed using the associated HTML.
  • Users can input values or select options based on the field types.
  1. Validation
  • The form controls enforce validation rules (e.g., required fields).
  • Invalid fields show error messages when touched.
  1. Submission
    • On submit, submitForm() checks the validity and handles the form values appropriately.

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>
}
Enter fullscreen mode Exit fullscreen mode

HTML Structure Breakdown

  1. @if(dynamicForm)

    Ensures the form renders only after it has been initialized with the fetched configuration.

  2. @for(field of formConfig.fields; track field)

    Iterates over the fields array from the configuration to dynamically render form controls.

  3. @switch(field.type)

    Dynamically selects the type of form control to render based on the type property in the field configuration (e.g., text, number, or select).

  4. Input Field Types

  • Text Fields (@case('text')) Renders an <input> element for text fields.
  • Number Fields (@case('number')) Renders an <input> element with type="number".
  • Select Fields (@case('select')) Renders a <select> element, dynamically populating its options from the options array in the field configuration.
  1. 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.
  1. 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();
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
mahmoudalaskalany profile image
Mahmoud Alaskalany

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