Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for How to Build Dynamic Forms in Angular
Dany Paredes
Dany Paredes

Posted on • Edited on • Originally published atdanywalls.com

     

How to Build Dynamic Forms in Angular

When we build Angular Apps, the creation and build of forms is a manual process. The maintenance for small changes like adding a new field or switching field type from input to a date field should be easy, but why not create forms to be flexible and respond to business changes?

These changes require we touch the form again to update the hard-coded form declaration and fields. Why not change to make our form dynamic and not hard-coded?

Today we will learn how to create forms using reactive forms from a model and bind them to inputs, radio, and checkboxes in the template dynamic.

I know the code needs types and interfaces, but I want to make a manageable article.

Scenario

We work for the marketing team, which wants a form to request the users Firstname, Lastname, and age. Let's build it.

First, declare aFormGroup fieldregisterForm and create the methodbuildForm() to manually add every field in the formGroup.

import{Component,OnInit,VERSION}from'@angular/core';import{FormControl,FormGroup}from'@angular/forms';@Component({selector:'my-app',templateUrl:'./app.component.html',styleUrls:['./app.component.css'],})exportclassAppComponentimplementsOnInit{registerForm:FormGroup;ngOnInit(){this.buildForm();}buildForm(){this.registerForm=newFormGroup({name:newFormControl(''),lastName:newFormControl(''),age:newFormControl(''),});}}
Enter fullscreen modeExit fullscreen mode

Add the HTML markup with the inputs linked with the form using the[formGroup] directive.

<h1>Register</h1><form[formGroup]="registerForm"><label>Name:<inputtype="text"formControlName="name"/></label><label>LastName:<inputtype="text"formControlName="lastName"/></label><label> Age:<inputtype="number"formControlName="age"/></label></form>
Enter fullscreen modeExit fullscreen mode

And Finally, we have our static forms!

But tomorrow, marketing wants to request the address or a checkbox for the newsletter. We need to update the form declaration, add theformControl and the inputs, and you know the whole process.

We want to avoid repeating the same task every time. We need to turn on the form to dynamic and react to business changes without touching the html or typescript file again.

Create Dynamic FormControl and Inputs

First, we will have two tasks to build our form dynamic.

  • Create the Form Group from the business object.

  • Show the list of fields to render in the form.

First, renameregisterModel tomodel and declare the fields array. It will have all elements with inputs model for our dynamic form:

fields:[];model={name:'',lastName:'',address:'',age:'',};
Enter fullscreen modeExit fullscreen mode

Next, create the methodgetFormControlFields(), with theformGroupFields object and Iterate over all properties in the model using thefor, to push informGroupFields. Add every field into thefields array.

The code looks like this:

getFormControlsFields(){constformGroupFields={};for(constfieldofObject.keys(this.model)){formGroupFields[field]=newFormControl("");this.fields.push(field);}returnformGroupFields;}
Enter fullscreen modeExit fullscreen mode

HOLD A SECOND! Do you want to learn to build complex forms and form controls quickly??

Check out the Advanced Angular Forms & Custom Form Control Masterclass by Decoded Frontend.

Learn Advanced Angular Forms Build

In thebuildForm() method, add the variableformGroupFields with the value fromgetFormControlFields() and assign theformGroupFields to theregisterForm.

buildForm(){constformGroupFields=this.getFormControlsFields();this.registerForm=newFormGroup(formGroupFields);}
Enter fullscreen modeExit fullscreen mode

Next, render the fields in theHTML, using the*ngFor directive to iterate over thefields array. Use the variablefield to show the label and set the inputformControlName with the same field value.

<form[formGroup]="registerForm"><div*ngFor="let field of fields"><label>{{field}}</label><inputtype="text"[formControlName]="field"/></form>
Enter fullscreen modeExit fullscreen mode

Save changes, and we get the same form, generated dynamically, from the definition.

2.png

But it's just the beginning. We want to split a little bit of the responsibility to allow us to make the form flexible to changes without pain.

Separate the Form Process and FieldType

