Refactor Reactive Form with components in Angular

Reading Time: 5 minutes

Loading

Introduction

The simple CSS grid generator has three reactive forms while these forms have material components that behave similarly. In grid form. Grid gap is a number input field and Gap unit is a drop down list. Similarly, Grid Column Gap and Grid Column Unit are number input field and drop down list respectively. We can refactor the reactive form with components to encapsulate the behavior and eliminate duplicate codes.

The component encapsulates an input field and a drop down list. It also accepts inputs from parent component and passes the values to the input controls to populate data.

In this post, we are going to create a new component consisted of two input controls and render it in the reactive form. Furthermore, we can repeat the same exercise to reuse the same component in grid template columns and grid template rows forms.

let's go

Define the interface for the new component

First, I define an interface to store the data that the input field and drop down list require.

export interface CompositeFieldDropdownConfiguration {
    controlName: string
    placeholder: string
    type: string
    min?: number | string
    max?: number | string
    unitControlName: string
    unitPlaceholder: string
    list: { value: string; text: string }[]
}
  • controlName – Control name of the input field
  • placeholder – Placeholder of the input field
  • type – Type of the input field. It could be number, text, etc
  • min – Minimum value of the input field
  • max – Maximum value of the input field
  • unitControlName – Control name of the drop down list
  • unitPlaceholder – Placeholder of the drop down list
  • list – list items of the drop down list

Build the new component for the reactive form

Next, we begin to build the new AppGridValueFieldComponent component. AppGridValueFieldComponent accepts a FormGroup input that is used to retrieve form control by form control name. Then, we can bind the form control to the HTML input element. It also has an CompositeFieldDropdownConfiguration input in order to pass the property values to the attributes of the HTML input elements.

