By TEN BITCOMB

Writing Your Own Decorators In Ember

With the release of Ember 3.10, we can now use ECMAScript decorator syntax out of the box. But what exactly is a decorator, and how can we write our own?

You're Already Using Decorators

Features such as computed properties are fundamental to Ember, and if you've written Ember apps, you've no doubt used the computed function, like so:

import EmberObject, { computed } from '@ember/object';

Person = EmberObject.extend({
  // these will be supplied by `create`
  firstName: null,
  lastName: null,

  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

let ironMan = Person.create({
  firstName: 'Tony',
  lastName:  'Stark'
});

ironMan.fullName; // "Tony Stark"

That computed function is actually a decorator. A decorator is just a function that takes an object and "decorates" it with something. That's all a decorator is! They're nothing fancy or unique to any programming language, and you've already been using them.

So then why are we talking about decorators in 2019?

We could ask ourselves the same thing about ECMAScript class syntax, but here we are.

Technically, Ember has supported native JavaScript classes for a long time. But there was a problem.

Let's refactor the classic-style Ember code from above and make it use a native class and class fields:

import EmberObject, { computed } from '@ember/object';

class Person extends EmberObject {

  fullName = computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  });

};

let ironMan = Person.create({
  firstName: 'Tony',
  lastName:  'Stark'
});

ironMan.fullName; // "Tony Stark”
ironMan.lastName = ‘Soprano';
ironMan.fullName; // "Tony Stark”

Why doesn't our computed property recompute like it does with the extend method?

Because of the nature of how classic Ember defines classes(passing an object to extend), a class being extended from EmberObject is aware of itself as it's being defined, and so it can look at the return value being passed to it by the computed decorator and know to recompute the fullName property when other properties change.

The difference with a native class is that code from the class isn't being run during design-time, meaning that the output of the computed function can't be contextualized to the class as it's being defined. This has been a fundamental problem with using native class syntax in Ember because, without the initial step of a class learning about its decorators and setting up the necessary observers, functionality like computed properties are broken.

Enter Decorator Syntax

A solution to this problem has been proposed as a standard for the ECMAScript syntax.

It suggests a syntactic sugar for applying decorators to functions, methods, properties, and classes at design time.

The proposal is in Stage 2(draft), meaning that the concept itself will eventually be added to ECMAScript, but there are still details that need to be worked out.

But plugins for Babel will let us use the future standard today!

With the latest Ember, we can write this:

import EmberObject from ‘@ember/object';
import { computed } from '@ember-decorators/object';

class Person extends EmberObject {

  constructor(){
    super(...arguments);
    this.set('firstName', null);
    this.set('lastName', null);
  }

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

};

let ironMan = Person.create({
  firstName: 'Tony',
  lastName:  'Stark'
});

ironMan.fullName; // "Tony Stark”
ironMan.lastName = 'Soprano';
ironMan.fullName; // "Tony Soprano”

With this new syntax(and somewhat different code under the hood), we can now use today's JavaScript and still use the power of Ember APIs.

Writing Your Own Decorators

I keep referring to ECMAScript decorator syntax because the proposed standard doesn't actually add new constructs to the language(as of the writing of this post).

This is what a syntax-compatible decorator that persists an attribute to localStorage might look like:

function localStorage(object, property, descriptor){
  return {
    get() { 
      return window.localStorage.getItem(property);
    },
    set(value){
      window.localStorage.setItem(property, value);
      this.notifyPropertyChange(property);
    }
  };
}

Something about this seems vaguely familiar.

In fact, it looks exactly like Object.defineProperty.

// Object.defineProperty(obj, prop, descriptor)
Object.defineProperty(myObject, 'someProperty', {
  get() { 
    return window.localStorage.getItem(property);
  },
  set(value){
    window.localStorage.setItem(property, value);
    this.notifyPropertyChange(property);
  }
});

What's actually happening when we use decorator syntax is that we are allowing Object.defineProperty to be performed on the target object as that object is being defined.

In the case of the decorator above, we are simply returning a descriptor defining a getter and a setter that use localStorage.

Example use in Ember:

function localStorage(object, property, descriptor){
  return {
    get() { 
      return window.localStorage.getItem(property);
    },
    set(value){
      window.localStorage.setItem(property, value);
      this.notifyPropertyChange(property);
    }
  };
}

class Location extends Service {

  @localStorage latitude;
  @localStorage longitude;

  getCoordinates(){}{
   ...
  }

};

The Location service will persist its latitude and longitude properties to localStorage.


Let's write a decorator that will run a function after 3 seconds, as well as affect the state of the target object.

import Component from '@ember/component';
import { computed } from '@ember/object';
import { later } from '@ember/runloop';

function runLater(time){
  return function (object, property, descriptor){
    return {
      value(){
        this.set('isWaiting', true);
        later(() => {
          this.set('isWaiting', false);
          descriptor.value() 
        }, time);
      }
    };
  };
};

class MyButton extends Component {
  tagName = 'button';

  @computed('isWaiting')
  get buttonText(){
    if(this.isWaiting){
      return 'Wait for it...';
    } else {
      return 'Click me and wait 3 seconds';
    }
  }  
  
  @runLater(3000)
  click(){
    alert('Decorators are awesome!');
  }
}

export default MyButton;

Here's our templates:

{{!-- app/templates/components/my-button.hbs --}}
{{buttonText}}
{{!-- app/templates/application.hbs --}}
{{my-button}}

Now let's see it in action:

Yes, the code samples from above are what's running.

Conclusion

The new decorator syntax will help Ember and JavaScript see a bright future, as having a standard in place creates greater potential for sharing code between frameworks. They make objects in JavaScript more easily composable, and can be an excellent alternative to Ember mixins.