Angular Dynamic Forms using JSON

Angular's Reactive Forms module is a powerful tool. It allows for the creation of dynamic, data-driven forms, enhancing both flexibility and control over UI interactions.

One significant advantage of dynamic reactive forms is adaptability - they can evolve in real-time in response to changes in the data model, elevating the user experience by allowing instant feedback and easy form manipulation.

In the context of Angular dynamic forms, JSON can be used to define the form structure dynamically. By specifying form elements, their types, validation rules, and even the order of appearance within a JSON object, we can create a form structure that is easy to understand, modify, and maintain.

Imagine the possibility of building forms based on metadata that can be altered at runtime or fetched from a server. This would mean a lower maintenance cost, better adaptability, and more efficient data handling for your web applications.

Note: the whole code from this tutorial is on GitHub

Dynamic Angular forms guide

Defining the Form Structure with JSON

In dynamic forms, the form structure is not defined statically in the HTML template, but rather it's defined programmatically with JavaScript. However, JavaScript objects, while flexible, can be somewhat verbose and cumbersome to work with for defining structured data, like a form layout.

This is where JSON comes in. JSON is a lightweight, text-based data interchange format that's easy to read and write.

It's an ideal choice for defining structured data like our form layout. By using JSON, we can define the form's structure in a readable and maintainable format. Each form control can be defined as a JSON object with properties like type, label, value, validations, and more.

Here is a simple example of a JSON object that defines a form with two fields: 'name' and 'email':

[
   {
     "type": "text",
     "label": "Name",
     "name": "name",
     "value": "",
     "validations": [
       {
         "name": "required",
         "validator": "required",
         "message": "Name is required"
       }
     ]
   },
   {
     "type": "email",
     "label": "Email",
     "name": "email",
     "value": "",
     "validations": [
       {
         "name": "required",
         "validator": "required",
         "message": "Email is required"
       },
       {
         "name": "pattern",
         "validator": "email",
         "message": "Invalid email format"
       }
     ]
   }
 ]

In this JSON object, each form control is defined as an object with properties such as 'type', 'label', 'name', 'value', and 'validations'. The 'validations' property is an array of validation rules for the form control.

Different Form Control Types and Their Properties in JSON

  • type: This defines the type of the form control. It can be 'text', 'email', 'password', 'number', 'date', etc., which corresponds to the types of HTML input elements.
  • label: The label property defines the text that will be used as the label for the form control.
  • name: This defines the unique name for the form control that is used to retrieve its value.
  • value: The initial value for the form control. It can be any valid value for the given type of form control.
  • validations: This is an array of validation rules for the form control. Each rule is an object with properties:

    • name: A unique name for the validation rule.
    • validator: The validator function to be applied, like 'required', 'email', 'minlength', 'maxlength', etc.
    • message: The error message to display if the validation fails.

Remember, the types of form control and the properties you define will depend on your form's specific needs.

For example, you might need to add more properties for dropdown-type-like options, or a checkbox-type-like checked. The JSON structure can be as simple or complex as your form requires.

Creating the Angular Dynamic Form Component

Creating a New Angular Component for the Dynamic Form

Let's start by creating a new Angular component dedicated to handling our dynamic form. You can do this using the Angular CLI's generate component command. In your terminal, navigate to your project's root directory and run:

 ng generate component DynamicForm

This command will create a new component named DynamicForm with its own HTML, CSS, and TypeScript files, as well as update the app.module.ts file with the new component.

Importing Necessary Angular Modules and Dependencies for Reactive Forms

Next, we'll import the necessary Angular modules and dependencies for creating reactive forms.

Open the dynamic-form.component.ts file and start by importing the FormGroup and FormBuilder classes from @angular/forms.

FormGroup is a class that tracks the value and state of a group of FormControl instances, and FormBuilder is a helper class that simplifies common reactive form tasks. Here's how you can import these classes:

import { Component, OnInit } from '@angular/core';
 import { FormGroup, FormBuilder } from '@angular/forms';

Setting Up the Basic Structure of the Form Component Template

With the necessary classes imported, we can start setting up the basic structure of our form component.

