Know All About Decorators in JavaScript

In web development, JavaScript is considered the most recommended language to build user interfaces that are highly complex and can come from various needs, especially coming from business requirements.

In this article, we are 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 are 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 supported in TypeScript and Babel plugins.)

Here’s an example of that in use:

@filterNames // This is the decorator
class MyClass {
  constructor(children) {
    this.children = children
  }
}

Those of you who have never seen this type of code (specifically the @filterNames syntax) might feel a little frightened of decorators when realizing that this is applying a decorator.

Decorators this way are just syntactical sugar. Understanding and implementing a decorator might be easier than you think. They’re simple but powerful.

We’ll be taking a look at some examples of decorators in JavaScript and create our decorator to see how it can be useful to our code.

When Is a Good Time to Use a Decorator?

Fortunately, there are multiple ways a decorator can be useful to us.

Adding Dynamic Behavior to Existing Objects

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, let’s say we have a Vehicle class that implements a method called drive. Vehicles have tires, so we’ll also randomly implement a getTires method to return the number of tires they have.

Here’s what that may look like:

function Vehicle(name) {
  this.name = name
}

Vehicle.prototype.getTires = function() {
  return 4
}

Vehicle.prototype.drive= function(target) {
  console.log(`I'm going drive a ${target.name}. You better be ready`)
}

// Or with classes

class Vehicle {
  constructor(name) {
    this.name = name
  }

  getTeeths() {
    return 2
  }

  drive(target) {
    console.log(
      `I'm going drive a ${target.name}. You better be ready`,
    )
  }
}

In reality, there are different vehicles, like motorcycles, for example. A motorcycle is still a vehicle, but a car is not a motorcycle, which means that there must be some differentiating features between them that must not mix.

Since a car is a vehicle, we can build a withCar decorator that decorates an instance of a vehicle if desired so that it can represent cars.

Remember, a decorator should only extend or add new behavior to something but not change its implementation.

Knowing this, the withCar decorator is quite simple:

function withCar(car) {
  car.getTires = function() {
    return 4
  }
}

const saturnTheCar = new Vehicle('saturn')
withCar(saturnTheCar)

console.log(saturnTheCar.getTires())

Our decorator withCar implements getTires so that it returns 4 because cars have four tires. When we use this decorator, we’re mostly silently decorating (converting in this case) a Car to represent a Vehicle that is a motorcycle.

You can achieve the same goal using subclassing with inheritance, as shown below:

function Car(name) {
  Vehicle.call(this, name)
  
  this.getTires = function() {
    return 4
  }
}

const malibuTheCar = new Car('malibu')

// or using classes

class Car extends Vehicle {
  getTires() {
    return 4
  }
}

const malibuTheCar = new Car('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 manipulating a vehicle to be more aligned with the features of a car. 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 unusual.

Let’s 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 decorates our web page accordingly:

theme.applyStylesheet(stylesheet)

Continue with this in mind: providing open opportunities to support plugin development

How do we make Theme return us a custom theme when calling createStylesheet that we can work with to extend from instead of having to work with the default one?

Decorators can come in handy as it allows us to return a different, predefined default theme to work.

We’ll create a decorator that will help us apply a blood theme that decorates the Theme so that it generates a default stylesheet that represents the blood theme instead of the original.

We’ll call this decorator nightmareTheme:

function nightmareTheme(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',
    }
  }
}

We decorate a theme with just one line:

const theme = new Theme()
nightmareTheme(theme) // Applying the decorator
const stylesheet = theme.createStylesheet()
console.log(stylesheet)

The Theme now gives us a default nightmare stylesheet to work with:

{
  "name": "nightmare",
  "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)

We can create as many themes as we want and apply them any time we want to. We left our code open for plugins like custom themes, for example.

Applying Temporary Behavior

Another good time to use decorators is when we’re looking for ways to temporarily apply behaviors to objects because we plan to remove them in the future.

For example, if the Halloween season is approaching, we could easily create a Halloween stylesheet and apply it as a decorator. We can remove it easily from the code when the Christmas season is over.

In the case of our previous example, we need to do to convert back to the original stylesheet by removing the nightmareTheme(Theme) line.

Subclassing/Inheritance

Another good use case for using decorators is when creating subclasses starts to become unmanageable when our code becomes 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.

Debug Modes

Another useful use case is creating a debug mode decorator where it logs everything that happens to the console. For example, here is a debugTheme decorator that is useful 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()
nightmareTheme(theme)
if (process.env.NODE_ENV === 'development') debugTheme(theme)

Our console now gives useful information when we’re running our app in development mode.

Conclusion

And that is the end of this post! I hope you found this to be valuable and look out for more in the future!

Latest articles

How to do Destructuring in JavaScript

What is Destructuring? At its core, destructuring is the idea that we are pulling out values individually from a...

Writable Streams, Streams Piping, and Error Handling in Node.js

A Writable Stream A writable stream is a stream of data that is created to write some streaming data....

Here are Six Front-End Development Channels on YouTube

The Coding Train https://www.youtube.com/watch?v=bKEaK7WNLzM If you love programming then this is the...

Real World Use Cases for JavaScript Proxies

Introduction to Proxy In programming terms, a proxy is an entity that acts on behalf of some other entity....
14.0k Followers
Follow

Related articles

Leave a reply

Please enter your comment!
Please enter your name here