Theapp.component does a few tasks, creating the model, form, and rendering the input. Let's clean it up a little bit.

Create thedynamic-form.component with an input property to get the model to generate, and updateregisterForm todynamicFormGroup. Move the functionbuildForm andgetFormControlsFields to the dynamic form component.

import{Component,Input,OnInit}from"@angular/core";import{FormControl,FormGroup}from"@angular/forms";@Component({selector:"app-dynamic-form",templateUrl:"./dynamic-form.component.html",styleUrls:["./dynamic-form.component.css"],})exportclassDynamicFormComponentimplementsOnInit{dynamicFormGroup:FormGroup;@Input()model:{};fields=[];ngOnInit(){this.buildForm();}buildForm(){constformGroupFields=this.getFormControlsFields();this.dynamicFormGroup=newFormGroup(formGroupFields);}getFormControlsFields(){constformGroupFields={};for(constfieldofObject.keys(this.model)){formGroupFields[field]=newFormControl("");this.fields.push(field);}returnformGroupFields;}}
Enter fullscreen modeExit fullscreen mode

Remember to update theformGroup in the html todynamicFormGroup

Next, create a new componentdynamic-field with the responsibility of rendering the field. Add twoInput() propertiesfield andformName.

import{Component,Input}from"@angular/core";@Component({selector:"app-field-input",templateUrl:"./dynamic-field.component.html",styleUrls:["./dynamic-field.component.css"],})exportclassDynamicFieldComponent{@Input()field:{};@Input()formName:string;}
Enter fullscreen modeExit fullscreen mode

Add theHTML markup with the input and the label.

<form[formGroup]="formName"><label>{{field}}</label><inputtype="text"[formControlName]="field"/></form>
Enter fullscreen modeExit fullscreen mode

Open theapp.component to pass the model to the dynamic form. It takes the responsibility to process the model, and thedynamic-field renders the input.

import{Component}from"@angular/core";@Component({selector:"my-app",templateUrl:"./app.component.html",styleUrls:["./app.component.css"],})exportclassAppComponent{model={name:"",lastName:"",address:"",age:"",};}
Enter fullscreen modeExit fullscreen mode

TheHTML passes the property model with the definition.

<app-dynamic-form[model]="model"></app-dynamic-form>
Enter fullscreen modeExit fullscreen mode

Perfect, we have a few separate tasks and responsibilities. The following challenge shows different control types.

Show Inputs By Type

The Dynamic form renders a single type of input. In a real-world scenario, we need more types likedate,select,input,radio, andcheckbox.

The information about the control types must come from the model to thedynamic-field.

Change themodel with the following propertiestype,value, andlabel. To make it a bit fun, change the age to typenumber, and create a new propertybirthDay of typedate.

model={firstname:{type:"text",value:"",label:"FirstName",},lastname:{type:"text",value:"",label:"LastName",},address:{type:"text",value:"",label:"Address",},age:{type:"number",value:"",label:"age",},birthDay:{type:"date",value:"",label:"Birthday",},};}
Enter fullscreen modeExit fullscreen mode

Save the new fieldbirthDay shown in the form.

3.png

We will make small changes in thegetFormsControlsFields method to process the metadata.

Please create a new variable,fieldProps to store the field with the metadata from the model. Use the value property to assign theformControl and push the field with the propertyfieldName in the fields array.

We will use the metadata properties in the dynamic-field component

getFormControlsFields(){constformGroupFields={};for(constfieldofObject.keys(this.model)){constfieldProps=this.model[field];formGroupFields[field]=newFormControl(fieldProps.value);this.fields.push({...fieldProps,fieldName:field});}returnformGroupFields;}
Enter fullscreen modeExit fullscreen mode

Finally, go to thedynamic.component.html and use these propertiesfield.label, changeformControlName to usefield.fieldName, and bind the type withfield.type.

<form[formGroup]="formName"><label>{{field.label}}</label><input[type]="field.type"[formControlName]="field.fieldName"/></form>
Enter fullscreen modeExit fullscreen mode

Save the changes and see the new controls with a type.

