Previous: FAQ & Fact Sheet
Next: Lifecycle Events
Normally, you define tasks by decorating a generator function like
@task *myGeneratorFn() { /* ... */ }
.
Occasionally, you may want to be able to expose additional state of the task,
e.g. you might want to show the percentage progress of an uploadFile
task,
but unless you're using the techniques described below there's no good
place to expose that data to the template other than to set some properties
on the host object, but then you lose a lot of the benefits of encapsulation
in the process.
In cases like these, you can use Encapsulated Tasks, which behave just
like regular tasks, but with one crucial difference: the value
of this
within the task function points to the currently
running TaskInstance, rather than the host object that the task lives on
(e.g. a Component, Controller, etc). This allows for some nice patterns
where all of the state produced/mutated by a task can be contained (encapsulated)
within the Task itself, rather than having to live on the host
object.
To create an encapsulated task, decorate an object (instead of a generator function)
with the @task
decorator that defines a perform
generator function. The object can also contain initial values for task state,
as well as computed properties and anything else supported by classic Ember objects.
import { task } from 'ember-concurrency'; export default class EncapsulatedTaskComponent extends Component { outerFoo = 123; @task *regularTask(value) { // this is a classic/regular ember-concurrency task, // which has direct access to the host object that it // lives on via `this` console.log(this.outerFoo); // => 123 yield doSomeAsync(); this.set('outerFoo', value); } @task encapsulatedTask = { innerFoo: 456, // this `*perform() {}` syntax is valid JavaScript shorthand // syntax for `perform: function * () {}` *perform(value) { // this is an encapulated task. It does NOT have // direct access to the host object it lives on, but rather // only the properties defined within the POJO passed // to the `task()` constructor. console.log(this.innerFoo); // => 456 // `this` is the currently executing TaskInstance, so // you can also get classic TaskInstance properties // provided by ember-concurrency. console.log(this.isRunning); // => true yield doSomeAsync(); this.set('innerFoo', value); }, } }
This example demonstrates how to use encapsulated tasks to model file uploads. It keeps all of the upload state within each TaskInstance, and uses Derived State to expose the values set within the encapsulated tasks.
import { task, timeout } from 'ember-concurrency'; export default class EncapsulatedTaskController extends Controller { @task({ enqueue: true }) uploadFile = { progress: 0, url: null, stateText: computed('progress', function () { let progress = this.progress; if (progress < 49) { return 'Just started...'; } else if (progress < 100) { return 'Halfway there...'; } else { return 'Done!'; } }), *perform(makeUrl) { this.set('url', makeUrl()); while (this.progress < 100) { yield timeout(200); let newProgress = this.progress + Math.floor(Math.random() * 6) + 5; this.set('progress', Math.min(100, newProgress)); } return '(upload result data)'; }, }; makeRandomUrl() { return `https://www.${randomWord()}.edu`; } }
<p> <button {{on "click" (perform this.uploadFile this.makeRandomUrl)}} type="button"> Start Upload </button> </p> <h5>Queued Uploads: {{this.uploadFile.numQueued}}</h5> {{#let this.uploadFile.last as |encapsTask|}} <h5> Uploading to {{encapsTask.url}} ({{encapsTask.stateText}}): {{encapsTask.progress}}% </h5> {{/let}} {{#if this.uploadFile.lastSuccessful}} <h5 style="color: green;" {{! template-lint-disable no-inline-styles }}> <strong> Upload to {{this.uploadFile.lastSuccessful.url}}: {{this.uploadFile.lastSuccessful.value}} </strong> </h5> {{/if}}
Previous: FAQ & Fact Sheet
Next: Lifecycle Events