Use Formly to generate your forms dynamically
One of the most common features we add to every single application is CRUD, the ability for our users to interact with our app through forms.
It can be tedious sometimes. For example, I don’t know about you, but I always forget to include
ReactiveFormsModule
in the module I create my form, and I only remember when the entire console
goes red with errors.
What if we could let something else do the heavy lifting for us? We send the fields we want to see, and something else handles the rendering, the validation, the values, etc.
That’s where Formly comes in. It’s a dynamic form library that acts as a form renderer. You pass the fields, and Formly generates the form for you.
Today, you’ll learn how to use Formly to create a form-renderer component where you’ll centralize all the functionality, and then you’ll be able to use it throughout your entire application.
We’ll break the process into 5 steps:
- Installing and Initializing Formly.
- Create the form shell and handle the form result.
- Adding fields dynamically.
- Adding Formly built-in validators.
- Creating your custom validators.
Installing and Initializing Formly
The first thing you’d need is an @ionic/angular
application. If you haven’t created one, feel free
to go through the introduction tutorial first.
If you have your app ready, let’s start creating the component and the module we’ll use for our form renderer. For that, open your terminal in the root folder of your project and type:
ionic generate module form-renderer && ionic generate component form-renderer
If you use Windows regular cmd, you might need to break those into two separate commands:
ionic generate module form-renderer
ionic generate component form-renderer
After we have our shell component for Formly, we’re going to install Formly, luckily they provide a custom schematic that will do the updates for us, open the terminal again and type:
ng add @ngx-formly/schematics --ui-theme=ionic --module=app/form-renderer
Where:
ng add @ngx-formly/schematics
will install the packages you need.--ui-theme=ionic
tells Formly we’ll use Ionic as our UI library (Formly supports bootstrap, angular material, IOnic, and others out of the box).--module=app/form-renderer
tells Formly we’ll use the newly createdform-renderer
as our module to initialize Formly there.
If you open your form-renderer.module.ts
file you’ll see something similar to this:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { FormRendererComponent } from './form-renderer.component';
import { FormlyModule } from '@ngx-formly/core';
import { FormlyIonicModule } from '@ngx-formly/ionic';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
ReactiveFormsModule,
FormlyModule.forRoot({ extras: { lazyRender: true } }),
FormlyIonicModule,
],
declarations: [FormRendererComponent],
exports: [FormRendererComponent],
})
export class FormRendererModule {}
Notice how it added ReactiveFormsModule FormlyModule FormlyIonicModule
to our
FormRendererModule
.
Also, exports: [FormRendererComponent],
isn’t something the CLI does by default, so make sure you
export the component here.
Create the Form Shell and Handle Form Results
Now it’s time to create the shell that will hold our forms for us, for that, we’ll work on the
form-renderer
component.
First, let’s add the view, open form-renderer.component.html
file and add the following:
<div class="form-container">
<form [formGroup]="customForm" (ngSubmit)="submitForm(customForm)" *ngIf="formFieldList">
<formly-form [model]="formModel" [fields]="formFieldList" [form]="customForm"></formly-form>
<div class="form-buttons">
<ion-button expand="block" type="submit" color="primary" [disabled]="!customForm.valid"
>Submit</ion-button
>
<ion-button expand="block" type="reset" color="medium">Clear Form</ion-button>
</div>
</form>
</div>
Notice there are a couple of things we’re doing here:
- We’re creating a parent
<form>
that will be ourReactiveForm
and initialize Formly. - Inside, we’re declaring a
<formly-form>
and telling it which form, fields, and model it will load. - We’re adding 2 buttons, one to clear the form and another one to handle the submitted value.
- The submit button is disabled until the form is valid.
You probably see some errors on your editor, no worries, it’s because we haven’t created some of the
things we’re referencing here, so let’s now do that, open the form-renderer.component.ts
file, and
let’s start by importing what we need.
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FormlyFieldConfig } from '@ngx-formly/core';
We’re importing:
FormlyFieldConfig
from formly to type our fields.FormGroup
from@angular/forms
to create our form.- And
EventEmitter, Input, Output
from@angular/core
to receive and pass the data from this component to its parent.
Then, let’s create the functionality:
export class FormRendererComponent {
@Input() formFieldList: FormlyFieldConfig[] = [];
@Input() formModel: unknown;
@Output() formSubmit: EventEmitter<any> = new EventEmitter();
customForm: FormGroup = new FormGroup({});
submitForm(formToSubmit: FormGroup): void {
const { value } = formToSubmit;
this.formSubmit.emit(value);
}
}
We’re doing a couple of things here:
- We’re adding the
@Input()
to send the fields and model from the parent component to our form. - We’re adding the
@Output()
event emitter andsubmitForm()
function so that when the user submits the form, we can send the value back to the parent component.
With that, our form is ready to work. Now, let’s open another one of our components, since this is a
test, let’s say you’re working on the AppComponent
itself.
Adding Form Fields Dynamically
First, we need to import our FormRendererModule
in the module we want to use, in this case, the
AppModule
, for that, go ahead and open the app.module.ts
file and add it to the imports
array.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { FormRendererComponentModule } from '../form-renderer/form-renderer.module';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
FormRendererModule, // Here it is
],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}
Once it’s added, you can now use it from your component, so go ahead and open your
app.component.html
file and add your form:
<app-form-renderer
[formModel]="formModel"
[formFieldList]="formFields"
(formSubmit)="getFormEvent($event)"
></app-form-renderer>
I have app-form-renderer
as a selector, but you can double-check your form-renderer.component.ts
file to ensure you have the correct selector here.
Here we’re telling the app component 4 things:
- First, to render our
FormRendererComponent
. - To send our
formModel
class variable as theformModel
property. - To send our
formFields
class variable as theformFieldList
property. - To set our
getFormEvent
function to handle theformSubmit
event.
We don’t have any of those, so we’ll move to our app.component.ts
file to create them all.
import { Component } from '@angular/core';
import { FormlyFieldConfig } from '@ngx-formly/core';
@Component({
selector: 'app-component',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
formModel: unknown = {};
formFields: FormlyFieldConfig[] = [];
constructor() {}
}
Now, we can start creating our fields, we’ll focus on the formFields
array and add them there.
Formly uses a declarative JSON where you can pass the fields like this:
formFields: FormlyFieldConfig[] = [
{
key: 'name',
type: 'input',
templateOptions: {
label: 'Full Name',
placeholder: 'Please enter your full name'
},
},
{
key: 'Textarea',
type: 'textarea',
templateOptions: {
label: 'Textarea',
placeholder: 'Placeholder',
},
},
{
key: 'Checkbox',
type: 'checkbox',
templateOptions: {
label: 'Accept terms',
},
},
];
As you can see, we’re able to pass the key and type for each field, and then, we pass a property
called templateOptions
, in this property, we can pass more data, like the label, placeholder, some
validators, etc.
Using Formly Built-in validators
Formly has several built-in validators, for example, we can pass the required validator, we can pass min, max for the value of numbers, or we can also validate the length of strings, and validate against patterns:
fields: FormlyFieldConfig[] = [
{
key: 'name',
type: 'input',
templateOptions: {
label: 'Name (required)',
required: true,
},
},
{
key: 'age',
type: 'input',
templateOptions: {
label: 'Age (min= 18, max= 40)',
type: 'number',
min: 18,
max: 40,
required: true,
},
},
{
key: 'password',
type: 'input',
templateOptions: {
label: 'Password (minLength = 6)',
type: 'password',
required: true,
minLength: 6,
},
},
{
key: 'ip',
type: 'input',
templateOptions: {
label: 'IP Address (pattern = /(\d{1,3}\.){3}\d{1,3}/)',
pattern: /(\d{1,3}\.){3}\d{1,3}/,
required: true,
},
validation: {
messages: {
pattern: (error, field: FormlyFieldConfig) => `"${field.formControl.value}" is not avalid IP Address`,
},
},
},
];
Creating custom validators
We can also add our custom validators, for example, let’s say we want to have in our form validation for the password field, where we have 2 fields to add our password, and we confirm that the user added it properly in both fields.
For that, we can add a custom validator, let’s call it fieldMatch
, for this validator to apply
everywhere, we’ll go and add it in the form-renderer.module.ts
file.
import {
AbstractControl,
...
} from '@angular/forms';
function fieldMatchValidator(control: AbstractControl) {
const { password, passwordConfirm } = control.value;
if (!passwordConfirm || !password) { return null; }
if (passwordConfirm === password) { return null; }
return {
fieldMatch: { message: `Your password doesn't match the confirmation` },
};
}
@NgModule({
imports: [
FormlyModule.forRoot({
extras: { lazyRender: true },
validators: [{ name: 'fieldMatch', validation: fieldMatchValidator }],
}),
FormlyIonicModule,
],
declarations: [FormRendererComponent],
exports: [FormRendererComponent],
})
export class FormRendererModule {}
After our new validator is in use, we can go back to the component and send a new field, like this:
fields: FormlyFieldConfig[] = [{
validators: {
validation: [
{ name: 'fieldMatch', options: { errorPath: 'passwordConfirm' } },
],
},
fieldGroup: [
{
key: 'password',
type: 'input',
templateOptions: {
type: 'password',
label: 'Password',
required: true,
},
},
{
key: 'passwordConfirm',
type: 'input',
templateOptions: {
type: 'password',
label: 'Confirm Password',
placeholder: 'Please re-enter your password',
required: true,
},
},
],
}];
Our form renderer will understand that we’re trying to use the fieldMatch
validator when typing
into our passwordConfirm
field.
With this, we could create lots of custom logic and maybe port this form-renderer component into all our apps as a way to make our process faster and a bit less buggy 😊
You get the idea. There are lots of things we can do and add to start making our small libraries that come with us from project to project 😊