topaxi.codes

Using ES7 Decorators In Ember

Thanks to Babel and the Ember add-on ember-computed-decorators it is possible to use some pretty nifty ES7 (or post ES2015) syntax for computed properties, event-listeners, observers and more!

Decorators make it very easy to annotate and modify properties or classes using a declarative syntax.

Decorators are currently a stage 1 proposal in ecma262, to use it with ember-cli, we have to enable it in our Brocfile.js.

var EmberApp = require('ember-cli/lib/broccoli/ember-app')  
var app      = new EmberApp({  
  babel: {
    optional: [ 'es7.decorators' ]
  }
})

module.exports = app.toTree()  

The ember-computed-decorators add-on add some Ember and Ember Data specific decorators to your project ready to be used.

$ ember install ember-computed-decorators

Simple example by converting this file to this:

import Ember from 'ember'  
import { computed, observes } from 'ember-computed-decorators'

export default Ember.Component.extend({  
  @on('didInsertElement')
  setupEditable() {
    let options = {
      placeholder: this.get('placeholder'),
      emptytext:   this.get('emptytext'),
      value:       this.get('value'),
      success:     (res, value) => this.sendAction('setValue', value)
    }

    let $editable = Ember.$('<a>').addClass('editable').editable(options)
    this.$().append($editable)
  },

  @observes('value')
  updateValue() {
    this.$('.editable').editable('setValue', this.get('value'))
  }
})

As you can see, we moved the relevant events or observers above our functions. With this new syntax we have two nice side effects, first we immediately see when each function is triggered without skipping to the end of the function, second we can use the ES2015 concise method syntax, which is shorter and more pleasant to the eye.

Decorators are evaluated from bottom to top, which means, decorators closer to the function get executed first.

@computed('firstName', 'lastName') // executes second
@readOnly                          // executes first
fullName() {  
  return `${this.get('firstName')} ${this.get('lastName')}`
}

But this shouldn't matter in most cases.

Another example using the Ember Data decorators:

import DS from 'ember-data'  
import { readOnly, computed } from 'ember-computed-decorators'  
import { attr, hasMany, belongsTo } from 'ember-computed-decorators/ember-data'

export default DS.Model.extend({  
  @attr('string') username,
  @attr('string') firstName,
  @attr('string') lastName,
  @attr('date') birthday,
  @hasMany('user') friends,
  @belongsTo('team') team,

  @readOnly
  @computed('firstName', 'lastName')
  fullName() {
    return `${this.get('firstName')} ${this.get('lastName')}`
  },

  @readOnly
  @computed
  age() {
    let now       = new Date
    let birthday  = this.get('birthday')
    let age       = now.getFullYear() - birthday.getFullYear()
    let monthDiff = now.getMonth() - birthday.getMonth()

    if (monthDiff < 0 || monthDiff === 0 && now.getDate() < birthday.getDate()) {
      age--
    }

    return age
  }
})

As of this writing, the following decorators for Ember are available:

For import { ... } from 'ember-computed-decorators':

and for import { ... } from 'ember-computed-decorators/ember-data:

One last note, the ember-computed-decorators and even the decorator proposal are still in flux and probably shouldn't be used in any production code.