How to auto-generate advanced forms using Formly

Automatically generate advanced forms with Formly. Part one of the blog series about automating.

Nearly every programmer has experienced long and confusing code at least once in their lives. Such complexity often happens naturally during development, by adding new code to improve the product. Additionally, when new people start working on a piece of code, it can be very hard for them to navigate. One excellent solution to such complexity issues is the reduction of source code by auto-generating everything that is just boilerplate. What remains is shorter and cleaner code, just the behavior of your programs.

When it comes to front-end programming, one specific area that is worth auto-generating are forms. Nowadays, applications usually contain many forms, which should be held consistent in their looks, usage and behavior. This makes them perfect candidates for auto-generation. In Angular a well-working way to auto-generate forms is using Formly, which is open-source and easy to use. Formly provides all the features needed for forms and can be extended with custom field types, custom validations and wrappers.

While the official documentation of Formly provides detailed explanations on its usage, getting into more complex features and use cases often gets progressively harder. Let’s take a look at how we at Symflower approached auto-generating forms with Formly, and how it can help you to auto-generate your own as well!

Getting started with Formly

It is worth noting that the official examples section of Formly does an excellent job at describing your basic workflow. We recommend taking a look at these examples before moving on, as they are incredibly useful on their own.

On to more complex examples: one neat trick here is to look at GitHub issues of Formly. There are already a wide variety of questions about more complex form features, where the developers working on their own setups provided great solutions.

Now that we have some good examples, let’s start working with Formly: the getting started guide can help you with the installation process.

Ready to implement some auto-generation? How about we take a look at an example of a login page implemented with Formly? First, we will create a relatively simple login form, which helps us introduce some basic Formly features and custom validation. Later on, we will try to add more complex features to our form and see how to implement features that are not directly provided by Formly.

The whole project for this example is open source and can be found in our tutorial repository. Give the repository a star, if you like this blog post. We highly appreciate it.

Setting up a login form

Our login form needs two fields, an email field and a password field, as well as a login button. It could look the following way.

Basic login form with an email and a password field and a login button.

To implement this form, we first need a form component. It defines the outline of a basic form with one submit button.

import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FormlyFieldConfig } from '@ngx-formly/core';
import { FormAttributes } from '../../models/form-attributes.model';
import { Form } from '../../models/form.model';
import { FormsService } from '../../services/forms.service';

@Component({
    selector: 'sym-form',
    styleUrls: ['./form.component.scss'],
    templateUrl: './form.component.html',
})
export class FormComponent implements OnInit {
    @Input()
    public formAttributes: FormAttributes;

    @Output()
    public submitted = new EventEmitter<any>();

    formGroup: FormGroup = new FormGroup({});

    fields: FormlyFieldConfig[] = [];

    constructor(private formsService: FormsService) {}

    ngOnInit(): void {
        if (this.formAttributes.formType) {
            this.formsService.getForm(this.formAttributes.formType, this.formAttributes.model).subscribe((form: Form) => {
                this.fields = form.fields;
            });
        }
    }

    submit() {
        this.submitted.emit(this.formAttributes.model);
    }
}

<form class="form">
	<formly-form
	  [form]="formGroup"
	  [model]="formAttributes.model"
	  [fields]="fields"
	>
	</formly-form>
	<div *ngIf="formAttributes.buttonText">
	  <button
		class="button-submit btn btn-success w-100"
		type="submit"
		[disabled]="!formGroup.valid"
		(click)="submit()"
	  >
		{{ formAttributes.buttonText }}
	  </button>
	</div>
  </form>

Next, a login page needs to be defined. It displays the form and handles submitting all inputs.

<sym-form
  class="form"
  [formAttributes]="formAttributes"
  (submitted)="submit($event)"
>
</sym-form>

import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { FormAttributes } from '../models/form-attributes.model';
import { FormType } from '../models/forms.enum';
import { LoginFormModel } from '../models/login-form.model';

@Component({
    selector: 'sym-login',
    styleUrls: ['./login.component.scss'],
    templateUrl: './login.component.html',
})
export class LoginComponent implements OnInit {
    formAttributes: FormAttributes;
    loginFormModel: LoginFormModel;

    constructor(private title: Title) {}

    ngOnInit() {
        this.title.setTitle('Login');

        this.loginFormModel = new LoginFormModel();
        this.formAttributes = {
            buttonText: 'Login',
            formType: FormType.LOGIN,
            model: this.loginFormModel,
        };
    }

    submit(formModel: LoginFormModel) {
        console.log('Successfully logged in');
    }
}

The following code shows all the models for our project. The login form model defines variables for the input field values. The form attributes model contains the attributes for the form component. They can also be added separately to the form, but this a another step towards shorter and cleaner code.