We'll define a property for our form as an instance of FormGroup, and we'll use FormBuilder to create the form structure. Add the following code within your DynamicFormComponent class:

export class DynamicFormComponent implements OnInit {
   dynamicForm: FormGroup;constructor(private formBuilder: FormBuilder) { }ngOnInit() {
     this.dynamicForm = this.formBuilder.group({
       // Here we will add our form controls dynamically
     });
   }
 }

This sets up an empty form group. In the next section, we will fetch the form structure from our JSON object and use it to add form controls to our form dynamically.

Meanwhile, let's prepare our form template. Open dynamic-form.component.html and replace its contents with:

<form [formGroup]="dynamicForm">
   <!-- Here we will add our form controls dynamically -->
 </form>

This is the basic structure of our form component. We've bound our form group to the form using the formGroup directive. In the next steps, we'll add form controls to this form based on our JSON-defined structure.

Populating the Form with JSON Data

Loading the JSON Data into the Form Component

Typically, you might fetch your form structure from a server using an HTTP request. However, for simplicity's sake, we will define our form structure within a service.

Create a new service named form-service by running:

 ng generate service FormService

Open the newly generated form-service.service.ts file and add the following method that returns our form structure:

import { Injectable } from '@angular/core';@Injectable({
   providedIn: 'root'
 })
 export class FormService {constructor() { }getFormStructure() {
     // Return the JSON structure defined earlier
     return [
       //... your JSON structure here
     ];
   }
 }

Next, import the FormService into the DynamicFormComponent and call the getFormStructure method:

import { Component, OnInit } from '@angular/core';
 import { FormGroup, FormBuilder } from '@angular/forms';
 import { FormService } from '../form-service.service';export class DynamicFormComponent implements OnInit {
   dynamicForm: FormGroup;constructor(private formBuilder: FormBuilder, private formService: FormService) { }ngOnInit() {
     const formStructure = this.formService.getFormStructure();// We'll parse the form structure next
   }
 }

Parsing JSON Data and Dynamically Generating Form Controls

Let's parse our form structure and generate form controls. We'll loop over our form structure and create a form control for each item using the formBuilder:

ngOnInit() {
   const formStructure = this.formService.getFormStructure();
   
   let formGroup = {};
   formStructure.forEach(control => {
     formGroup[control.name] = [control.value || ''];
   });this.dynamicForm = this.formBuilder.group(formGroup);
 }

Here we've created a form control for each item in our JSON object, initializing it with the provided value or an empty string if no value is provided.

Binding Form Controls to the Template and Displaying Them

The final step is to bind our form controls to the template.

In our form template, we'll use the formControlName directive to bind the form controls to their corresponding input elements. Since our form controls are dynamic, we'll use the *ngFor directive to loop over the form controls and generate an input for each one.

In dynamic-form.component.html, add or replace with:

<form [formGroup]="dynamicForm">
   <div *ngFor="let control of formService.getFormStructure()">
     <label>{{control.label}}</label>
     <input [type]="control.type" [formControlName]="control.name">
   </div>
 </form>

In this template, we're generating a label and an input for each form control in our form structure.

The input's type and form control name are bound to the respective properties of the form control.

With this, we've created a dynamic form in Angular using JSON data.

Handling Form Submissions and validation

Implementing Form Submission Handling

Now, we want to be able to submit the form and log the form values.

To handle form submissions, we'll add a submit event to our form in the template, and we'll create a method named onSubmit in our component to handle the event.

Add a button to the form in the dynamic-form.component.html:

<form [formGroup]="dynamicForm" (ngSubmit)="onSubmit()">
   <div *ngFor="let control of formService.getFormStructure()">
     <label>{{control.label}}</label>
     <input [type]="control.type" [formControlName]="control.name">
   </div>
   <button type="submit">Submit</button>
 </form>

Next, create the onSubmit method in the DynamicFormComponent class to handle form submissions:

 onSubmit() {
   console.log(this.dynamicForm.value);
 }

This method will log the form values when the form is submitted.

Validating Form Inputs

