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.
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 fieldplaceholder
– Placeholder of the input fieldtype
– Type of the input field. It could be number, text, etcmin
– Minimum value of the input fieldmax
– Maximum value of the input fieldunitControlName
– Control name of the drop down listunitPlaceholder
– Placeholder of the drop down listlist
– 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:
- Github Repository: https://github.com/railsstudent/ng-simple-cssgrid-generator
- FormControl: https://angular.io/api/forms/FormControl