import { FormType } from './forms.enum';

export interface FormAttributes {
    buttonText: string;
    formType: FormType;
    model: any;
}

export enum FormType {
    LOGIN = 'login',
}

import { FormlyFieldConfig } from '@ngx-formly/core';

export class Form {
    fields: FormlyFieldConfig[];

    constructor(fields: FormlyFieldConfig[]) {
        this.fields = fields;
    }
}

export class LoginFormModel {
    email: string;
    password: string;

    constructor() {
        this.email = '';
        this.password = '';
    }
}

Next are all the service files. This also contains the login.ts file which defines the Formly schema. The forms service selects wich form should be displayed.

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Form } from '../models/form.model';
import { FormType } from '../models/forms.enum';
import { Login } from './login';

@Injectable({ providedIn: 'root' })
export class FormsService {
    getForm(form: FormType, model: any): Observable<Form> {
        switch (form) {
            case FormType.LOGIN:
                return of(new Form(Login.getLoginFormConfig()));
            default:
                throw new Error('Fields for the given form ' + form + ' does not exist!');
        }
    }
}

import { FormControl } from '@angular/forms';
import { FormlyFieldConfig } from '@ngx-formly/core';
import { EmailValidator } from './custom-validator.service';

export class Login {
    public static getLoginFormConfig(): FormlyFieldConfig[] {
        return [
            {
                className: 'input-email',
                focus: true,
                key: 'email',
                templateOptions: {
                    placeholder: 'you@company.com',
                    required: true,
                    type: 'email',
                },
                type: 'input',
                validation: {
                    messages: {
                        required: `Please enter an email address.`,
                    },
                },
                validators: {
                    email: {
                        expression: (c: FormControl) => EmailValidator(c),
                        message: 'Please enter a valid email address.',
                    },
                },
            },
            {
                className: 'input-password',
                key: 'password',
                templateOptions: {
                    minLength: 5,
                    placeholder: 'password',
                    required: true,
                    type: 'password',
                },
                type: 'input',
                validation: {
                    messages: {
                        minlength: `The password has to be longer.`,
                        required: `Please enter a password.`,
                    },
                },
            },
        ];
    }
}

Lastly, we need to add some imports to our app module and define some styles for a pleasant design.

import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { FormlyBootstrapModule } from '@ngx-formly/bootstrap';
import { FormlyModule } from '@ngx-formly/core';
import { AppComponent } from './app.component';
import { LoginComponent } from './pages/login.component';
import { FormComponent } from './components/form/form.component';

@NgModule({
    bootstrap: [AppComponent],
    declarations: [AppComponent, FormComponent, LoginComponent],
    imports: [
        BrowserModule,
        FormlyBootstrapModule,
        FormlyModule.forRoot({
            extras: { lazyRender: true, resetFieldOnHide: true },
        }),
        FormsModule,
        ReactiveFormsModule,
    ],
})
export class AppModule {}

@import '~bootstrap/dist/css/bootstrap.min.css';

.btn {
    margin-top: 1.5rem;
}

.btn:disabled {
    cursor: default;
}

body {
    background-color: #c5e1e2;
}

form {
    margin: 5rem;
}

input {
    margin-top: 1rem;
}

In the Formly schema, input field attributes, such as the type of the input field or the placeholder text, can be set. It is important that the key attribute has the same name as the variable of the login form model. For a login form, we might want to set the email field to be focused by default. This can easily be done by setting the “focus” attribute to true.

Standard validations, such as “required” or “minLength”, can be set in the template options. To define error messages for these validations, the variable messages in validation need to be set for the previously defined standard validation.

validation: {
     messages: {
        minLength: 'The password has to be longer.',
        required: 'Please enter a password.',
    },
},
Basic login form with error messages.

For custom validations, an extra attribute called “validators” needs to be added to the field. This contains an attribute defining the name of the validator, e.g. “email”. Here, we can define the expression that should be checked, as well as an error message. A function can be also defined for the expressions in a separate file.

validators: {
    email: {
        expression: (c: FormControl) => EmailValidator(c),
        message: 'Please enter a valid email address.',
    },
},
export function EmailValidator(control: FormControl): boolean {
  return control.value.match(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/);
}

With this implementation, we now have a pretty neat login form, with all the basic needs, like validations and error messages. The big advantage of this is that the form component and the form model can be reused for similar forms. Additionally, the custom validations can be used in all forms. We can still improve our login form with more advanced features, though — let’s see what we can do!

Implementing advanced Formly features

One very helpful feature for login forms is a show password button. The type of the password field gets changed from password to text and back to password on a second click. It could look like this:

Login form with a show password button.

We decided to implement the show password button by using an image with an eye icon that has the described functionality when clicking on it.

This, in itself, is more of a challenge when using Formly. Displaying an image inside of an input field is not directly possible, but we can use a trick to still be able to do it. For Formly, it is possible to combine fields defined in a Formly schema with HTML components. The first step in this is to create a new component defining a password field in HTML, which contains the wanted show password button.

import { Component } from '@angular/core';
import { FieldType } from '@ngx-formly/core';

@Component({
    selector: 'sym-password',
    styleUrls: ['./password.component.scss'],
    templateUrl: './password.component.html',
})
export class PasswordComponent extends FieldType {
    showPassword = false;

    public togglePasswordShown() {
        this.showPassword = !this.showPassword;
    }
}

<div class="input-group">
	<input
	  class="form-control"
	  [formControl]="formControl"
	  minlength="5"
	  required
	  placeholder="password"
	  [type]="showPassword ? 'text' : 'password'"
	  [formlyAttributes]="field"
	  [ngClass]="{ invalid: showError }"
	  i18n-placeholder="placeholder of the password input field"
	/>
	<img
	  alt="Show password"
	  class="icon-show input-icon pointer"
	  src="./assets/seePassword.svg"
	  (click)="togglePasswordShown()"
	  [ngClass]="{
		'd-none': showError
	  }"
	  i18n-alt="alternative text for the show password button"
	/>
	<img
	  alt="Show password error"
	  class="icon-show-error d-none input-icon pointer"
	  src="./assets/seePasswordError.svg"
	  (click)="togglePasswordShown()"
	  [ngClass]="{
		'd-block': showError
	  }"
	  i18n-alt="
		alternative text for the show password button when an error occured
	  "
	/>
  </div>

  <div
	*ngIf="formControl.touched"
	class="text-danger"
	i18n="notification that occures when the password is too short"
  >
	<formly-validation-message [field]="field"></formly-validation-message>
  </div>

.input-icon {
    left: auto;
    position: absolute;
    right: 10px;
    top: 10px;
    z-index: 2000;
}

import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { FormlyBootstrapModule } from '@ngx-formly/bootstrap';
import { FormlyModule } from '@ngx-formly/core';
import { AppComponent } from './app.component';
import { LoginComponent } from './pages/login.component';
import { FormComponent } from './components/form/form.component';
import { PasswordComponent } from './components/password/password.component';

@NgModule({
    bootstrap: [AppComponent],
    declarations: [AppComponent, FormComponent, LoginComponent],
    imports: [
        BrowserModule,
        FormlyBootstrapModule,
        FormlyModule.forRoot({
            extras: { lazyRender: true, resetFieldOnHide: true },
            types: [{ name: 'password', component: PasswordComponent }],
        }),
        FormsModule,
        ReactiveFormsModule,
    ],
})
export class AppModule {}

To use this field instead of the previously defined one, we can add a new attribute to the root attributes of the Formly module.

FormlyModule.forRoot({
    extras: { lazyRender: true, resetFieldOnHide: true },
    types: [{ name: 'password', component: PasswordComponent }],
}),

What’s going to be a little bit tricky for us here are the error messages for this field. In our example, the validations are still handled by Formly. The error messages can be displayed in the field component the following way.

<div *ngIf="formControl.touched" class="text-danger">
  <formly-validation-message [field]="field"></formly-validation-message>
</div>
Login form with a show password button and error messages.

The error message from Formly can be displayed in a stylized div. Formly checks its validations even if the input field was not touched. For required fields, a Formly validation message is immediately created after creating the form. Usually, this only gets displayed when the field is touched. In our case, we have to do this ourselves by only displaying the div, containing the error message, if the password field was touched.

With this approach, we are able to implement our intended show password button. This idea of using a separate HTML component, instead of the original Formly field, can be used whenever Formly does not provide a fitting solution for a complex feature. This makes it possible to have additional features and still use auto-generated forms.

Conclusion

Auto-generated code improves writing and maintaining code. This code is less intertwined with other code and similar parts can be easily reused. When it comes to forms, it makes writing long and complex HTML code obsolete, and therefore makes development not just simpler and faster, but especially consistent over your whole code base. Reusing code and better readability also lead to easier maintainable code.

Formly provides a well-working solution for form auto-generation in Angular. It is easy to implement, is open-source, and provides some special features. Even if something is not directly possible with Formly a solution is provided, by using a separate HTML component, as we’ve shown above.

If you are interested in more exciting blog posts about cleaner code and better testing, subscribe to our newsletter, and follow us on Twitter, Facebook, and LinkedIn.

Technical | 2022-03-08