Angular provides built-in validators that you can use to validate form inputs. In our JSON structure, we've defined a validations array for each form control.

We can now use this information to add validation to our form controls.

Modify the ngOnInit method to add validators:

import { Validators } from '@angular/forms';ngOnInit() {
   const formStructure = this.formService.getFormStructure();
   
   let formGroup = {};
   formStructure.forEach(control => {
     let controlValidators = [];if (control.validations) {
       control.validations.forEach(validation => {
         if (validation.validator === 'required') controlValidators.push(Validators.required);
         if (validation.validator === 'email') controlValidators.push(Validators.email);
         // Add more built-in validators as needed
       });
     }
 ​
     formGroup[control.name] = [control.value || '', controlValidators];
   });this.dynamicForm = this.formBuilder.group(formGroup);
 }

In this code, we're adding the necessary validators to each form control based on the validations array.

Displaying Validation Errors

To display validation errors, we'll add a span element to our form control template that will display the error message when the form control is invalid.

Modify the template in dynamic-form.component.html as follows:

 <form [formGroup]="dynamicForm" (ngSubmit)="onSubmit()">
   <div *ngFor="let control of formService.getFormStructure()">
     <label>{{control.label}}</label>
     <input [type]="control.type" [formControlName]="control.name">
     <span *ngIf="dynamicForm.get(control.name).invalid && dynamicForm.get(control.name).touched">
       {{getErrorMessage(control)}}
     </span>
   </div>
   <button type="submit">Submit</button>
 </form>

Here, we're using the *ngIf directive to conditionally display the span element when the form control is invalid.

Next, create the getErrorMessage method in the DynamicFormComponent class:

 getErrorMessage(control) {
   const formControl = this.dynamicForm.get(control.name);for (let validation of control.validations) {
     if (formControl.hasError(validation.name)) {
       return validation.message;
     }
   }return '';
 }

In this method, we're looping through the validations of the form control and returning the message of the first validation error we find.

And that's it!

We've implemented form submission handling and validation in our dynamic form. Now, when you fill out the form and click submit, you should see the form values logged in the console.

If you try to submit the form with invalid values, you should see the appropriate error messages.

Implementing Dynamic Form Control Interactions

In any practical application, form fields often have interactions.

For instance, the visibility of a form control might depend on the value of another control, some fields might enable or disable based on others, or options in a dropdown might change based on a selection in another dropdown.

Let's see how to implement such interactions in our dynamic form.

Implementing Dynamic Interactions based on JSON structure

To implement dynamic interactions, we first need to define these interactions in our JSON structure. For instance, to implement conditional visibility, we can add a visible property to our form controls. This property can be a boolean or a function that takes the form values and returns a boolean.

Here's an example JSON structure with a visible property:

 [
   {
     "type": "text",
     "label": "Name",
     "name": "name",
     "value": "",
     "visible": true
   },
   {
     "type": "email",
     "label": "Email",
     "name": "email",
     "value": "",
     "visible": form => form.name && form.name.length > 0
   }
 ]

In this example, the email field is only visible if the name field has a value.

Conditional Visibility, Enabling/Disabling Controls, Updating Options Dynamically

To implement these interactions in our form, we'll modify our form template to include *ngIf for conditional visibility, [disabled] for enabling/disabling controls, and [options] for updating options dynamically.

For conditional visibility:

 <div *ngFor="let control of formService.getFormStructure()">
   <ng-container *ngIf="control.visible === true || control.visible(dynamicForm.value)">
     <label>{{control.label}}</label>
     <input [type]="control.type" [formControlName]="control.name">
   </ng-container>
 </div>

For enabling/disabling controls, you might add an enabled property to your form controls:

 <div *ngFor="let control of formService.getFormStructure()">
   <label>{{control.label}}</label>
   <input [type]="control.type" [formControlName]="control.name" [disabled]="control.enabled === false || !control.enabled(dynamicForm.value)">
 </div>

For updating options dynamically, you might add an options property to your form controls, especially for select type:

 <div *ngFor="let control of formService.getFormStructure()">
   <label>{{control.label}}</label>
   <select *ngIf="control.type === 'select'" [formControlName]="control.name">
     <option *ngFor="let option of control.options(dynamicForm.value)" [value]="option.value">{{option.label}}</option>
   </select>
 </div>

