Design Patterns — Decorator in Angular

Image generated by Copilot AI

Structural patterns describe ways to combine classes and objects into larger structures.

Structural patterns consist of:
1. Adapter
2. Composite
3. Proxy
4. Flyweight
5. Facade
6. Bridge
7. Decorator

In this article, I will explain how the Decorator pattern works. You can see and run an example of the Decorator pattern in a demo project, which can be found at this link here.

In Angular, there are various types of decorators, each serving different purposes. Below are the most important ones:

1. Class Decorators
— @Component: A decorator used to define Angular components. It specifies metadata such as the HTML template, styles, component selector, and more.
— @Directive: A decorator used to define Angular directives that modify the appearance or behavior of DOM elements.
— @Pipe: A decorator that defines pipes in Angular, which transform data in the template.
— @Injectable: A decorator used to mark classes that can be injected (dependency injection). Typically used for services.
— @NgModule: A decorator used to define Angular modules. It contains metadata about the components, directives, pipes, and other modules used in the application.

2. Property Decorators
@Input: Decorator that marks a class field as an input property and supplies configuration metadata. The input property is bound to a DOM property in the template. During change detection, Angular automatically updates the data property with the DOM property’s value.
@Output: Decorator that marks a class field as an output property and supplies configuration metadata. The DOM property bound to the output property is automatically updated during change detection.
@HostBinding: Decorator that marks a DOM property or an element class, style or attribute as a host-binding property and supplies configuration metadata. Angular automatically checks host bindings during change detection, and if a binding changes it updates the host element of the directive.
@HostListener: Decorator that declares a DOM event to listen for, and provides a handler method to run when that event occurs.

3. Parameter Decorators
— @Attribute: It ensures the passing of static values. The decorator is placed in the constructor, where the parameter specifies the name of the element on the component.
— @Inject: It allows manual dependency injection into the class constructor as a parameter, creating a singleton, meaning a service that exists as a single instance within the application.
— @Optional: A decorator that indicates a dependency can be optional, and Angular won’t throw an error if it’s not found.
— @Self: A decorator that restricts dependency injection to the local injector (without delegating to parent injectors).
— @SkipSelf: A decorator that skips the local injector and looks for a dependency in the parent injector.
— @Host: A decorator that restricts dependency injection to the host component.

4. Method Decorators
— @ContentChild: A decorator that allows access to a single DOM element, @Component, or @Directive declared in content projection. In the parameter, you need to provide the reference name or type you want to access. It is important to remember that access to the component or element is only available once it has been rendered. Therefore, access can be obtained in the methods defined in the component lifecycle, such as ngAfterContentInit() or ngAfterContentChecked().
— @ContentChildren: A decorator that provides access to DOM elements, @Component, or @Directive declared in content projection as an immutable QueryList. In the parameter, you need to provide the reference name or type you want to access. It is important to remember that access to the component or element is available only after it has been rendered. Therefore, access can be obtained in the methods defined in the component lifecycle, such as ngAfterContentInit() or ngAfterContentChecked()
— @ViewChild: A decorator that allows access to a single DOM element, @Component or @Directive. The parameter should specify the reference name or the type you want to access. It should be noted that access to the component or element is available only after it has been rendered. Therefore, access can be obtained in methods defined in the component’s lifecycle, such as ngAfterViewInit() lub ngAfterViewChecked().
— @ViewChildren: A decorator that allows access to DOM elements, @Component or @Directive as an immutable list of type QueryList. You need to provide the reference name or the type you want to access in the parameter. It should be noted that access to the component or element is available only after it has been rendered. Therefore, access can be obtained in methods defined in the component’s lifecycle, such as ngAfterViewInit() lub ngAfterViewChecked()

These decorators are essential for creating and managing components, directives, services, and other aspects of an Angular application. Each one serves a specific function, allowing for effective utilization of Angular’s capabilities.

A decorator is a structural pattern that allows adding new responsibilities to objects dynamically by ‘wrapping’ them in special objects that provide the needed functionality. Also known as: Overlay, Wrapper.

When we want the user to receive notifications about events, it’s enough to implement the NotifierComponent class, which has a notificationContent field and a method that sends a message as an argument to the NotifierService:

Below is an example implementation:

import { Component } from '@angular/core';
import { NotifierService } from './service/notifier.service';
import { FormsModule } from '@angular/forms';

@Component({
selector: 'app-decorator',
standalone: true,
imports: [FormsModule],
providers: [NotifierService],
template: `
<input type="text"
(ngModelChange)="notificationContent = $event"
[ngModel]="notificationContent"/>
<button (click)="sendNotification(notificationContent)">Notify</button>
`,
styleUrl: './decorator.component.scss'
})
export class DecoratorComponent {

notificationContent = ''
constructor(private notifier: NotifierService) { }

sendNotification(message: string): void {
this.notifier.send(message);
}
}

Service:

import { Injectable } from '@angular/core';

@Injectable({
providedIn: 'root'
})
export class NotifierService {
send(message: string): void {
console.info(`Notifier: ${message}`);
}
}

In case the user wants to add sending notifications to platforms like Facebook or Slack, it’s enough to add the methods sendNotificationToFB and sendNotificationToSlack in the NotifierService.

In case the user wants to add sending notifications to platforms like Facebook or Slack, it’s enough to add the methods sendNotificationToFB and sendNotificationToSlack in the NotifierService.

Moreover, having different notifications like Facebook or Slack, the user might want the ability to send all possible notification options.

