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.