4.png

Add Selects, Radios, and Checkbox

The dynamic field component shows the input, but adding controls likeselect,radio, orcheckbox makes it a bit complex. I want to split each control into specific controls.

Create components for each controldynamic-inputdynamic-radio,dynamic-select, anddynamic-checkbox.

ng g components/dynamic-field/dynamic-checkboxng g components/dynamic-field/dynamic-radiong g components/dynamic-field/dynamic-selectng g components/dynamic-field/dynamic-input
Enter fullscreen modeExit fullscreen mode

Every component has two points in common, the field with metadata and theFormGroup to like with the main form.

Let's start with the Input and Checkbox:

Input

Declare thefield object with metadata and theformName as input properties.

exportclassDynamicInputComponent{@Input()field:{};@Input()formName:FormGroup;}
Enter fullscreen modeExit fullscreen mode

In the HTML Markup, use the metadata with the label and theformControlName with thefieldName.

<form[formGroup]="formName"><label>{{field.label}}</label><input[type]="field.type"[formControlName]="field.fieldName"/></form>
Enter fullscreen modeExit fullscreen mode

Checkbox

Like thedynamic-input component, add two fields with the field metadata and theformGroup.

exportclassDynamicCheckboxsComponent{@Input()field:any;@Input()formName:FormGroup;}
Enter fullscreen modeExit fullscreen mode

In the HTML Markup, add a checkbox.

<form[formGroup]="formName"><label>{{field.label}}<inputtype="checkbox"[name]="field.fieldName"[formControlName]="field.fieldName"[value]="field.value"/></label></form>
Enter fullscreen modeExit fullscreen mode

I want to split the checkbox from the input for personal reasons; the checkbox sometimes has particular styles.

Select

The properties are the same, but the metadata will become different. The select has a list of options, so we need to iterate over the list using thengFor directive.

The HTML Markup looks like this:

<form[formGroup]="formName"><label>{{field.label}}:</label><select[formControlName]="field.fieldName"><option*ngFor="let option of field.options"[value]="option.value">{{option.label}}</option></select></form>
Enter fullscreen modeExit fullscreen mode

Radio

The radio is close, similar to the select with a list ofoptions, but with a particular case, the name must be the same to allow select one single option. We add an extralabel to show the optionlabel.

<form[formGroup]="formName"><h3>{{field.label}}</h3><label*ngFor="let option of field.options"><labelngFor="let option of field.options"><inputtype="radio"[name]="field.fieldName"[formControlName]="field.fieldName"[value]="option.value">            {{option.label}}</label></label></form>
Enter fullscreen modeExit fullscreen mode

Ok, all components are ready, but with two missing points: show the components and update the metadata.

Show Dynamic Components And Update Model

We have components for each control type, but thedynamic-field.component is a bridge between them.

It picks the specific component by type. Using thengSwitch directive, we determine the control matching with the component type.

The final code looks like this:

<ng-container[ngSwitch]="field.type"><app-dynamic-input*ngSwitchCase="'text'"[formName]="formName"[field]="field"></app-dynamic-input><app-dynamic-select*ngSwitchCase="'select'"[formName]="formName"[field]="field"></app-dynamic-select><app-dynamic-radio*ngSwitchCase="'radio'"[formName]="formName"[field]="field"></app-dynamic-radio><app-dynamic-checkboxs*ngSwitchCase="'checkbox'"[formName]="formName"[field]="field"></app-dynamic-checkboxs></ng-container>
Enter fullscreen modeExit fullscreen mode

Learn more aboutswitchCase

Next, we add new fields with the metadata for each type:

typeBussines:radio suscriptionType:select newsletterIn:checkbox

The typeradio andselect must have the options object with{ label, value} fit component expectations.

typeBussines:{label:"Bussines Type",value:"premium",type:"radio",options:[{label:"Enterprise",value:"1500",},{label:"Home",value:"6",},{label:"Personal",value:"1",},],},newsletterIn:{label:"Suscribe to newsletter",value:"email",type:"checkbox"},suscriptionType:{label:"Suscription Type",value:"premium",type:"select",options:[{label:"Pick one",value:"",},{label:"Premium",value:"premium",},{label:"Basic",value:"basic",},],},
Enter fullscreen modeExit fullscreen mode