Note: The function-based properties like visible, enabled, and options should be functions in your service or component class, not in the JSON structure itself. The JSON structure would simply contain a string that references these functions.

And that's it!

With this, we have covered how to create a dynamic reactive form in Angular using a JSON structure, and how to implement dynamic interactions between form controls.

This approach offers a high level of flexibility and control over your forms, and can greatly simplify the process of creating complex forms.

Advanced Form Features

With Angular's reactive forms, we can build more complex features like form arrays, nested forms, and custom form control components.

We'll explore how to integrate these advanced features within our dynamic form structure.

Exploring Advanced Form Features

  • Form Arrays: A FormArray is an alternative to FormGroup for managing any number of unnamed controls. This is useful when you don't know the number of controls in a group until runtime. For example, you could allow the user to enter multiple addresses and represent them in a form array.
  • Nested Forms: A nested form group is useful when you have a section of a form that contains multiple controls, but you want to group those controls and handle them as a unit. This can help simplify the structure of your form, particularly if your form has complex requirements.
  • Custom Form Control Components: Sometimes, the standard form controls may not meet your needs. In such cases, you can create your own custom form control components.

Implementing Advanced Features Using JSON

Form Arrays: For form arrays, we would add a controls array to our JSON structure, representing the array of controls. Here's how you could define a form array for entering multiple addresses:

 {
   "type": "array",
   "label": "Addresses",
   "name": "addresses",
   "controls": [
     {
       "type": "text",
       "label": "Street",
       "name": "street"
     },
     {
       "type": "text",
       "label": "City",
       "name": "city"
     }
     // more controls as needed...
   ]
 }

Nested Forms: For nested forms, you would have a form control where the type is group, and it contains an array of controls:

 {
   "type": "group",
   "label": "Address",
   "name": "address",
   "controls": [
     {
       "type": "text",
       "label": "Street",
       "name": "street"
     },
     {
       "type": "text",
       "label": "City",
       "name": "city"
     }
     // more controls as needed...
   ]
 }

Custom Form Control Components: For a custom form control, you would have a form control where the type corresponds to the custom component's selector. You would also need to implement ControlValueAccessor in your custom component to ensure it can work with formControlName.

 {
   "type": "my-custom-component",
   "label": "My Custom Component",
   "name": "myCustomComponent"
 }

Then, in your form template, you would use Angular's dynamic component loader to load the appropriate component for each form control.

Implementing these advanced features would require modifying the way we parse our form structure and generate our form controls, as well as the way we bind our form controls in our form template. The exact implementation will depend on the specific requirements of your form. However, the examples above should give you a good starting point for defining these advanced features in your JSON structure.

Angular dynamic forms using JSON

The complete picture: A Code Summary for Dynamic Forms with JSON

Now that you have the theory bits and pieces, let's put them all together as a coherent working Angular dynamic form using JSON.

Setup

First, you need to create a new Angular project using Angular CLI. In your terminal, execute:

 ng new dynamic-forms

This will create a new Angular project. Navigate into the project directory:

 cd dynamic-forms

Creating Form Service

Create a new Angular service:

 ng g service form-service

Update the form-service.service.ts with the following:

 import { Injectable } from '@angular/core';@Injectable({
   providedIn: 'root'
 })
 export class FormService {
   formStructure = [
     {
       "type": "text",
       "label": "Name",
       "name": "name",
       "value": "",
       "validations": [
         {
           "name": "required",
           "validator": "required",
           "message": "Name is required"
         }
       ]
     },
     {
       "type": "email",
       "label": "Email",
       "name": "email",
       "value": "",
       "validations": [
         {
           "name": "required",
           "validator": "required",
           "message": "Email is required"
         },
         {
           "name": "pattern",
           "validator": "email",
           "message": "Invalid email format"
         }
       ]
     }
   ];getFormStructure() {
     return this.formStructure;
   }
 }

This service will provide our form's structure and validations.

Creating Dynamic Form Component

