Decorators in JavaScript

Christopher T.

February 23rd, 2020

Share This Post

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.

When is it 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, 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)

theme decorator in javascript apply default theme to web page

Continue with this in mind: Providing Open Opportunities to Support Plugin Development

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:

theme-decorator-javascript-apply-blood-theme

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.

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

Subclassing/Inheritance

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.

Debug Modes

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:

debug mode theme decorator javascript console log messages


Tags

javascript
decorators
composition
classes

Subscribe to the Newsletter
Get continuous updates
© jsmanifest 2021