Design Patterns — Builder in Angular
Creational design patterns are concerned with the way of creating objects.
In JavaScript, TypeScript, etc., the way to create an object is by using the new operator.
Creational patterns consist of:
1. Factory (Simple Factory)
2. Factory Method
3. Abstract Factory
4. Singleton
5. Prototype
6. Builder
7. Object Pool
In this article, I will explain how the Builder pattern works. You can see and run an example of the Builder pattern in a demo project, which can be found at this link here.
As the name suggests, the Builder Design Pattern is used to model the construction process. In everyday life, we can build things like a house, an apartment block, a castle, or a structure. To build a house, certain tasks must be performed in a specific order and specific products must be used. In web systems, building can be reflected in creating a form with specific fields. If the validation is successful, the form data will be sent to the API. The Forms, as well as houses, may differ from each other.
Angular provides an implementation of the Builder pattern. It is the FormBuilder
class from the @angular/forms library. This class includes several methods such as group, record, control, and array, each of which creates a form control. To use FormBuilder
, you need to inject the class into the constructor:
@Component({
// ...
})export class BuilderComponent {
constructor(private formBuilder: FormBuilder) {}
}
or pass it to the inject function:
@Component({
// ...
})
export class BuilderComponent {
private formBuilder = inject(FormBuilder)
}
The structure of the Builder design pattern consists of several elements.
Interface — declares the construction stages of the product common to all types of builders.
Builders — provide different implementations of the construction stages. Specific builders can create products, that do not share a common interface. In the example of our form, these are field settings, etc.
Products — these are the objects used to build the form in our case. This can be a FormControl
, FormGroup
, FormArray
, or FormRecord
. Products constructed by different builders do not need to belong to the same class hierarchy or interface.
Director class — defines the order in which construction steps should be called to create the form in our case.
Client — must assign one of the builder objects to the director. In our case, this could involve specifying validation or setting options. The client determines the application of the form and can pass data from the form to other methods. In our case, this is theBuilderComponent
component.
The implementation of a custom FormBuilder can be as follows:
import { NgFor } from '@angular/common';
import { Component, inject } from '@angular/core';
import { AbstractControlOptions, FormArray, FormBuilder, FormGroup, ReactiveFormsModule,
ValidatorFn, AsyncValidatorFn, FormRecord, Validators } from '@angular/forms';
class NewDefinitionFormGroup extends FormGroup {
constructor(controls: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
super(controls);
console.info('New definition keys', Object.keys(controls));
}
}
export class NewDefinitionFormBuilderPattern extends FormBuilder {
override group<T extends {}>(controls: T, options?: AbstractControlOptions | null): FormGroup {
return new NewDefinitionFormGroup(controls);
}
}
@Component({
selector: 'app-builder',
standalone: true,
imports: [ReactiveFormsModule, NgFor],
templateUrl: './builder.component.html',
styleUrl: './builder.component.scss',
providers: [{
provide: FormBuilder,
useClass: NewDefinitionFormBuilderPattern
}]
})
export class BuilderComponent {
private formBuilder = inject(FormBuilder)
radios: string[] = ['Male', 'Female'];
selects: string[] = ['USA', 'Canada', 'UK', 'Australia', 'Other'];
formBuilderGroup: FormGroup = this.formBuilder.group({
name: this.formBuilder.control('', [Validators.maxLength(10)]),
number: this.formBuilder.control(0, { nonNullable: true, }),
group: this.formBuilder.group({
groupName: this.formBuilder.control('', [Validators.required]),
}),
groupRadioSelectArrayChcekBox: this.formBuilder.group({
radio: this.formBuilder.control(''),
select: this.formBuilder.control(''),
checkBox: this.formBuilder.array([
this.createOption('checkBox 1'),
this.createOption('checkBox 2'),
this.createOption('checkBox 3')
])
}),
record: this.formBuilder.array([this.createRecord()])
});
createOption(defaultvalue: string, selected = false) {
return this.formBuilder.group({
selected: this.formBuilder.control(selected),
value: this.formBuilder.control(defaultvalue)
})
}
get checkBox(): FormArray {
return this.formBuilderGroup.get('groupRadioSelectArrayChcekBox.checkBox') as FormArray;
}
get record(): FormArray {
return this.formBuilderGroup.get('record') as FormArray;
}
createRecord(): FormRecord {
return this.formBuilder.record({
key: this.formBuilder.control(''),
value: this.formBuilder.control(''),
});
}
addRecord(): void {
this.record.push(this.createRecord());
}
save() {
console.log(this.formBuilderGroup);
}
reset() {
this.formBuilderGroup.reset()
}
}
The integration of the NewDefinitionFormBuilderPattern
class is located in the providers
array. During the instantiation of the class, the constructor will be replaced with the NewDefinitionFormBuilderPattern
class, which overrides the group
method and returns a newNewDefinitionFormGroup
class. The NewDefinitionFormGroup
class displays the form control keys in the console.
The template for the form structure with attached controls is as follows:
<div class="container">
<form [formGroup]="formBuilderGroup" (ngSubmit)="save()">
<div class="form-row">
<label class="form-label" for="name">Name:</label>
<input class="form-input" type="text" id="name" name="name"
formControlName="name">
</div>
<div class="form-row">
<label class="form-label" for="number">number:</label>
<input class="form-input" type="number" id="number" name="number"
formControlName="number">
</div>
<div class="form-row" formGroupName="group">
<label class="form-label" for="groupName">Group name:</label>
<input class="form-input" type="text" id="groupName"
formControlName="groupName">
</div>
<div class="form-group" formGroupName="groupRadioSelectArrayChcekBox">
<label class="form-label">Radios:</label>
<div *ngFor="let radio of radios">
<input class="form-input" type="radio" [value]="radio"
formControlName="radio">{{ radio }}
</div>
<div class="form-row">
<label class="form-label" for="select">Select:</label>
<select class="form-input" id="select"
formControlName="select">
<option *ngFor="let select of selects" [value]="select">
{{ select }}
</option>
</select>
</div>
<div class="form-group">
<label class="form-label">CheckBox: </label>
<div formArrayName="checkBox" *ngFor="let item of checkBox.controls; let i = index">
<div [formGroupName]="i">
<input class="form-input" type="checkbox" formControlName="selected">
{{ item.value.value }}
</div>
</div>
</div>
</div>
<div class="form-group">
<div formArrayName="record">
<label class="form-label">Rrecord: </label>
<button type="submit" (click)="addRecord()">
+ Add another record
</button>
@for ( record of record.controls; track record; let i = $index) {
<div class="form-row" [formGroupName]="i">
<label class="form-label" for="key-{{ i }}">Key:</label>
<input class="form-input" id="key-{{ i }}" type="text"
formControlName="key">
<label class="form-label" for="value-{{ i }}">Value:</label>
<input class="form-input" id="value-{{ i }}" type="text"
formControlName="value">
</div>
}
</div>
</div>
<button type="submit">Submit</button>
<button type="submit" (click)="reset()">reset</button>
</form>
</div>
The form structure is just a suggestion. The FormBuilder
class provides extensive methods and options for form functionality. The proposed validation methods can be combined and validated with multiple controls.
Below are the proposed SCSS styles built using Flexbox properties:
.container {
display: flex;
flex-direction: column;
align-items: center;
}
form {
width: 100%;
max-width: 700px;
display: flex;
flex-direction: column;
}
.form-row {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
.form-label {
flex: 2;
text-align: right;
margin-right: 10px;
}
.form-input {
flex: 3;
}
}
.form-group {
margin-bottom: 20px;
}
Summary
The Builder pattern allows changing the internal representation of the object being constructed while hiding the construction details. Each builder is independent of other builders and the rest of the system. It enhances modularity and allows for easy addition of new builders.
Materials:
- Java. Wzorce projektowe. Translation: Piotr Badarycz. Original title: Java design patterns. A Tutorial. Author: James William Cooper
- https://refactoring.guru/pl/design-patterns/builder
My other articles related to Design Patterns in Angular:
Structural patterns:
Design Patterns — Proxy in Angular
Design Patterns — Composite in Angular
Design Patterns — Adapter in Angular
Design Patterns — Decorator in Angular
Creational patterns:
Design Patterns — Factory (Simple Factory) in Angular
Behavioral patterns:
Design Patterns — Chain of Responsibility in Angular
Design Patterns — Memento in Angular
I’m not good at English. I write articles to practice English.