Decorate the world

By: Nicholas Mackey

Outline

  • What decorators are
  • How can we use them today
  • Short story
  • Decorator examples
  • Lessons learned

What are Javascript Decorators?

Look something like this:

                @someDecorator
              
From core-decorators library...

                @autobind
              
From Angular 2 framework...

                @NgModule()
              
Decorators offer a convenient declarative syntax to modify the shape of class declarations.
From the decorators draft spec (Yehuda Katz, Brian Terlson)

A decorator is:

  • an expression
  • that evaluates to a function
  • that takes 3 arguments: target, name, and descriptor (same signature as Object.defineProperty)
    
    function someDecorator() {
      returns function(target, name, descriptor){};
    }
                    
  • and optionally returns a decorator descriptor to install on the target object

They are sugar.

There isn't anything you can do with decorators that you can't already do.

But they make your life better :)

Current state of decorators

in flux...

Stage 2

Decorator Proposal

caniuse?

Babel 5 (decorators in)

Babel 6 (decorators out)

But almost back?

So now what?

You can use the 'legacy' plugin to get support for the original spec now

This is where we will focus.

How can we use decorators today?

Babel Setup


              npm install --save-dev babel-plugin-transform-decorators-legacy
            

.babelrc


{
  "presets": [ "es2015" ],
  "plugins": [
    "transform-decorators-legacy", // before class-properties
    "transform-class-properties" // optional
  ]
}
            

Example:


import { readonly } from 'decorators';

class Test {
  @readonly
  grade = 'B';
}

const mathTest = new Test();
mathTest.grade = A;
// Uncaught TypeError: Cannot assign to read only property 'grade'...
            

export function readonly(target, name, descriptor){
  descriptor.writable = false;
  return descriptor;
}
            

Parts of a decorator:


function someDecorator() {
  returns function(target, name, descriptor){};
}
            

target

this is the thing you are decorating

  • For classes this will be the class constructor
  • For methods/properties this will be the class prototype

name

this is the name of the thing you are decorating

  • For classes this will be undefined
  • For methods/properties this will be the method/property name

descriptor

this describes the data or accessor you are decorating

  • For classes this will be undefined
  • For methods/properties this will be the descriptor of the method/property you are decorating

Same descriptors that are used for Object.defineProperty()

Quick aside on descriptors

ES5 feature, part of Object.defineProperty()

2 types - data & accessor

Data Descriptor


{
  value: 'test', // could be number, object, function
  configurable: true, // can be deleted if true, defaults to false
  enumerable: true, // show in for in if true, defaults to false
  writable: true // can be updated, defaults to false
}
            

Accessor Descriptor


{
  get: function() {
    return 'test';
  },
  set: function(value) {
    this.value = value;
  },
  configurable: true, // can be deleted if true, defaults to false
  enumerable: true // show in for in if true, defaults to false
}
            

Example


import { decorator } from 'decorators';

class foo {
  @decorator
  bar() {}
}
            

// target: class prototype
// name: 'bar'
// descriptor: {
//   value: bar,
//   writable: true,
//   enumerable: false,
//   configurable: true
// }
export function decorator(target, name, descriptor) {
  // do stuff
};
            

Short story...

  • Older codebase using Backbone
  • Upgrading to ES6/2015 with babel

ES5


var View = Backbone.View.extend({
  tagName: 'li',
  className: 'stuff',
  events: {
    'click .btn': 'handleClick'
  },
  initialize: function() {},
  render: function() {},
  handleClick: function() {}
});
            

ES6/2015


class View extends Backbone.View {
  get tagName() {
    return 'li';
  }
  get className() {
    return 'stuff';
  }
  get events() {
    return {
      'click .btn': 'handleClick'
    };
  }
  initialize() {}
  render() {}
  handleClick() {}
}
            

Backbone & ES6/2015 don't quite go together

  • not totally compatible
  • class properties help, but not enough
  • didn't like the syntax

Went on vacation...

Decided to look into two things

  • typescript
  • decorators

Some Code...

go from this


class View extends Backbone.View {
  get tagName() {
    return 'li';
  }

  get className() {
    return 'stuff';
  }
}
            

to this


import { tagName, className } from 'decorators';

@tagName('li')
@className('stuff')
class View extends Backbone.View {}
            

(more on multiple decorators in a second)

go from this


class View extends Backbone.View {
  get events() {
    return {
      'click .btn': 'handleClick',
      'keypress input': 'handleKeyPress'
    };
  }

  handleClick {}

  handleKeypress {}
}
            

to this


import { on } from 'decorators';

class View extends Backbone.View {
  @on('click .btn')
  handleClick {}

  @on('click input')
  handleKeypress {}
}
            

Multiple decorators

Order matters

evaluated top to bottom, executed bottom to top


class Bar {
  @F
  @G
  foo() {}
}

F(G(foo()))
            

Decorator examples

Backbone Decorators

classname

Sets the classname on the prototype


import { className } from 'decorators';

@className('stuff')
class View extends Backbone.View {}
            

how?


export function className(value) {
  return function(target) {
    target.prototype.className = value;
  };
}
            

on

Sets an event trigger on the method


import { on } from 'decorators';

class View extends Backbone.View {
  @on('click .btn')
  handleClick {}
}
            

how?


export function on(eventName) {
  return function(target, name) {
    if (!target.events) {
      target.events = {};
    }
    target.events[eventName] = name;
  };
}
            

Core-Decorators

great decorator library meant to be used with the babel-legacy plugin

autobind

Binds the class to 'this' for the method or class


import { autobind } from 'core-decorators';

class test {
  @autobind
  handleEvent() {
    // 'this' is the class
  }
}
            

debounce

Creates a new debounced method that will wait the given time in ms before executing again


import { debounce } from 'core-decorators';

class test {
  content = '';
  @debounce(500)
  updateContent(content) {
    this.content = content;
  }
}
            

deprecate

Calls console.warn() with a deprecation message.


import { deprecate } from 'core-decorators';

class test {
  @deprecate('stop using this')
  oldMethod() {
    // do stuff
  }
}
            

Apply Decorators Helper

Applies decorators without transpiling


import { applyDecorators } from 'core-decorators';

class Foo {
  getFoo() {
    return this;
  }
}

applyDecorators(Foo, {
  getFoo: [autobind]
});
            

Lessons learned

Remember the exisiting descriptor


export function decorator(target, name, descriptor) {
  // modify the existing descriptor
}
            

Avoid collisions


export function maintainState(target) {
  Object.assign(target.prototype, {
    save,
    saveState,
    restoreState,
    hasStateChanged
  });
}
            

Use function keyword


export function decorator() {
  return function(target, name, descriptor) {
    // you want the context of this function to
    // be the same as the method you are decorating
  };
}
            

THE END

Presentation online at nmackey.com/presentation-decorators/ Slides available at github.com/nmackey/presentation-decorators Example code available at github.com/nmackey/presentation-decorators-examples

Web: nmackey.com / Twitter: @nicholas_mackey / Github: nmackey