@Component({
    selector: 'app-grid-value-field',
    ...
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppGridValueFieldComponent implements OnInit {
    @Input()
    formGroup: FormGroup

    @Input()
    fieldConfiguration: CompositeFieldDropdownConfiguration

    formControl: FormControl
    unitFormControl: FormControl

    ngOnInit() {
        const { controlName, unitControlName } = this.fieldConfiguration
        this.formControl = this.formGroup.get(controlName) as FormControl
        this.unitFormControl = this.formGroup.get(unitControlName) as FormControl
    }
}

In ngOnInit, we use controlName and unitControlName to look up the form controls in formGroup and assign them to instance members. Next, we register these instance members in the inline template.

Add inline template and style in the AppGridValueFieldComponent component

@Component({
    selector: 'app-grid-value-field',
    template: `
        <ng-container [formGroup]="formGroup">
            <mat-form-field appControlErrorContainer>
                <input
                    matInput
                    [formControl]="formControl"
                    [type]="fieldConfiguration.type || 'number'"
                    [placeholder]="fieldConfiguration.placeholder"
                    [min]="fieldConfiguration.min ?? null"
                    [max]="fieldConfiguration.max ?? null"
                />
            </mat-form-field>
            <mat-form-field>
                <mat-select [placeholder]="fieldConfiguration.unitPlaceholder" [formControl]="unitFormControl">
                    <mat-option *ngFor="let item of fieldConfiguration.list" [value]="item.value">
                        {{ item.text }}
                    </mat-option>
                </mat-select>
            </mat-form-field>
        </ng-container>
    `,
    styles: [
        `
            :host {
                display: block;
            }

            mat-form-field {
                margin-right: 0.5rem;
            }
        `,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush,
})

Then, we add inline template and inline styles to AppGridValueFieldComponent. <ng-container [formGroup]=”formGroup”> is required in order to register formControl instance. For the input field, we can optionally set type, placeholder, min value and max value. For the drop down list, we can optionally set placeholder and populate list items.

As the result, this is the complete code listing of the AppGridValueFieldComponent component

import { FormControl, FormGroup } from '@angular/forms'
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'
import { CompositeFieldDropdownConfiguration } from './appgrid-value-field.interface'

@Component({
    selector: 'app-grid-value-field',
    template: `
        <ng-container [formGroup]="formGroup">
            <mat-form-field appControlErrorContainer>
                <input
                    matInput
                    [formControl]="formControl"
                    [type]="fieldConfiguration.type || 'number'"
                    [placeholder]="fieldConfiguration.placeholder"
                    [min]="fieldConfiguration.min ?? null"
                    [max]="fieldConfiguration.max ?? null"
                />
            </mat-form-field>
            <mat-form-field>
                <mat-select [placeholder]="fieldConfiguration.unitPlaceholder" [formControl]="unitFormControl">
                    <mat-option *ngFor="let item of fieldConfiguration.list" [value]="item.value">
                        {{ item.text }}
                    </mat-option>
                </mat-select>
            </mat-form-field>
        </ng-container>
    `,
    styles: [
        `
            :host {
                display: block;
            }

            mat-form-field {
                margin-right: 0.5rem;
            }
        `,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppGridValueFieldComponent implements OnInit {
    @Input()
    formGroup: FormGroup

    @Input()
    fieldConfiguration: CompositeFieldDropdownConfiguration

    formControl: FormControl
    unitFormControl: FormControl

    ngOnInit() {
        const { controlName, unitControlName } = this.fieldConfiguration
        this.formControl = this.formGroup.get(controlName) as FormControl
        this.unitFormControl = this.formGroup.get(unitControlName) as FormControl
    }
}

Refactor reactive form to use the AppGridValueFieldComponent component

The refactoring of the reactive form begins with the declaration of new members to hold the configurations of Gap and Gap Column.

// AppgridFormComponent

gapConfiguration: CompositeFieldDropdownConfiguration
gapColConfiguration: CompositeFieldDropdownConfiguration

We continue to configure the values consumed by gap and gap column.

this.gapConfiguration = {
     controlName: 'gap',
     placeholder: 'Grid Gap',
     type: 'number',
     min: 0,
     max: 20,
     unitControlName: 'gapUnit',
     unitPlaceholder: 'Gap Unit',
     list: this.gapUnits.map((unit) => ({ text: unit, value: unit })),
}

this.gapColConfiguration = {
     controlName: 'gapCol',
     placeholder: 'Grid Column Gap',
     type: 'number',
     min: 0,
     max: 20,
     unitControlName: 'gapColUnit',
     unitPlaceholder: 'Gap Column Unit',
     list: this.gapColUnits.map((unit) => ({ text: unit, value: unit })),
}

After the setup, we are ready to refactor the reactive form in the template to use AppGridValueFieldComponent, gapConfiguration and gapColConfiguration.

<ng-container class="dimensions" [formGroup]="form">
     ... other material form fields ...
    <app-grid-value-field
        [formGroup]="form"
        [fieldConfiguration]="gapConfiguration"
    ></app-grid-value-field>
    <app-grid-value-field
        *ngIf="numGapLengths.value === 2"
        [formGroup]="form"
        [fieldConfiguration]="gapColConfiguration"
    ></app-grid-value-field>
    ... other component ...
</ng-container>

The template hides the details in components and results to shorter and maintainable markup.

Refactor reactive form of grid template rows and grid template columns forms

Finally, we apply the same process to refactor the reactive form of grid template rows and grid template column forms. The goal is to replace min value and max value with the reusable component.

Similarly, we declare and initialize new instance members in template form component.

// AppTemplateFormComponent

minWidthConfiguration: CompositeFieldDropdownConfiguration
maxWidthConfiguration: CompositeFieldDropdownConfiguration

this.minWidthConfiguration = {
    controlName: 'minWidth',
    placeholder: 'Min value',
    type: 'number',
    min: 1,
    unitControlName: 'minUnit',
    unitPlaceholder: 'Unit',
    list: [],
}

this.maxWidthConfiguration = {
     controlName: 'maxWidth',
     placeholder: 'Max value',
     type: 'number',
     min: 1,
     unitControlName: 'maxUnit',
     unitPlaceholder: 'Unit',
     list: this.units.map((unit) => ({ text: unit, value: unit })),
}

The next step is to get rid of the input fields of min value and max value and their sibling drop down lists from the template.

<ng-container [formGroup]="form">
    <fieldset class="settings">
        .. omitted for brevity ....
        <section class="section-width">
            <app-grid-value-field
                [formGroup]="form"
                [fieldConfiguration]="minWidthConfiguration"
            ></app-grid-value-field>
            <app-grid-value-field
                *ngIf="minmax.value === 'true'"
                [formGroup]="form"
                [fieldConfiguration]="maxWidthConfiguration"
            >
            </app-grid-value-field>
        </section>
    </fieldset>
</ng-container>

As the result, we have rebuilt reactive forms with customized and resusable component. When we relaunch the application, the behavior of these reactive forms should stay the same.

Final thoughts

In this post, we have seen one way of creating component to use in a reactive form. The component is reusable in any form group and we have seen it in AppgridForm, Grid Template Rows and Grid Template Columns forms.

This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.

Resources:

  1. Github Repository: https://github.com/railsstudent/ng-simple-cssgrid-generator
  2. FormControl: https://angular.io/api/forms/FormControl