Computed Property Setters
Computed properties are my favorite feature of Ember, hands-down. The more ways I discover to leverage computed properties, the less I need to override lifecycle hooks or define custom functions within a component. This makes them a powerful option in the Ember toolkit for writing declarative, concise code.
Out of the box, computed properties allow a property to be defined as the return value of the function passed to it, which will then be cached until any of its dependent properties are updated. If you are new to Ember, the Guides are pretty essential, and have a lot to say about computed properties.
When a computed property is declared in the usual way by being passed a function, it corresponds with the property’s getter. However, a computed property’s setter can also be defined if the property is passed an object instead:
import Ember from 'ember';
const { Component, computed, get } = Ember;
export default Component.extend({
foo: computed('bar', {
get() {
return get(this, 'bar');
},
// key is 'foo' here
set(key, value) {
// 'foo' is set to the return value of this function
return value;
}
})
});
This expanded syntax has come in handy often, and has allowed us to use computed properties in ways I didn’t initially anticipate.
Here are some problems we’ve solved through computed properties with a custom set
:
Non-Primitive Default Values
A common “gotcha” within Ember is declaring a property default as a non-primitive value:
import Ember from 'ember';
export default Ember.Object.extend({
names: ['Klaus', 'Michael']
});
An Ember Object declared like this will cause a developer grief, as the names
property within every instance of that object / component will reference the same array. This is because extending Ember.Object
defines properties on the resulting class’s prototype:
function BugCollector () {};
BugCollector.prototype.bugs = [ 'ant', 'caterpillar' ];
BugCollector.prototype.addBug = function (bug) {
this.bugs.push(bug);
return this.bugs;
}
const Terry = new BugCollector();
Terry.addBug('caterpillar'); // => [ 'ant', 'ladybug', 'caterpillar' ]
const Joe = new BugCollector();
Joe.addBug('weevil'); // [ 'ant', 'ladybug', 'caterpillar', 'weevil' ] :(
ES6 classes and their constructors make it so we don’t have to think about this too much anymore when we’re writing vanilla JavaScript. But until Ember classes can be declared with this syntax, it will remain a common concern for those of us in Ember-Land.
To get around this, I used to set up property defaults inside of the init
hook if they were Objects or Arrays:
import Ember from 'ember';
const { Component, set } = Ember;
export default Component.extend({
names: null,
init() {
set(this, 'names')([ 'Klaus', 'Michael' ]);
this._super(...arguments);
}
});
This works, but it has ugly boilerplate (_super
ugly). If the component becomes large and has to do this for multiple properties, it will become less readable, as it will force the developer to jump back and forth between the property declarations and the init
hook where their defaults are actually set up. I prefer a computed-property-based solution:
import Ember from 'ember';
const { Component, computed } = Ember;
export default Component.extend({
names: computed({
get() {
return [ 'Klaus', 'Michael' ];
},
set(_, names) {
return names;
}
})
});
A computed property can observe any number of values, but I was surprised to find that it can also observe none at all.
Computed properties cache their return values until one of their dependent keys updates. When a computed property doesn’t have any dependent keys, this never happens and it caches its last return value until explicitly set
.
If the above component is invoked with names
in its attrs hash, this will occur automatically on init
, overriding the default value as expected.
However, if names
is not explicitly passed into the component, its getter will lazily return a newly-constructed array of default values.
Writing computed properties this way is not only great for setting defaults; it’s also nice for writing a self-documenting API:
import Ember from 'ember';
const { Component, computed, assert, get } = Ember;
export default Component.extend({
name: computed({
get() {
return assert("This component requires a 'name' property!");
},
set(_, name) {
if (typeof name !== 'string') {
assert("'name' must be set to a string!");
}
return name;
}
})
});
Refactoring Observers
Using this pattern also helps eliminate many common use-cases for Observers. If you’ve visited the Ember Guide page for Observers, you’ve probably read this line:
Observers are often over-used by new Ember developers. Observers are used heavily within the Ember framework itself, but for most problems Ember app developers face, computed properties are the appropriate solution.
And if you’re like me, you probably ignored that advice and shamefully added a bunch of observers into your app anyway.
Maybe it was a simple observer like this:
import Ember from 'ember';
const { Component, observer } = Ember;
export default Component.extend({
name: 'Klaus',
onNameChange: observer('name', function () {
get(this, 'onnamechange')();
})
});
Like the guides claim, computed properties are a better implementation for the above, but they don’t make it clear how one would go about it. I’ve been slowly refactoring many of our observers it into something like this:
import Ember from 'ember';
const { Component, computed } = Ember;
export default Component.extend({
name: computed({
get() {
return 'Klaus';
},
set(_, name) {
get(this, 'onnamechange')();
return name;
}
})
});
You might need to add a little bit extra to this example if you don’t want onnamechange
to fire in the init
hook, though.
Real-World Example!
Our team was implementing a date-time picker component, using Ember Power Calendar with an extra input for time of day.
The ember-power-calendar component expects a selected
(the currently selected date), and a center
(the currently selected month). Whenever selected
updates, center
should read from it, so that the calendar re-centers on the correct month.
import Ember from 'ember';
const { Component, computed: { reads } } = Ember;
export default Component.extend({
selected: null,
center: reads('selected'),
onupdate() {},
actions: {
update(newSelectedValue) {
get(this, 'onupdate')(newSelectedValue);
}
}
});
However, users should also be able to browse months on the calendar directly without changing the selected date. Internally, this involves setting center
directly without updating selected
.
This is impossible to do with the above implementation. Once a user changes months, center
will no longer continue to update when a user selects a new date.
Unfortunately, calling set
on a computed property defined this way will simply overwrite the computed property and remove its binding to its dependent property. If the dependent property changes later, our computed property will not recompute itself as expected.
Because of this, we considered moving away from computed properties altogether:
import Ember from 'ember';
const { Component, get, set } = Ember;
export default Component.extend({
selected: null,
center: null,
onupdate() {},
init() {
this._super(...arguments);
set(this, 'center', get(this, 'selected'));
},
actions: {
update(newSelected) {
get(this, 'onupdate')(newSelected);
set(this, 'center')(newSelected);
}
}
});
This solves our immediate problem, but I’m not a huge fan. Once again, we’re overriding init
just to set up a property, which just doesn’t feel right.
Also, I mentioned that this was a date time picker, which had an extra input for the time of day. If a user has selected 11:59PM on the last day of a given month and bumps the time forward by one minute, how can this change propagate into the calendar component to correctly set the center
to the next month?
You could extract the logic for center
into the container component that manages the calendar and the date input, but that’s not ideal as it is really just display logic for the calendar.
What we want is a computed property that reads
another most of the time, but can still be manually set
from within the component.
import Ember from 'ember';
const { Component, computed, get } = Ember;
export default Component.extend({
selected: null,
onupdate() {},
center: computed('selected', {
get() {
return get(this, 'selected');
},
set(_, value) {
return value;
}
}),
actions: {
update(newSelectedValue) {
get(this, 'onupdate')(newSelectedValue);
}
}
});
Defining center
as a computed property with a custom setter gives us exactly what we need. If it is not explicitly set, center
will simply read from and return selected
. It can still be set manually, but a change in selected
will break its cache, causing it to resynchronize with selected
again.
We can update center
directly in the template with the mut
helper, but if selected
is updated anywhere - inside or outside of the calendar component - center
will adhere to the change.
What Do You Think?
Although we still write most of our computed properties in the usual style, it feels great to have access to a hidden “power mode” when I need it.
I haven’t seen much discussion in the Ember community about this pattern, and although it has worked out well for us I am curious to hear what you think!