Refactoring with computed property macros

Use Ember's built-in computed property macros to clean up your functions

Summary

Ember ships with several built-in computed property macros. They're great for cleaning up your code and making your apps more familiar to other Ember developers.

We'll go over three in this video: readyOnly, sum, and mapBy. Here's the final controller:

items: Ember.computed.readOnly('model'),
prices: Ember.computed.mapBy('items', 'price'),
subtotal: Ember.computed.sum('prices'),

total: Ember.computed('subtotal', 'discount', function() {
  let subtotal = this.get('subtotal');
  let tax = subtotal * .0875;
  let discount = subtotal * ((this.get('discount') || 0) / 100);

  return Math.round(subtotal + tax - discount);
})

Transcript

Today I want to refactor a computed property using Ember's built-in computed property macros.

So we're looking at a simple restaurant application here. It has a list of items with a name and price. You can change the price, you can also add an item down here, give it a new name, set the price, and you can see down here that as we change the prices of the items this total at the bottom is updating as well.

And finally we have a field down here for discount which also takes a number. So we can put something like 10% in, and we'll see that the total gets that percentage taken off its final price.

Over here we're looking at the controller that's backing this template right here for this application. And we can see that the total is being calculated from this computed property right here. This function depends on each item's price and the discount, and it starts by looping through each item in the model array, calculating the total here, and then it looks like we're multiplying by the tax which is 8.75%, adding that to the total, coming down here and then calculating the discount as a percentage, and then multiplying that by the total - and then we return the rounded value of that here.

Now, why do we need to refactor this function in the first place? Writing long functions like this definitely gets the job done. But a key part of becoming more productive as an Ember developer is learning how to use the framework's primitives in places where you might be used to writing your own code. Doing this not only saves you from writing some code yourself, but it also makes those parts of your application familiar to other Ember developers. When they come into your project and start working on your app, they'll be able to get going much more quickly.

So let's come back to this total function and refactor it a bit. Now the first thing we can do is clean up some of these local variables here. Let's call this subtotal, and we'll change it here as well. And then let's give this a name too. This is actually adding the tax on top of our total, but instead we can actually give this a name and we'll calculate the tax as .0875 of our subtotal.

Then we can also calculate the discount. Right now this is actually on top of the total, which from the first implementation had the tax in it already. But let's assume that the intention here was actually to calculate that discount on top of the subtotal, which makes more sense. So we'll find the discount by multiplying the subtotal by our discount - and we don't need to subtract from 100 anymore.

Now, instead of returning the total, we want to return the subtotal plus the tax minus the discount. If we save that, we can see that our total's being calculated here, and if we change prices or add an item it looks like it's still working. And then we'll add a discount as well.

Now, already adding some names to these variables has helped clean this function up a bit. But what I really want to show you in this video is Ember's computed property macros.

So what I like to do is come to this Ember.computed page, and this is just a page within the guides, and this gives us a list of all of the computed property macros that ship with Ember.

Now, computed property macros are really just computed properties that do something that's very common, like calculating a sum, or mapping from an array to a property of the elements of that array. And so I've found it very helpful to become familiar with this list because a lot of these come up in your code, and they're easy ways to clean things up.

So the first thing we want to look at is this one called readOnly. This is a really simple one that just creates a read-only alias to some other property on an object. And what I like to use this for is to just make my code a bit more expressive, and to give names to some things without actually transforming that data. And so the first thing here, is that when looking at this total function we see it depends on model.@each.price. Well, it's not super clear just by looking at this controller what the model is. And so what we can do is create items, which will be Ember.computed - and all these macros live off the .computed namespace - so we type Ember.computed.readOnly, and that's going to point to the model property. And so here, and anywhere else in our code, we can actually use items instead of model. And so now when somebody has to just read this computed property, they can say "OK, first we take all the items, and then we get their price." And that kind of makes it a bit more expressive.

Now if we go back to the index and look, the second one I want to go over is this one called sum. And sum calculates the sum of the values in an array. Now if we come back to our code, we'll see that we have an array of items, but this is an array of objects, and the price comes from the price property. And note first that here we're really just summing up these items and so, that kind of tells us that we might be able to use the computed.sum macro in this part of our code.

But first we need to get an array of just prices instead of the items. So what we really want to be able to do is something like this:

subtotal: Ember.computed.sum('prices')

But again we don't have this right now, because we just have this array of items.

Now, if we come back to the index again, there's one more I want to show you, which is called mapBy - and this is exactly what we need. mapBy is a macro that takes an array of objects and maps it to a new array, pulling some property or object off of each key in the original array. And so this is exactly what we need to transform our list of items into a list of prices. So mapBy takes a key and the property you want to map each item to.

So let's use this to write prices. So prices will just be:

prices: Ember.computed.mapBy('items', 'price')

We'll take our items, and just pluck the price off of each one. And now we have a prices array that's just a list of the prices, we can calculate the subtotal like this, and now our total can just depend on the subtotal and the discount, and it'll start like this, by getting the subtotal. We can get rid of this code right here, and now the total function becomes much more readable.

Now let's save this, come back to our app, test it out, and it looks like everything's working.

Alright, so this has really cleaned things up. Right now, our app just shows the total. If we were going to show maybe the discount and the tax as well, we might even make intermediate computed properties for these ones, just so it's even easier to see what's going on. But since our app just shows the total, I think this looks pretty good for now.


So, the main point here is to keep your computed properties short and expressive, and to use good variable names so it's easy for other developers to see what's going on, and also familiarize yourself with this Ember.computed page so you always know when these computed property macros can clean up your code.

👋 Hey there, Ember dev!

We hope you enjoyed this free video 🙂

If you like it and want to keep learning with us, we've written a free 6-lesson email course about the fundamental patterns of modern component design in Ember.

To get the first lesson now, enter your best email address below:

You can also check out more details about the course by clicking here.

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.