Create a new Angular component:

 ng g component dynamic-form

Update the dynamic-form.component.ts with the following:

 import { Component, OnInit } from '@angular/core';
 import { FormGroup, FormBuilder, Validators } from '@angular/forms';
 import { FormService } from '../form-service.service';@Component({
   selector: 'app-dynamic-form',
   templateUrl: './dynamic-form.component.html',
   styleUrls: ['./dynamic-form.component.css']
 })
 export class DynamicFormComponent implements OnInit {
   dynamicForm!: FormGroup;constructor(private formBuilder: FormBuilder, public formService: FormService) { }ngOnInit() {
     const formStructure = this.formService.getFormStructure();let formGroup: { [key: string]: any } = {};
     formStructure.forEach(control => {
       let controlValidators: any[] = [];if (control.validations) {
         control.validations.forEach((validation: any) => {
           if (validation.validator === 'required') controlValidators.push(Validators.required);
           if (validation.validator === 'email') controlValidators.push(Validators.email);
           // Add more built-in validators as needed
         });
       }
 ​
       formGroup[control.name] = [control.value || '', controlValidators];
     });this.dynamicForm = this.formBuilder.group(formGroup);
   }onSubmit() {
     console.log(this.dynamicForm.value);
   }getErrorMessage(control: any) {
     const formControl = this.dynamicForm.get(control.name);for (let validation of control.validations) {
       if (formControl?.hasError(validation.name)) {
         return validation.message;
       }
     }return '';
   }
 }

The above component will dynamically create our form with the structure and validations provided by our form service.

Update the dynamic-form.component.html with the following:

 <form [formGroup]="dynamicForm" (ngSubmit)="onSubmit()">
   <div *ngFor="let control of formService.getFormStructure()">
     <label>{{control.label}}</label>
     <input [type]="control.type" [formControlName]="control.name">
     <span *ngIf="dynamicForm.get(control.name)?.invalid && dynamicForm.get(control.name)?.touched">
       {{getErrorMessage(control)}}
     </span>
   </div>
   <button type="submit">Submit</button>
 </form>

Here we are iterating over each control in our form structure and creating a corresponding input element in our form.

Update App Module

Lastly, update the app.module.ts:

 import { NgModule } from '@angular/core';
 import { BrowserModule } from '@angular/platform-browser';
 import { AppRoutingModule } from './app-routing.module';
 import { AppComponent } from './app.component';
 import { ReactiveFormsModule } from '@angular/forms';
 import { DynamicFormComponent } from './dynamic-form/dynamic-form.component';@NgModule({
   declarations: [
     AppComponent,
     DynamicFormComponent
   ],
   imports: [
     BrowserModule,
     AppRoutingModule,
     ReactiveFormsModule
   ],
   providers: [],
   bootstrap: [AppComponent]
 })
 export class AppModule { }

Here, we are importing ReactiveFormsModule which is necessary to handle our dynamic form and we are declaring our DynamicFormComponent.

Running the Project

Run the project using ng serve, and open http://localhost:4200 to view it in the browser.

To view the form directly on load, replace the contents of the app.component.html with .

That's it!

Now you should be able to create a dynamic form in Angular based on a JSON object. The form should be flexible enough to accommodate new fields and validations simply by modifying the JSON object.

Docker Deep Dive - free eBook

Docker like a pro!

GET FREE EBOOK

Upgrading your Angular dynamic forms through JSON

Now, you're equipped with a dynamic and versatile tool for creating reactive forms in Angular. However, this is just the beginning. The true beauty of this approach lies in its ability to be extended and customized to fit a wide array of complex scenarios.

Therefore, I encourage you to experiment with what you've learned. Tinker with the JSON structure, add more complex validations, create intricate form control interactions, and devise custom form controls. As you experiment, you'll uncover the true power and flexibility this approach offers.

With a better understanding of dynamic reactive forms in Angular, you're well on your way to building sophisticated, user-friendly forms for your applications.

Happy coding!

icon

Join the team where quality matters

Be proud of your code and enjoy your life, great minds are always welcomed

Join now

Related posts