By adding such functionality, you can extend it with a Notification class, which can be inherited by services like NotifierFBService, NotifierSlackService, and NotifierService, where methods for sending notifications through various options are implemented. The passing of these services is done using the Dependency Injection mechanism. However, this makes the code less readable, so a different approach to structuring it is needed.

A way to avoid the complexity of combining notification options can be achieved using the decorator design pattern.

The Decorator is also known as a Wrapper. This term effectively conveys the main idea of this pattern. The Notifications class, which implements the Notifier interface, serves as the wrapper. Below is the code for the Notificationsclass and theNotifier interface:

export interface Notifier {
send(message: string): void;
}

export class Notifications implements Notifier {
send(message: string): void {
console.info(`Messages with the content have been sent: ${message}`);
}
}

The wrapper is an object that holds a reference to the wrapped object, using either aggregation or composition. In our case, aggregation of the Notifier type is used, delegating all received requests to it. Below is the base decorator that has a reference to Notifier:

import { Notifier } from '../notifier';

export class BaseDecorator implements Notifier {
protected wrappee: Notifier;

constructor(wrappee: Notifier) {
this.wrappee = wrappee;
}

send(message: string): void {
console.info('BaseDecoratorService');
this.wrappee.send(message);
}
}

The classes SlackDecoratorService, FacebookDecoratorService, and NotifierDecoratorService inherit from the BaseDecoratorclass. These classes can add services that communicate with an API of a server sending requests. They are concrete decorators defining additional behaviors that can be dynamically assigned to components. Below is the implementation of the SlackDecoratorService, FacebookDecoratorService and NotifierDecoratorService classes:

import { BaseDecorator } from '../baseDecorator/base-decorator.service';

export class SlackDecoratorService extends BaseDecorator {

override send(message: string) {
console.info('sendSlack')
super.send(message)
}
}
import { BaseDecorator } from '../baseDecorator/base-decorator.service';

export class FacebookDecoratorService extends BaseDecorator {

override send(message: string) {
console.info('sendFacebook')
super.send(message)
}
}
import { BaseDecorator } from '../baseDecorator/base-decorator.service';

export class NotifierDecoratorService extends BaseDecorator {

override send(message: string) {
console.info('sendNotifier')
super.send(message)
}
}

The application of the decorator pattern is in the component DecoratorComponent:

import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NgFor } from '@angular/common';
import { SlackDecoratorService } from './service/slack/slack-decorator.service';
import { NotifierDecoratorService } from './service/notifier/notifier-decorator.service';
import { FacebookDecoratorService } from './service/facebook/facebook-decorator.service';
import { Notifications, Notifier } from './service/notifier';


@Component({
selector: 'app-decorator',
standalone: true,
imports: [FormsModule, NgFor],
template: `
<ul>
<li *ngFor="let item of list">
<input type="checkbox" [(ngModel)]="item.checked">{{item.title}}
</li>
</ul>

<input type="text" (ngModelChange)="notificationContent = $event" [ngModel]="notificationContent" />
<button (click)="sendNotification(notificationContent)">Notify</button>
`,
styleUrl: './decorator.component.scss'
})
export class DecoratorComponent {

list = [{
id: 1,
title: 'Facebook',
checked: true,
}, {
id: 2,
title: 'Slack',
checked: false,
}, {
id: 3,
title: 'Message',
checked: true,
},
]
notificationContent = ''
notifier: Notifier = new Notifications()

sendNotification(message: string): void {
let decoratedNotifier = this.notifier;

if (this.list[0].checked) {
decoratedNotifier = new FacebookDecoratorService(decoratedNotifier);
}
if (this.list[1].checked) {
decoratedNotifier = new SlackDecoratorService(decoratedNotifier);
}
if (this.list[2].checked) {
decoratedNotifier = new NotifierDecoratorService(decoratedNotifier);
}

decoratedNotifier.send(message);
}
}

The wrapper is created using new Notifications() , to which the notification objects SlackDecoratorService, FacebookDecoratorService and NotifierDecoratorService are appropriately added. The resulting object will have a stack-like structure. The last decorator in the stack will be the object that the client actually interacts with.

Below is a print screen of the directory structure of an example of using the decorator design pattern:

directory structure of an example of using the decorator design pattern

The structure of the Decorator pattern consists of several elements. The example implementation reflects these elements:

  • Client: The client can wrap components in multiple layers of decorators, as long as it interacts with all objects through the component interface. In our case, this is theDecoratorComponent component.
  • Component: Declares theNotifier interface, which is common to both the wrappers and the wrapped objects.
  • Concrete Component: TheBaseDecoratorclass wraps objects. It defines basic behavior that can then be modified through decorators by inheriting the concrete component.
  • Base Decorator Class: Contains a field intended for a reference to the wrapped object. In our case, aggregation is used in the BaseDecorator class. The field type should be declared as the component interface so that it can hold both concrete components and other decorators.
  • Concrete Decorators: Define additional behaviors that can be dynamically assigned to components, such as services that send requests. Concrete decorators override the methods of the base decorator and perform their actions either before or after invoking the parent class method. In our case, these are SlackDecoratorService, FacebookDecoratorService and NotifierDecoratorService.

Summary

The Decorator is useful for mechanisms that create various combinations. It allows adding modifications without the need to create a complex inheritance hierarchy. However, a drawback of the decorator pattern is the introduction of many small objects, which can be challenging to maintain.

--

--