Dynamic Forms in Angular 19: Creating Flexible and Scalable User Interfaces with Standalone Components (original) (raw)

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:

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:

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)
  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
  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()

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()

Example Output (FormGroup Structure):

   {
     Name: ['', [Validators.required]],
     Age: ['', [Validators.required]],
     Gender: ['', [Validators.required]]
   }

Enter fullscreen mode Exit fullscreen mode


  1. submitForm()

Example Output (Form Value):

   {
     Name: 'John Doe',
     Age: 30,
     Gender: 'Male'
   }

Enter fullscreen mode Exit fullscreen mode


How It Works Together

  1. Initialization
  1. Form Construction
  1. User Interaction
  1. Validation
  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 [formControlName]="field.label" />
        } 
        @case('number') {
          <input
            [formControlName]="field.label"
            type="number"
          />
        } 
        @case('select') {
          <select [formControlName]="field.label">
            @for(option of field.options; track option) {
              <option [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
  1. Validation Feedback
  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!


👋 Let's Connect!

If you found this article useful, let's connect:

🔗 Follow me on LinkedIn
💻 Check out my GitHub
Buy me a coffee