Skip to main content

Leveraging Computed Properties in EmberJS

<p>Modern-day applications often consist of complex data layers, backed with restful APIs and/or complex back-end systems to provide the services. Fortunately for us, it's very simple today to create such applications using modern fronting framework and tooling. Frameworks like AngularJS, EmberJS, Aurelia, amongst many other frameworks, have demonstrated that the data logic can be extracted to a simpler layer, letting the user deal with transactions, and not deal with API configuration. Building an application with this approach can often yield to very high development velocities, at the risk of ...</p>

Modern-day applications often consist of complex data layers, backed with restful APIs and/or complex back-end systems to provide the services. Fortunately for us, it's very simple today to create such applications using modern fronting framework and tooling. Frameworks like AngularJS, EmberJS, Aurelia, amongst many other frameworks, have demonstrated that the data logic can be extracted to a simpler layer, letting the user deal with transactions, and not deal with API configuration. Building an application with this approach can often yield to very high development velocities, at the risk of increasing technical debt; frameworks like these greatly simplify tasks, but require the entire development team to understand the given framework. In many cases, using a complex framework can result in a lot of confusion, given that resources are often disparate, or even nonexistent. Such is the case, I found, for ember data, a library to simplify data transactions using EmberJS.

In this write-up, I hope to show you how to leverage Ember Data's computed properties. Understanding how computed properties work in ember is key to simplifying your application. By having data layer which performs most operations, you can easily avoid groundwork for initialization of their models.

A refresher on computed properties

A computed property is a property which is computed in real-time. Computed properties are not sent the server, when the model was saved, deleted, or modified. For this reason, it is important to understand that not everything should be a computed property. In most cases, it's best to simplify the data layer as much as possible, without restricting performance. (Recall that computed properties are dependent on key values, and therefore can cause a lot of performance issues, and headaches when tracing performance).

The canonical example of a computed property is creating someone's full name. As you are well aware, a person's full name is the combination of their first, middle, and last names.

On the official EmberJS website, here is the example they provide:

export default DS.Model.extend({
    firstName: DS.attr("string"),
    lastName: DS.attr("string"),
    fullName: Ember.computed("firstName", "lastName", () => {
        return `${this.get("firstName"} ${this.get("lastName")}`;
    }
});

Can you see what's wrong? What if the user doesn't have a first name? Suppose we only type in "Renner" in the input field bound to lastName. fullName would simply result in Renner.

Using notEmpty to complete the fullName

What we really need is a way to check if both the firstName and the lastName are present? Then, if they are, set the fullName? A naive approach would be to use observers (now depreciated):

export default DS.Model.extend({
    firstName: DS.attr("string"),
    lastName: DS.attr("string"),
    fullName: DS.attr("string"),
    namesChanged: => {
        if (!Ember.isBlank(this.get("firstName")) && !Ember.isBlank(this.get("lastName"))) {
            this.set("fullName", `${this.get("firstName"} ${this.get("lastName")}`);
        }
    }.observes("firstName", "lastName")
});

By opting for an observer instead of a computed property, we allow fullName to be sent to the server and saved in the data model, which may or may not be a good thing. In this case, it makes very little sense, however, to have fullName as part of the data model, as fullName can always be found by looking at firstName and lastName of the given model. Besides, observers suck.

A better solution would be to use Javascript's Array type to group all the names of a given candidate. This can be done with a combination of Array.splice() and Array.concat(), with a safeguard using Ember.makeArray():

export default DS.Model.extend({
    firstName: DS.attr("string"),
    lastName: DS.attr("string"),
    names: Ember.computed.group("firstName", "lastName"),
    hasFullName: Ember.computed.equal("names.length", 2),
    fullName: Ember.computed("firstName", "lastName", () => {
        return Ember.makeArray(this.get("names")).splice().concat(" ");
    }
});

Using Ember.computed.oneWay

To address some performance issues, users have recently taken refuge in ember's new one-way data binding features. Starting in Ember 2.0, one-way data bindings are default, and the user must explicitly opt in to two-way bindings. Using one-way data binding reduces the complexity of a given computed property, and prevents changes from propagating in the other direction. For example, if we are tracking the length of a given has many relationship, we me opt to do so in the following manner:

models/applicant:

export default DS.Model.extend({
    jobs: DS.hasMany("job"),
    hasJobs: Ember.computed.notEmpty("jobs")
});

Using the computed method notEmpty works as expected. However, this means that Ember's Run loop must execute the method computed.notEmpty every time a new job is added to the applicant.

A better solution would be to leverage Ember's oneWay data binding. This way, we can bind to the length attribute, without having to worry about propagating changes to the length attribute of jobs. Just like in the previous case, using Ember.computed.alias, we can use an macro key to get the property we are interested:

export default DS.Model.extend({
    jobs: DS.hasMany("job"),
    hasJobs: Ember.computed.oneWay("jobs.length")
});

Using this approach will be faster because the length property of jobs will always return an integer (because deep down, jobs is an instance of Ember's ArrayProxy); in the case that the jobs array is empty, it will return 0, and 0 in Handlebars evaluates to false.

Common Ember.Computed Mistakes

Combining Arrays

In many cases, you might find yourself trying to combine arrays. For example, suppose you're building an application to perform event planning and to manage events such as weddings, corporate events, and private events. In building such an application, you would have to maintain a list of people both going to the events and people not going to the events. For each event, you would typically have a list of all attendees to an event, and a list of all attendees not attending an event, and you might want to combine these attendees. A naiive approach would be to have a property on the given component or controller which represents each group of attendees:

export default Ember.Controller.extend({
    attendees: Ember.A(),
    attendeesNotGoing: Ember.A()
}

Thus, the logic to add or remove attendees would look like this:

export default Ember.Controller.extend({
    attendees: Ember.A(),
    attendeesNotGoing: Ember.A(),
    actions: {
        unRSVP: function(attendee) {
            this.get("attendees").removeObject(attendee);
            this.get("attendeesNotGoing").pushObject(attendee);
        }
    }
}

Can you see the problem here? Although this approach will work, maintaining two separate lists of attendees forces us to carry out two separate operations when the add or remove a given attendee. Wouldn't it be easier if we could just add or remove a given attendee in one line?

A better approach would be to use the Ember.Computed.filterBy computed property helper. In fact, the solution is to not combine arrays at all; instead, we can keep all the attendees in one array, and filter out the attendees which are or aren't going:

export default Ember.Controller.extend({
    attendees: Ember.A(),
    attendeesNotGoing: Ember.computed.filterBy("attendees", "rsvp", true),
    attendeesGoing: Ember.computed.filterBy("attendees", "rsvp", false)
}

Thus, the logic to how to remove a given applicant would only be one line:

export default Ember.Controller.extend({
    attendees: Ember.A(),
    attendeesNotGoing: Ember.computed.filterBy("attendees", "rsvp", true),
    attendeesGoing: Ember.computed.filterBy("attendees", "rsvp", false),
    actions: {
        unRSVP: function(attendee) {
            attendee.set("rsvp", false);
        },
        rsvp: function(attendee) {
            attendee.set("rsvp", true);
        }
    }
}

But wait! Can you still see that there are some problems? We can further optimize this code and remove both rsvp() and unRSVP() actions, replacing them with a single toggleRSVP(). This is easily done with Ember's toggleProperty method:

export default Ember.Controller.extend({
    attendees: Ember.A(),
    attendeesNotGoing: Ember.computed.filterBy("attendees", "rsvp", true),
    attendeesGoing: Ember.computed.filterBy("attendees", "rsvp", false),
    actions: {
        toggleRSVP: function(attendee) {
            attendee.toggleProperty("rsvp");
        }
    }
}

And here, instead of having a separate button for RSVP and for unRSVP, we can simply find the current value of the attendee's status to the template:

templates/components/event.hbs

<table class="event">
    <thead>
        <tr>
            <th>Event Name</th>
            <th>Status</th>
            <th>Attendees</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>{{event.name}}</td>
            <td>{{event.status}}</td>
            <td>
                {{#if model.attendeesGoing.length}}
                    <ul>
                        {{#each model.attendeesGoing as |attendee|}}
                            <li>{{attendee.fullName}}</li>
                        {{/each}}
                    </ul>
                {{/if}}
            </td>
        </tr>
    </tbody>
</table>

Creating Records the Right Way

When I first started using Ember Data, I didn't understand why saving records was so tedious. Using any model that leveraged composition, I would often find myself calling the save() method several times, within 4 promises deep. Here's an example of one of my save() methods:

saveVehicles() {
            var promises = [];
            this.get("currentModel.applicant.vehicles").forEach((vehicle) => {
                // only save vehicle loan when `hasLoan` is set
                if (vehicle.get("loan")) {
                    let vehicleLoan = vehicle.get("loan");
                    if (vehicle.get("isFinanced")) {
                        vehicleLoan.then((loan) => {
                            return loan.save();
                        });
                    }
                    else {
                        vehicleLoan.then((loan) => {
                            // when the vehicle isn't financed, there's no liability
                            return loan.destroyRecord();
                        })
                    }
                    promises.push(vehicleLoan);
                }
                if (vehicle.get("asset")) {
                    let vehicleAsset = vehicle.get("asset").then((asset) => {
                        return asset.save();
                    });
                    promises.push(vehicleAsset);
                }
                promises.push(vehicle.save());
            });
            return Ember.RSVP.all(promises);
        },

The reason this got complicated so quickly is because I had dependent models (using the relationships DS.hasMany, DS.belongsTo, and so on. In other words, I had to createRecord the dependent record before creating the parent record.

ES7 Async to the Rescue

One way to deal with the 4 levels deep of promises is to use ES6 generators and the ES2016 (ES7) extended promise await API. Generators are a new feature in ES6 which allow asynchronous operations with the keyword yield. At compile-time, Babel (the ES6 transpiler) searches for every declartion of the keyword yield, and those occurences thereafter act as return points of each succeeded promise that is returned from the function.

Like generators, the await keyword awaits the result of a given promise, and does not execute the next yielded value until it's resolved.

Generators are actually functions in disguise, with the exception that insted of return, they use yield, to indicate the success of a promise.

Let's re-write the complex saveVehicles() method with a generator:

saveVehicles() {
            this.get("currentModel.applicant.vehicles").forEach((vehicle) => {
                return new Ember.RSVP.Promise((resolve, reject) => {
                    // only save vehicle loan when `hasLoan` is set
                    if (vehicle.get("loan")) {
                        let vehicleLoan = await vehicle.get("loan");
                        if (vehicle.get("isFinanced")) {
                            await vehicleLoan.save();
                        }
                        else {
                            await vehicleLoan.destroyRecord();
                        }
                    }
                    if (vehicleLoan.get("asset")) {
                        let vehicleAsset = await vehicleLoan.get("asset");
                        await asset.save();
                    }
                });
            });
        },
        // ...

In order to use ES2016 features, we need to tell Babel:

ember-cli-build.js

var EmberApp = require("ember-cli/lib/broccoli/ember-app")

module.exports = function(defaults) {  
  var app = new EmberApp(defaults, {
    babel: {
      includePolyfill: true
    },
    // You might want to disable jshint as it does not support async/await yet.
    hinting: false
  })

  return app.toTree()
}

Then, on your command-line:

npm install --save-dev regenerator

In his blog post, Damian explores usage of ES2016/ES7 in far greater detail, and I highly recommend you read his post for more information.

Addendum: Async Computed Properties

In some cases, you may need to compute values in a model based on other models, as described above in the saveVehicles example. In these cases, a typical, hacky solution might involve proxying out the promisy value using something like Ember.computed.alias, and then using a long chain of then, catch, and finallys. For these cases, luckily, I have discovered Ember CLI Dispatch.

Here's a trivial example of ember-cli-dispatch's usage, for getting parameters for a given item:

import Ember from "ember";
import ajax from "ember-ajax";
import computedPromise from "ember-cli-dispatch/utils/computed-promise";
import ENV from "../../config/environment";

export default Ember.Component.extend({
    module: null,
    metadata: computedPromise("module.id", function() {
        return ajax({
            type: "GET",
            data: this.get("module.id"),
            url: ENV.apiEndpoint
        })
    }, "Fetching metadata"),
); 

In this example, the metadata for the object is only resolved after the component is loaded. The above assumes you've set up your config/environment.js file with your adapter endpoint.

But again. do you see a problem with this? Why not refactor this to occur within the Model?

app/models/module.js:

import DS from "ember-data";

export default DS.Model.extend({
    title: DS.attr("string", {defaultValue: "Untitled Module"}),
    metadata: DS.hasMany("data")
    //...
});

Then, as soon as module.metadata is referenced (because all relationships are async unless you opt out in Ember 2.0+), Ember will make the async request, only when needed.

But most of all, metadata should really be a part of the object payload, as specified in the ember data docs.

Altogether, I'm still not convinced there are many use cases for something like Ember CLI Dispatch, as most logic can be done in the model, and lazily computed on demand. In any case, you should now have the resources to cleanly compute, create, and update your Ember Data components.