Async Computed Properties in Ember
Computed properties are flexible, efficient, and easy to understand. When I was first learning Ember, they reminded me of what I enjoyed so much about Excel - my first programming 🥰.
...and then I met ember-concurrency.
I didn't want to choose between them (computed properties and ember concurrency) — I wanted to define computed properties with generator functions and have the backing ember-concurrency task built seamlessly.
Like this:
// ✂️
tempForecast: computedTask('selectedDate', function * () {
const {
date,
buildForecastUrl,
} = this;
const url = buildForecastUrl(date);
const response = yield fetch(url);
if (response.status === 200) {
const json = yield response.json();
return json['temperature'];
}
})
// ✂️
With computed tasks, there are no promises to deal with in the template, and only a single computed task explicitly declared in the component.
// ✂️
{{#if this.tempForecastTask.isRunning}}
{{fa-icon "sun" spin=true}}
{{else}}
{{this.tempForecast}}
<span class="text-grey">
℉
</span>
{{/if}}
// ✂️
How does it work? I added this function in my utils that builds the task and computed property.
// manifesto/utils/computed-task.js
import {
defineProperty
} from '@ember/object';
import {
task
} from 'ember-concurrency';
import ComputedProperty from '@ember/object/computed';
import {
expandProperties
} from '@ember/object/computed';
const _CP = ComputedProperty;
function parseArgs(args) {
return {
dks: args.slice(0, -1),
gf: args[args.length - 1],
};
}
const ComputedTaskProperty = function (...args) {
const { dks, gf } = parseArgs(args);
return _CP.call(this, function(pn) {
const tn = `${pn}Task`;
const isInitKn = [
'isCtInit',
pn
].join('-');
const isInit = this.get(isInitKn);
const vkn = [
tn,
'lastSuccessful',
'value'
].join('.');
if (!isInit) {
defineProperty(
this,
tn,
task(gf).restartable()
);
this.addObserver(vkn, () => {
this.notifyPropertyChange(pn);
});
this.get(tn).perform();
const eks = [];
dks.forEach((dk) => {
expandProperties(dk, (p) => {
eks.push(p);
});
});
eks.forEach((ek) => {
this.addObserver(ek, () => {
this.get(tn).perform();
});
});
this.set(isInitKn, true);
}
return this.get(vkn);
});
}
ComputedTaskProperty.prototype = Object.create(
ComputedProperty.prototype
);
export default function computedTask(...args) {
return new ComputedTaskProperty(...args);
}
It's not perfect, but I've used it for a while in our applications, and its been fine™.
A few final notes:
-
Why didn't I release it as an add on?
Because I'm a lousy maintainer. -
Why aren't there more options?
Because we haven't needed them. -
Why do you have to "get" the computed task in init?
Because it's generated lazily and the task wouldn't have been created if I hadn't "gotten" it. -
Should something like this be provided by ember-concurrency?
Maybe? I'd like it, but given that I'm haven't even released it as an add on, who am I to say.