Ember Best Practices: Actions Down, Data Up... wait what?

donuts

Welcome back to the fourth installment of DockYard’s Ember Best Practices Blog Post Series! Last time Aaron Sikes explains why we can move away from the Ember {{input}} helper with angle-bracket components and improved actions. Aaron briefly discusses “Data down, actions up”, the topic of this post. So if you want a bit of context, stick around here for a minute then head over to his post afterwards.

Data Down, Actions Up

Two-way data binding is what originally attracted many to Ember.js. Data changes that affect the model are immediately propagated to the corresponding template, and conversely, any changes made in the UI are promptly reflected in the underlying model. Magic! This conveniently removes unnecessary boilerplate code.

However, when employed in large scale and complex applications, two-way data binding can often become more of a headache than a blessing. Have you struggled with where to mutate your data? Have you spent an excessive amount of time tracing the source of mutated data? You are not alone. The Ember core team in their quest for continuous growth and improvement, has decided that in Ember 2.X, one-way bindings will be the default. And the ability to opt in to two-way data binding will be provided. This new adopted data flow model will simplify communication between components and can be encapsulated in the phrase: “data down, actions up”.

That’s all well and good, but what does that actually look like in the wild?

Enough jabbering and show us!

Let’s build an app together. Our app will be a simple budgeting tool. Picture the money managing application, Mint, and more specifically an elementary version of its budgeting feature.

// application/index/route.js

import Ember from 'ember';

export default Ember.Route.extend({
  model () {
   return this.store.findAll('expense');
  },
  
  setupController(controller, model) {
    controller.set('expenses', model);
  }
});

Assume our server responds with a list of expenses, where each expense has a category and amount. In our template we provide an input box for the user to tell us their income. Then we iterate over the expenses, wrapping each expense in a component. And lastly, we display their total remaining money (income - expenses).

{{! application/index/template.hbs }}

<h4>Income:</h4> 
{{input class="income-input" type="integer" value=income}}

<table class="expenses">
  <tbody>
    {{#each expenses as |expense|}}
      {{expense-summary expense=expense}}
    {{/each}}
  </tbody>
</table>

<h4>Total: {{total}}</h4>
// application/index/controller.js
import Ember from 'ember';

export default Ember.Controller.extend({
  income:  0,
  total: Ember.computed('expenses', 'income', {
    get() {
      const amounts = this.get('expenses').mapBy('amount');
      const expenseTotal = amounts.reduce((previousValue, currentValue) => {
        return previousValue + currentValue;
      }, 0);
      
      return this.get('income') - expenseTotal;
    }
  })

In each row of our expenses table, we have an expenseSummaryComponent that displays the expense category, provides an input box for the expense amount, and has a “Save” button.

{{! expense-summary/template.hbs }}

<tr>
  <td>{{expense.category}}</td>
  <td>{{input type="integer" value=expense.amount}}</td>
  <td><button {{action 'amountChanged'}}>Save</button></td>
</tr>

In our expense-summary component we implement the amountChanged action.

// expense-summary/component.js
import Ember from 'ember';

export default Ember.Component.extend({
  actions: {
    amountChanged() {
      const expense = this.get('expense');
      expense.save();
    }
  }
});

Yay! Done! However, we forgot something - we haven’t applied our new mantra, “data down, actions up”, to this code yet.The issue is that the expenseSummaryComponent is updating the data (expense.amount), but the data should solely belong to our IndexController. Not only did the IndexController give the expenseSummaryComponent the expense data, but it also uses the expenses data to calculate the total. Meaning, if the IndexController was out of sync and had stale expense data, it would be displaying the incorrect total. While that may not be a huge concern in this simple scenario, it can be really difficult to trace in more extensive and intricate applications.

The Better Way

Instead we need to pass an action to our expenseSummaryComponent.

{{! parent component: expense-table/template.hbs }}

<table class="expenses">
  <tbody>
    {{#each expense in expenses}}
    	{{expense-summary expense=expense amountChanged="update"}}
    {{/each}}
  </tbody>
</table>

The expenseSummaryComponent template looks the same as before. But this time, the action amountChanged, called when the user clicks the “Save” button in the component, will look different.

// expense-summary/component.js
import Ember from 'ember';

export default Ember.Component.extend({
  actions: {
    amountChanged() {
      const expense = this.get(this, 'expense');
      this.sendAction('amountChanged', expense); 
    }
  }
});

The update action is now sent up to the parent Component or Controller, along with the expense as a parameter.

// application/index/controller.js
import Ember from 'ember';

export default Ember.Controller.extend({
  ...
  
  actions: {
    update(expense) {
      expense.save();
    }
  }

Since expense.save() is now happening in the Controller, the server will respond with the updated version of the expense (with the correct amount) and update the template automatically.

The Angle-Bracket Way

In Ember, components (and solely components) are the way of the future! And the future is now, with the recent release of Ember v2.1.0, routable components and angle-bracket components are live on Canary behind a feature flag. Here is what that might look like for our budget form example.

{{! expense-table/template.js}}

<table class="expenses">
  <tbody>
    {{#each expenses as |expense|}}
      <expense-summary expense=expense amountChanged={{action "update"}}>
    {{/each}}
  </tbody>
</table>
// expense-summary/component.js

export default Ember.Component.extend({
  actions: {
    amountChanged() {
      this.attrs.amountChanged(this.get('expense'));
    }
  }
});

Final Remarks

I won’t dive anymore into that in this blog post. A little birdy told me that closure actions will be the subject of a future Ember Best Practices Post. So look out for it! And remember to go back to Aaron’s post on using native input elements if you haven’t already.

I have put together an Ember Twiddle with the example we worked through today. It also demonstrates how by following the “data down, actions up” best practice, changes in the UI of one component can easily propagate and be reflected in other components. Check it out and feel free to play around!

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box