Ember.computed.apply

When a Computed Property needs to observe a long list of properties, especially if that list can be generated at runtime, it can save a lot of effort and maintenance headaches to generate that CP dynamically.

An example from Ghost's editor controller mixin:

import PostModel from 'ghost/models/post';

const {Mixin, computed} = Ember;

const watchedProps = [
    'model.scratch',
    'model.titleScratch',
    'model.hasDirtyAttributes',
    'model.tags.[]'
];

PostModel.eachAttribute(function (name) {
    watchedProps.push(`model.${name}`);
});

export default Mixin.extend({

    hasDirtyAttributes: computed.apply(Ember, watchedProps.concat({
        get() {
            ...
        },
        set() {
            ...
        }
    })

});

This starts off pretty simply by building up an array of property names ready for use in a computed property definition. It then uses JavaScript's Function.prototype.apply method to take that array and create a CP without needing to manually specify every property that it watches.

NB. The array of attribute dependencies is built during application instantiation, this method won't work for dynamic CPs where the dependent attribute list changes during runtime.

DS.Model.eachAttribute

Somehow I hadn't come across this before. Ember Data makes any attributes registered with DS.attr available on the model class under the attributes property, providing model.eachAttribute to loop through them. Very useful in this case where it's used to pull the model attributes in to dynamically build the watched property list.

Ember.computed.apply

In functional programming (or mathematical) parlance, whenever a function is called/invoked with some arguments it is said that the function is applied to those arguments - that provides a bit of a clue as to what .apply is doing here.

Ember.computed.apply takes what would normally have been the argument list as an array and then "calls/invokes" the .computed method passing in that array as if it's elements had been passed in as a standard arguments list. In effect, it translates to the more recognisable:

Ember.computed('model.scratch', 'model.titleScratch', ..., function  () { ... })

The first argument that is given to .apply - Ember - is what provides the this context within the .computed function application. TODO: Why is Ember passed in and not this or something else?

watchedProps.concat({get(), set()}) is used to push the CP's body definition on to the end of the arguments array. If only a getter is required then a function expression could be passed instead:

Ember.computed.apply(Ember, watchedProps.concat(function () { ... } ));

An ES6 shortcut

ES6's new spread operator can replace the use of .apply() in this instance. By taking the array of property names and using the spread operator to expand it into function arguments our CP definition becomes more concise:

hasDirtyAttributes: computed(...watchedProps, function () {
    ...
}
Mastodon