Design Patterns — Decorator in Angular
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 Notifications
class 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 BaseDecorator
class. 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:
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 the
DecoratorComponent
component. - Component: Declares the
Notifier
interface, which is common to both the wrappers and the wrapped objects. - Concrete Component: The
BaseDecorator
class 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
andNotifierDecoratorService
.
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.
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
Creational patterns:
Design Patterns — Builder in Angular
Design Patterns — 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.