Save and reload. The new components work with the structure and thedynamic-field picks the specific component.

5.png

Validations

We need a complete form with validations. I want to make this article brief, but validation is essential in the forms.

My example is basic about adding a required validator but feel free to add more if you want.

First, we must change themodel with new metadatarules, with the fieldrequired with thetrue value.

firstname:{type:"text",value:"",label:"FirstName",rules:{required:true,}},
Enter fullscreen modeExit fullscreen mode

The validators are part of the form controls. We process the rule to set the validator for theformControl in a new method,addValidators, and the return value stored in thevalidators variable to assign in theformControl.

constvalidators=this.addValidator(fieldProps.rules);formGroupFields[field]=newFormControl(fieldProps.value,validators);
Enter fullscreen modeExit fullscreen mode

If the rule object is empty, return an empty array

In theaddValidator, use theObject.keys and iterate over every property in therules object. Use aswitch case to math with the value and return theValidator.

In Our scenario, the rule required returns the Validator.required.

privateaddValidator(rules){if(!rules){return[];}constvalidators=Object.keys(rules).map((rule)=>{switch(rule){case"required":returnValidators.required;//add more cases for the future.}});returnvalidators;}
Enter fullscreen modeExit fullscreen mode

Ok, we already configure theformControl with the validator, but we need to show the label if the control is invalid. Create a new component,dynamic-error , with two input properties,formGroup, andfieldName.

import{Component,Input}from"@angular/core";import{FormGroup}from"@angular/forms";@Component({selector:"app-dynamic-error",templateUrl:"./dynamic-error.component.html",styleUrls:["./dynamic-error.component.css"],})exportclassDynamicErrorComponent{@Input()formName:FormGroup;@Input()fieldName:string;}
Enter fullscreen modeExit fullscreen mode

We find the control by name using the form reference in the HTML. If it isinvalid,dirty, ortouched by the user, show a message.

<div*ngIf="formName.controls[fieldName].invalid && (formName.controls[fieldName].dirty || formName.controls[fieldName].touched)"class="alert"><div*ngIf="formName.controls[fieldName].errors.required">        * {{fieldName}}</div></div>
Enter fullscreen modeExit fullscreen mode

Finally, add thedynamic-error component in thedynamic-form component and pass thefieldName and theformGroup.

<form[formGroup]="dynamicFormGroup"><div*ngFor="let field of fields"><app-field-input[formName]="dynamicFormGroup"[field]="field"></app-field-input><app-dynamic-error[formName]="dynamicFormGroup"[fieldName]="field.fieldName"></app-dynamic-error></div></form>
Enter fullscreen modeExit fullscreen mode

Read more aboutvalidators in Angular

dynamicforms.gif

Yeah!! The validators work with our dynamic forms.

You have a stable version of dynamic forms if you reach this part. I try to make this post short, but I hear feedback from other users like @Juan Berzosa Tejero and .... motivated to do some refactors.

Refactor Time

Propagation of FormGroup

After @Juan Berzosa Tejero take time to review the article, he asked me about the propagation ofFormGroup using the@Input() withformName, and it starts to make noise. Luckily I found the directiveFormGroupDirective in the Angular Documentation. It helps us to bind an existingFormGroup orFormRecord to a DOM element.

I decide to use and refactor the code; we are going to start with thedynamic-error.component to simplify, but the steps are similar for all child components.

Remove the@Input() decorator fromformName and inject theFormGroupDirective in the component constructor.

Add thengOnInit lifecycle to set theformName with theFormGroupDirective.control to bind theFormGroup to it.

The final code looks like this:

import{Component,Input,OnInit}from"@angular/core";import{FormGroup,FormGroupDirective}from"@angular/forms";@Component({selector:"app-dynamic-error",templateUrl:"./dynamic-error.component.html",styleUrls:["./dynamic-error.component.css"],})exportclassDynamicErrorComponentimplementsOnInit{formName:FormGroup;@Input()fieldName:string;constructor(privateformgroupDirective:FormGroupDirective){}ngOnInit(){this.formName=this.formgroupDirective.control;}}
Enter fullscreen modeExit fullscreen mode

Thedynamic-form doesn't need to pass theformGroupName anymore. It only needs the field metadata. The code looks like this:

<form[formGroup]="dynamicFormGroup"><div*ngFor="let field of fields"><app-field-input[field]="field"></app-field-input><app-dynamic-error[fieldName]="field.fieldName"></app-dynamic-error></div></form>
Enter fullscreen modeExit fullscreen mode

If you replicate the same for all child components,dynamic-field no longer needs to setformName.

<ng-container[ngSwitch]="field.type"><app-dynamic-input*ngSwitchCase="'text'"[field]="field"></app-dynamic-input><app-dynamic-input*ngSwitchCase="'number'"[field]="field"></app-dynamic-input><app-dynamic-select*ngSwitchCase="'select'"[field]="field"></app-dynamic-select><app-dynamic-radio*ngSwitchCase="'radio'"[field]="field"></app-dynamic-radio><app-dynamic-checkboxs*ngSwitchCase="'checkbox'"[field]="field"></app-dynamic-checkboxs></ng-container>
Enter fullscreen modeExit fullscreen mode

Done, we did the refactor! Feel free to read more aboutFormGroup Directive.

Remove the ngSwitch

Yesterday, @Maxime Lyakhov, leave a message about the ngSwich. He was right about the ngSwitch in the HTML; it is difficult to maintain.

My first idea is to load the specific component dynamically usingViewChild andViewContainerRef and set the input variables with thesetInput() method.

Note: I update the project to angular 14 because theAPI to load dynamic componentsis easier.

First, add a template variable to theng-container to make a reference using the viewchild.

<ng-container#dynamicInputContainer></ng-container>
Enter fullscreen modeExit fullscreen mode

Next, declare the viewchild pointing to the dynamicInput container. It works as a placeholder for our dynamic components.

@ViewChild('dynamicInputContainer',{read:ViewContainerRef})dynamicInputContainer!:ViewContainerRef;
Enter fullscreen modeExit fullscreen mode

Add a new array with all supported components with key and component.

supportedDynamicComponents=[{name:'text',component:DynamicInputComponent},{name:'number',component:DynamicInputComponent},{name:'select',component:DynamicSelectComponent},{name:'radio',component:DynamicRadioComponent},{name:'date',component:DynamicInputComponent},{name:'checkbox',component:DynamicCheckboxsComponent}]
Enter fullscreen modeExit fullscreen mode

Note: A service can provide the supported component or external variables list, but I try to keep the article short.

CreategetComponentByType method to find the component in the suppertedDynamicComponents , if not exist, return DynamicInputComponent.

getComponentByType(type:string):any{constcomponentDynamic=this.supportedDynamicComponents.find(c=>c.name===type);returncomponentDynamic.component||DynamicInputComponent;}
Enter fullscreen modeExit fullscreen mode

Next, a new methodregisterDynamicField(). It takes the responsibility of creating an instance from thegetComponentType() and setting the input field required by the components.

We do three steps:

  1. Get the component by type using the field property and store in thecomponentInstance variable.

  2. Using the createComponent pass the instance and get the dynamic component.

  3. Pass the field to the inputfield using thesetInput() method.

privateregisterDynamicField(){constcomponentInstance=this.getComponentByType(this.field.type)constdynamicComponent=this.dynamicInputContainer.createComponent(componentInstance)dynamicComponent.setInput('field',this.field);this.cd.detectChanges();}
Enter fullscreen modeExit fullscreen mode

Because the input property field changes , we need to trigger the change detection to keep the component sync.

_Learn more aboutChangeDetection_

The ViewChild is only available on the AfterviewInit lifecycle, implement the interface and call the methodregisterDynamicField.

ngAfterViewInit():void{this.registerDynamicField();}
Enter fullscreen modeExit fullscreen mode

Save the changes, everything continues working as expected, and the ngSwitch is gone.

Learn more about viewchild

Trigger Event onChange or onBlur

@Rakesh Prakash asked how to trigger events on changes or blur events attached to the input fields, and the first idea came to my head.

by default, the form updates the values on every keystroke, triggers the validator and the update values, and may not always be desirable.

Sometimes we want to have control over the moment value updates and validators, but Angular helps us with theupdateOn in Angular Forms.

TheupdateOn set the update strategy of our form controls and which DOM event triggers updates.

The options forupdateOn in theFormControl supported are'change' | 'blur' | 'submit';

  • change it is the default when the input or element DOM changes.

  • blur: when the user blurs the DOM element;

  • submit: when the submit event is triggered on the parent form.

Configure the model; for example, we want to trigger the update when the user blurs the name, add a new property in the model 'triggerOn'

firstname:{type:"text",value:"",label:"FirstName",triggerOn:'blur'rules:{required:true,}},
Enter fullscreen modeExit fullscreen mode

set the property update in the FormControl creation, use the property or set a default value like 'change'.

First, create a subscription to the control, to validate that it only triggers blur, not every time the input change.

Open the dynamic-form.component.ts

ngOnInit(){this.buildForm();this.dynamicFormGroup.controls['firstname'].valueChanges.subscribe((v)=>{console.log(v);});}
Enter fullscreen modeExit fullscreen mode

Update the getFormControlsFields methods and add the validator and the new property updateOn in a single object

formGroupFields[field]=newFormControl(fieldProps.value,{updateOn:fieldProps.triggerOn||'change',validators,});
Enter fullscreen modeExit fullscreen mode

The final code looks like this:

privategetFormControlsFields(){constformGroupFields={};for(constfieldofObject.keys(this.model)){constfieldProps=this.model[field];constvalidators=this.addValidator(fieldProps.rules);formGroupFields[field]=newFormControl(fieldProps.value,{updateOn:fieldProps.triggerOn||'change',validators,});this.fields.push({...fieldProps,fieldName:field});}returnformGroupFields;}
Enter fullscreen modeExit fullscreen mode

Save the changes, and the firstName only triggers the blur event; if it is not defined, then use the change by default.

If you want to apply the same for all form controls, check out myTrigger Validation in Angular Forms article.

Read Values

@Fabian asked me how to read the values from the dynamic form. We should read a single value or all dynamics properties in the model.

For a single value, use the field name and the dynamicForm.get method.

this.dynamicFormGroup.get('name').value
Enter fullscreen modeExit fullscreen mode

If we want to read all properties in the model, use theObject.keys. It returns an array with all keys in the model. Use these keys with the get method in the dynamicForm to read the control value.

The code looks like this:

Object.keys(this.model).forEach((k)=>{console.log(this.dynamicFormGroup.get(k).value)});
Enter fullscreen modeExit fullscreen mode

If you want to read the values in JSON Format, use the JSON.stringify method:

onSubmit(){console.log(JSON.stringify(this.dynamicFormGroup.value));}
Enter fullscreen modeExit fullscreen mode

Recap

We learned how to add dynamic fields from a structure and generate inputs likeselect,checkbox,radio, orinputs a few times. The model may be an API response from the backend.

If we want to add a new field, add it to the API response, and you feel free to add more types in the dynamic-field component.

We can improve the code by using interfaces for each component type, likedropdown,checkbox, or the form itself. Also, create helper functions to create most of the boilerplate code for dropdown.

Maybe you know a better way to do it if you have an idea? Let’s hear it.

Top comments(1)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
mhcrocky profile image
mhcrocky
Patience, persistence and perspiration make an unbeatable combination for success.
  • Joined

great works!

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

I'm an NBA lover, GDE in Angular, love work with Angular, Typescript and Testing Library, and sharing content in danywalls.com and ng-content.com
  • Location
    Barcelona
  • Joined

More fromDany Paredes

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp