Decorators in JavaScript
February 23rd, 2020
In web development, JavaScript is considered the most recommended language to build user interfaces that are highly complex, which can be coming from various needs especially coming from business requirements. And in this article we will be going over a useful pattern in JavaScript called decorators.
Decorators are objects you can use to dynamically add additional functionality to another object, without having to change the implementation of that object. Just from understanding that definition we can most likely come to an agreement that they can become useful to our app code.
If you were like me, they might be a little confusing at first especially since in TypeScript the syntax was out of the ordinary. It doesn't quite feel like JavaScript to be applying decorators to classes when applying them using the modern syntax (Currently upported in TypeScript and in babel plugins).
Here's an example of that in use:
@filterMales // This is the decorator
class MyClass {
constructor(children) {
this.children = children
}
}
Those of you who have never seen this type of code (specifically the @filterMales
syntax) might feel a little frightened of decorators when realizing that this is applying a decorator. Decorators this way are just syntax sugar. Understanding and implementing a decorator might be easier than you think. If you've been developing in JavaScript for awhile, you've probably already implemented a decorator without even noticing it. They're simple but powerful.
We'll be taking a look at some examples of decorators in JavaScript and create our own decorator to see how it can be useful to our code.
Fortunately there are multiple ways a decorator can be useful to us.
As previously mentioned, one scenario that can be very useful is when you need to dynamically add additional logic to objects without having to deal with some alternatives (like subclassing or inheritance).
Remember this: decorators can inject stuff into objects without the outside world even knowing how they're going to do it.
For example, lets say we have a Frog
class that will implement a method called lick
. Frogs have teeth so we'll also randomly implement a getTeeths
method to return the amount of teeths they have.
Here's what that may look like:
function Frog(name) {
this.name = name
}
Frog.prototype.getTeeths = function() {
return 2
}
Frog.prototype.lick = function(target) {
console.log(`I'm going lick you, ${target.name}. You better taste delicious`)
}
// Or with classes
class Frog {
constructor(name) {
this.name = name
}
getTeeths() {
return 2
}
lick(target) {
console.log(
`I'm going lick you, ${target.name}. You better taste delicious`,
)
}
}
In reality there are different frogs, like a toad for example. A toad is still a frog but a frog is not a toad which means that there must be some differentiating features between them that must not be mixed.
Since a toad is a frog, we can build a withToad
decorator that will decorate an instance of a frog if desired so that it can represent toads.
Remember, a decorator should only extend or add additional behavior to something but not change its implementation.
Knowing this, the withToad
decorator is actually quite simple:
function withToad(frog) {
frog.getTeeths = function() {
return 0
}
}
const mikeTheFrog = new Frog('mike')
withToad(mikeTheFrog)
console.log(mikeTheFrog.getTeeths())
Our decorator withToad
re-implements getTeeths
so that it returns 0
because toads do not have teeth. When we use this decorator we're essentially silently decorating (converting in this case) a frog to represent a frog that is a toad.
You can achieve the same goal using subclassing with inheritance as shown below:
function Toad(name) {
Frog.call(this, name)
this.getTeeths = function() {
return 0
}
}
const kellyTheToad = new Toad('kelly')
// or using classes
class Toad extends Frog {
getTeeths() {
return 0
}
}
const kellyTheToad = new Toad('kelly')
The difference between the two approaches is that by using decorators you don't have to create classes for toads.
Our examples showed how decorators were used to manipulate a frog to be more aligned with the features of a toad.
Let's now look at a better example of how we can use decorators to extend functionality. This is where things begin to get a little interesting.
Lets pretend we're building an app that supports various custom predefined themes for users to style their control panel. We'll implement a Theme
with the method createStylesheet
to create a compatible stylesheet to work with, an applyStyles
method to parse and apply this stylesheet to the DOM, allowing itself to call applyStyle
to apply them to the DOM:
function Theme() {}
Theme.prototype.createStylesheet = function() {
return {
header: {
color: '#333',
fontStyle: 'italic',
fontFamily: 'Roboto, sans-serif',
},
background: {
backgroundColor: '#fff',
},
button: {
backgroundColor: '#fff',
color: '#333',
},
color: '#fff',
}
}
Theme.prototype.applyStylesheet = function(stylesheet) {
const bodyElem = document.querySelector('body')
const headerElem = document.getElementById('header')
const buttonElems = document.querySelectorAll('button')
this.applyStyles(bodyElem, stylesheet.background)
this.applyStyles(headerElem, stylesheet.header)
buttonElems.forEach((buttonElem) => {
this.applyStyles(buttonElem, stylesheet.button)
})
}
Theme.prototype.applyStyles = function(elem, styles) {
for (let key in styles) {
if (styles.hasOwnProperty(key)) {
elem.style[key] = styles[key]
}
}
}
Things are looking great. We have now defined our Theme
API and now we can create a stylesheet like so:
const theme = new Theme()
const stylesheet = theme.createStylesheet()
Here is what stylesheet
currently looks like:
{
"header": {
"color": "#333",
"fontStyle": "italic",
"fontFamily": "Roboto, sans-serif"
},
"background": { "backgroundColor": "#fff" },
"button": { "backgroundColor": "#fff", "color": "#333" },
"color": "#fff"
}
And now we can use it like so, which will decorate our web page accordingly:
theme.applyStylesheet(stylesheet)
How do we make theme
return to us a custom theme when calling createStylesheet
that we can work with to extend from instead of having to work with the default one?
This is where decorators can come in handy as it will allow us to return to us a different predefined default theme to work with.
We'll create a decorator that will help us apply a blood
theme that will decorate Theme
so that it generates us a default stylesheet that will represent the blood
theme instead of the original.
We'll call this decorator bloodTheme
:
function bloodTheme(originalTheme) {
const originalStylesheet = originalTheme.createStylesheet()
originalTheme.createStylesheet = function() {
return {
name: 'blood',
...originalStylesheet,
header: {
...originalStylesheet.header,
color: '#fff',
fontStyle: 'italic',
},
background: {
...originalStylesheet.background,
color: '#fff',
backgroundColor: '#C53719',
},
button: {
...originalStylesheet.button,
backgroundColor: 'maroon',
color: '#fff',
},
primary: '#C53719',
secondary: 'maroon',
textColor: '#fff',
}
}
}
Now all we have to do is decorate a theme
with just one line:
const theme = new Theme()
bloodTheme(theme) // Applying the decorator
const stylesheet = theme.createStylesheet()
console.log(stylesheet)
The theme now gives us a default blood
stylesheet to work with:
{
"name": "blood",
"header": {
"color": "#fff",
"fontStyle": "italic",
"fontFamily": "Roboto, sans-serif"
},
"background": { "backgroundColor": "#C53719", "color": "#fff" },
"button": { "backgroundColor": "maroon", "color": "#fff" },
"color": "#fff",
"primary": "#C53719",
"secondary": "maroon",
"textColor": "#fff"
}
As you can see the code/implementation of theme
did not change. Applying the custom stylesheet did not change either:
theme.applyStylesheet(stylesheet)
Now our web page will have the blood
theme styles applied:
We can create as many themes as we want and apply them any time we wanted to. This means we left our code open for plugins like custom themes for example.
Another good time to use decorators is when we're looking for ways to temporarily apply behaviors to objects because we plan to remove it in the future.
For example, if christmas season is approaching we could easily create a christmas stylesheet and apply it as a decorator. This is great because we can remove it easily from the code when christmas season is over. In the case of our previous example, all we needed to do to convert back to the original stylesheet was just remove the bloodTheme(theme)
line.
Another good use case for using decorators is when creating subclasses start to become unmanageable when our code is becoming large. However, this problem is not that much of an issue in JavaScript as opposed to static languages like Java--unless you're heavily using class inheritance implementations in JavaScript.
Another useful use case is creating a debug mode decorator where when applied it will log every thing that happens to the console. For example here is a debugTheme
decorator that will be useful to us in development mode:
function debugTheme(originalTheme) {
const stylesheet = originalTheme.createStylesheet()
console.log(
'%cStylesheet created:',
'color:green;font-weight:bold;',
stylesheet,
)
if (!stylesheet.primary) {
console.warn(
'A stylesheet was created without a primary theme color. There may be layout glitches.',
)
}
}
const theme = new Theme()
bloodTheme(theme)
if (process.env.NODE_ENV === 'development') debugTheme(theme)
Our console now gives useful information when we're running our app in development
mode: