Observable path patterns in Ember
26 March 2014
Property paths are the heart and soul of Ember.js apps. You use them in templates and you define dependencies for computed properties and observers through property paths.
In this post, I concentrate on this latter and show you various ways of setting up these dependencies through a practical example. There is a pretty good section in the guides about one path pattern. Here, I intend to cover more (all?) of them.
Badges are back
I am going to build on the badges "micro-app" that I had started to develop in my previous post about getters in setters.
There are two minimal model classes, User and Badge:
We also create a couple of instances to have some data to show and work with:
1var cory = App.User.create({ name: "Cory Filibuster" }); 2 3var rook = App.Badge.create({ 4 name: "R00k", 5 score: 1, 6 unlocked: true 7}); 8 9var taciturn = App.Badge.create({ 10 name: "Taciturn", 11 score: 10 12}); 13 14var talkative = App.Badge.create({ 15 name: "Talkative", 16 score: 100 17}); 18 19var hemingway = App.Badge.create({ 20 name: "Hemingway", 21 score: 1000 22});
Our application looks like this initally:
Simple property chain
The simplest, non-aggregate property path is just a series of names, connected by dots. This designates a property that you can arrive at by walking the path, segment by segment, where each of them gets you to another object until you finally reach the property.
(If a path is very long, you should probably think about the dependencies between your objects and the structure of your code.)
You see that the profile panel has the user's first name as its header. The property that gets displayed there can be defined by such a path:
This sets up a computed property (CP) that will be recomputed whenever user.name
changes. The arguments to the property
call are called the dependent keys of the CP and you can pass as many as you would like (although, thanks to the various property path patterns, you will rarely need a lot).
Now, whenever the name property of the user
property on the controller changes, firstName
is recomputed and this change gets propagated to all the instances where firstName
is used (e.g in the header of the panel).
Above that, the user.name
key also triggers a change if the user
object itself changes. To see that, we turn to the thing you should only ever use for demo purposes, the __container__
object:
You can see the name change in the header right away:
Aggregate property paths
On other occasions, a CP should depend on an array of items. Whenever something gets added to or removed from the array, the property needs to be updated.
One example of that is the number of badges in the profile panel:
The model here is the array of badges so when we add another one through the New badge panel, badgeCount
gets its new value:
What I said about the user.name
path triggering an update when the user changes also holds true here. If the array of badges was swapped out for another array, it would trigger the recalculation of badgeCount
.
Aggregate property path with a specified property
There are cases where the value of the CP becomes stale also when the items in the dependent array stay the same, but a certain property of one of them changes. Ember has a way to express this very succintly.
The example is the "Total score" in the profile panel:
This is the most inclusive of the patterns we have seen so far. It prompts an update if the model changes, if any item is added or removed and also if the score of any item changes. If we type this at the console:
1App.__container__.lookup('controller:index').set('model.lastObject.score', 200);
, then the total score changes accordingly, even though no item was inserted or deleted:
Brace yourself
To present the next pattern, let's assume that not all badge scores need to be tallied to get the total but only the unlocked ones (which makes total sense). So the dependent keys for totalScore
needs to account for that. That's pretty easy:
When the second badge is unlocked, the score jumps from 1 to 11 (and the number of badges from 1 to 2), so the dependent keys work fine:
1App.__container__.lookup('controller:index').get('model').objectAt(1).set('unlocked', true);
Starting with Ember 1.4.0, though, there is a more concise way to define the same, called "property brace expansion". It works very similar to argument expansion in the shell:
This establishes that totalScore should be recomputed if either the score
or unlocked
properties of any item in the model array changes.
An important restriction of property brace expansion is that the expansion part can only be placed at the end of the path, so e.g property('{foo,bar}.baz')
will not have the desired effect.
Computed property macros are the bee's knees
Computed property macros have several advantages. They are very expressive, very performant and perhaps most importantly more robust than typing out the property path patterns by hand where a typo can cause a considerable amount of head-scratching.
They are also a joy to work with and compose. In fact, all the CP definitions above can be re-defined by using only macros:
1App.IndexController = Ember.ArrayController.extend({ 2 (...) 3 badgeCount: Ember.computed.alias('unlockedBadges.length'), 4 unlockedBadges: Ember.computed.filterBy('model', 'unlocked'), 5 unlockedScores: Ember.computed.mapBy('unlockedBadges', 'score'), 6 totalScore: Ember.computed.sum('unlockedScores'), 7});
They have one big disadvantage, though. It is very hard to use them in a blog post to explain property path patterns.
(The code presented here can be found as a gist on Github)
ps. Yes, that Maggie Raindance.
Share on Twitter