Decorate the world – Outline – What are Javascript Decorators?



Decorate the world – Outline – What are Javascript Decorators?

0 0


presentation-decorators

Presentation on javascript decorators.

On Github nmackey / presentation-decorators

Decorate the world

By: Nicholas Mackey

Who is already using decorators? How many people have written their own?

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

Decorate the world By: Nicholas Mackey Who is already using decorators? How many people have written their own?