Design Patterns — Builder in Angular

Bulicka Ola
Design Patterns — Builder in Angular
5 min readJul 20, 2024
Image generated by Copilot AI

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 theBuilderComponentcomponent.

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 NewDefinitionFormBuilderPatternclass is located in the providers array. During the instantiation of the class, the constructor will be replaced with the NewDefinitionFormBuilderPatternclass, which overrides the groupmethod and returns a newNewDefinitionFormGroupclass. The NewDefinitionFormGroupclass 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 FormBuilderclass 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:

  1. Java. Wzorce projektowe. Translation: Piotr Badarycz. Original title: Java design patterns. A Tutorial. Author: James William Cooper
  2. https://refactoring.guru/pl/design-patterns/builder

--

--