I can’t imagine software without forms. So thought to share a code sample that I use as a reusable Angular code in my projects for forms.
Creating a form for each and every requirement would be time-consuming if we are not using a reusable component or a common function that creates the form dynamically with varied inputs. Most of the time we end up writing individual code for each form assuming that it is one time for a project and will only have few forms for a project ignoring that the reusable code once written is tested and will become bug free eventually reducing the time and effort to re-create a form that fits any dynamic requirement.
Best practice is to create forms dynamically, using metadata that describes the business object model. Dynamic forms can be a very powerful instrument. Imagine you have a varying object model like in no code platform, where the user can decide how many fields should be there. Think of interfaces such as where you wouldn’t know about how many fields and which kind of fields the user of the form is about to define. There are a lot of interesting use cases.
To create a dynamic form, we have to go through an exercise of defining the structure of form as well as data we have to capture. Look at the below object that represents the example structure
// employee.tsexport const employee = {
name: {
label: 'Name',
value: 'Sangeetha',
type: 'text',
validation: {
required: true
}
},
age: {
label: 'Age',
value: 26,
type: 'text'
},
gender: {
label: 'Gender',
value: 'F',
type: 'radio',
options: [{ label: 'Male', value: 'M' }, { label: 'Female', value: 'F' }]
},
city: {
label: 'City',
value: '39010',
type: 'select',
options: [
{ label: '(choose one)', value: '' },
{ label: 'Hyderabad', value: '39100' },
{ label: 'Chennai', value: '39010' },
{ label: 'Bangalore', value: '39057' }
],
validation: {
required: true
}
}
};
The name itself represents the property of our Employee model. That’s the property that gets updated with the user input value in the forms. The actual value of the property name which gets data-bound to the form input field. The data might already present in our model/store/DB, or a default value. The value in the label key is used for display in the form to instruct the user on what to input for the field associated or below it. The type indicates which HTML input field should be rendered (text, radio, select,…) and a set of validations to be applied to the input field of the form.
To start, let’s take a closer look at how we would bind the Employee’s name property using Angular’s reactive form approach. Create a FormGroup with a FormControl for our name property in the component class.@Component({...})
export class employeeEditComponent {
form: FormGroup;
constructor() {
this.form = new FormGroup({
name: new FormControl()
})
}
}
Then, you have to bind the component template with the FormGroup we just created (form member variable). Use the [formGroup] directive and then the formControlName directive to associate name FormControl to the correct HTML input field.@Component({
selector: 'employee-edit',
template: `
...
<form [formGroup]="form">
<div>
Name: <input type="text" formControlName="name" />
</div>
</form> ...
`
})
export class EmployeeEditComponent {
form;
...
}
Rendering the form dynamically:
Now let’s make it dynamic, create a new Angular component with your preferred name in my case I’m using dynamic-form.component.ts and this component creates a dynamic form based on the @Input it receives.@Component({...})
export class DynamicFormComponent implements OnInit {
@Input() dataObject;
}
Pass the employee object containing the metadata of the dynamic form.import { employee } from './employee';
@Component({
selector: 'my-app',
template: `
<dynamic-form [dataObject]="employee"></dynamic-form>
`
})
export class AppComponent {
employee;
constructor() {
this.employee = employee;
}
This new structure is more suitable for iterating over it and to setup the form. We use Object.keys(..) to iterate over all of the properties coming from the @Input and store the result in a member variable objectProps of our component (which we’ll access later from within our template).@Component({...})
export class DynamicFormComponent implements OnInit {
@Input() dataObject;
objectProps;
constructor() {
}
ngOnInit() {
// remap the API to be suitable for iterating over it
this.objectProps =
Object.keys(this.dataObject)
.map(prop => {
return Object.assign({}, { key: prop} , this.dataObject[prop]);
});
}
...
}
Dynamically render FormGroup and FormControls:
Let’s build the FormGroup and FormControls from the dynamic inputs and use them for creating the binding on our HTML form.@Component({...})
export class DynamicFormComponent implements OnInit {
@Input() dataObject;
form: FormGroup;
objectProps; constructor() {
} ngOnInit() {
// remap the API to be suitable for iterating over it
this.objectProps = ...
// setup the form
const formGroup = {};
for(let prop of Object.keys(this.dataObject)) {
formGroup[prop] = new FormControl(this.dataObject[prop].value || '', this.mapValidators(this.dataObject[prop].validation));
} this.form = new FormGroup(formGroup);
}
mapValidators(validators) {...}
...
}
Note, we iterate over our dataObject which again comes from our component @Input. For each property we create a new FormControl, passing in the property value this.dataObject[prop].value (or an empty string) and we map it to the formGroup using the property name as key formGroup[prop]. formGroup it is a plain JavaScript object which we then assign to the form member variable of the component.
Form Validators:
We have to invoke a this.mapValidators(...) function passing the validators specified in the validation property of our dataObject coming from the @Input. The function is quite simple, all it does is to take the defined validation and map it to appropriate form validator instance:private mapValidators(validators) {
const formValidators = [];
if(validators) {
for(const validation of Object.keys(validators)) {
if(validation === 'required') {
formValidators.push(Validators.required);
} else if(validation === 'min') {
formValidators.push(Validators.min(validators[validation]));
}
}
}
return formValidators;
}
Rendering of HTML
We have our form, FormGroup object as well as the array of properties to map that is stored in the objectProps member. Use those two in our template to construct the actual HTML form.
First, we create the HTML form and bind the FormGroup we created.<form novalidate (ngSubmit)="onSubmit(form.value)" [formGroup]="form">
<p>
<button type="submit">Save</button>
</p>
</form>
Next, we iterate over the objectProps to make sure we created a form input field for all our object’s properties. For instance, we should see the labels with the specified value being rendered.<form novalidate (ngSubmit)="onSubmit(form.value)" [formGroup]="form">
<div *ngFor="let prop of objectProps">
<label [attr.for]="prop">{{prop.label}}</label>
</div>
<p>
<button type="submit">Save</button>
</p>
</form>
Let’s check that for our simple name property which needs a <input type="text">
field.<form novalidate (ngSubmit)="onSubmit(form.value)" [formGroup]="form">
<div *ngFor="let prop of objectProps">
<label [attr.for]="prop">{{prop.label}}</label>
<div [ngSwitch]="prop.type">
<input *ngSwitchCase="'text'"
[formControlName]="prop.key"
[id]="prop.key" [type]="prop.type">
</div>
</div>
...
</form>
we use a ngSwitch statement on the type property which defines which kind of input field we need. We then specify a ngSwitchCase=”‘text'” to define the case for text inputs. By doing so we also bind the formControlName accordingly as well as the type and id property of the HTML input field.<input *ngSwitchCase="'text'"
[formControlName]="prop.key"
[id]="prop.key" [type]="prop.type">
This is it. As the last step, let’s also render our validators. The current example just account for required validators, but it should be an easy task for us to extend it for the complex validations as well.<div [ngSwitch]="prop.type">
<input *ngSwitchCase="'text'"
[formControlName]="prop.key"
[id]="prop.key" [type]="prop.type">
</div>
<div class="error" *ngIf="form.get(prop.key).invalid && (form.get(prop.key).dirty || form.get(prop.key).touched)">
<div *ngIf="form.get(prop.key).errors.required">
{{ prop.label }} is required.
</div>
</div>
Mapping few more input fields types
Add a few more ngSwitchCase statements and establish the binding with the reactive form.
Dropdown:
We need value for all available options to render a dropdown. If you looked at my data sample city property is have this structure:city: {
label: 'City',
value: '39010',
type: 'select',
options: [
{ label: '(choose one)', value: '' },
{ label: 'Hyderabad', value: '39100' },
{ label: 'Chennai', value: '39010' },
{ label: 'Bangalore', value: '39057' }
],
validation: {
required: true
}
}
In our HTML we will need the options property for rendering the possible options for the user to choose from.<div *ngSwitchCase="'select'">
<select [formControlName]="prop.key">
<option *ngFor="let option of prop.options" [value]="option.value">
{{ option.label }}
</option>
</select>
</div>
Radio button lists:
Radio button lists are similar to our previous dropdown input field. Take a look:<div *ngSwitchCase="'radio'">
<label *ngFor="let option of prop.options">
<input
type="radio"
[name]="prop.key"
[formControlName]="prop.key"
[value]="option.value"> {{option.label}}
</label>
</div>
Conclusion
As you have seen, creating a simple dynamic form using the reactive approach in Angular is extremely easy. Of course in a real-world scenario things might get more complex due to the complexity of your forms or your application requirements.