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:

  1. Installing and Initializing Formly.
  2. Create the form shell and handle the form result.
  3. Adding fields dynamically.
  4. Adding Formly built-in validators.
  5. 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 created form-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 our ReactiveForm 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 and submitForm() 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 the formModel property.
  • To send our formFields class variable as the formFieldList property.
  • To set our getFormEvent function to handle the formSubmit 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 😊