A Task Manager with Alerts in Ember

A while ago I read this article on architecture in Ember apps. In one of the examples - monitoring background tasks - the author suggests creating a service that would be coupled with some components to display and manage arbitrary tasks.

I thought this was an interesting idea and decided to try and implement such a solution. In this post I will show my approach and explain how it works.

Feel free to check out the repository if you're interested in the finished product.

What to expect

In this short tutorial we will create an Ember Service for holding a collection of Tasks. This service will be connected with two components:

  • a component for showing alerts on finished tasks
  • a component for showing the status of all tasks

All this will be connected to a simple input, which will allow us to start named tasks. For the sake of simplicity, the tasks in this tutorial will be simple timers that finish after a random number of seconds. In a real application the tasks could be file uploads or API calls.

Setup

We'll start by generating an empty project and adding the routes, components and service we'll need later:

$ ember new ember-task-manager-example
$ cd ember-task-manager-example
$ ember g route application
$ ember g route test
$ ember g template index
$ ember g component alerts-container
$ ember g component task-monitor
$ ember g service task-manager
$ ember g initializer task-manager

The Service

Now that we have the necessary structure, we'll implement the Task Manager service. First, we'll configure the initializer:

// app/initializers/task-manager.js
export function initialize(container, application) {  
  application.inject('route', 'taskManager', 'service:task-manager');
  application.inject('component', 'taskManager', 'service:task-manager');
}

export default {  
  name: 'task-manager',
  initialize
};

The above will inject the Task Manager service into all routes and components. Thanks to this we will be able to access it simply by calling this.get('taskManager').

Next up is the actual service. Here's the code, some explanation follows below:

// app/services/task-manager.js
import Ember from 'ember';

export default Ember.Service.extend({  
  tasks: null,

  alerter: null,
  monitor: null,

  init() {
    this._super(...arguments);
    this.set('tasks', Ember.A());
  },

  _taskStarted(task) {
    this.get('tasks').pushObject(task);
    if(this.get('monitor')) {
      this.get('monitor').addTask(task);
    }
  },

  _taskFinished(task) {
    task.set('finished', true);
    if(this.get('alerter')) {
      this.get('alerter').addAlert(task.get('alert'));
    }
  },

  addListener(type, component) {
    this.set(type, component);
    if(type === 'monitor' && component) {
      this.get('tasks').map(task => {
        component.addTask(task);
      });
    }
  },

  destroyListener(type) {
    this.set(type, null);
  },

  runTask(task) {
    this._taskStarted(task);
    task.run().then(() => {
      this._taskFinished(task);
    });
  }
});

During the init() call we set the service's tasks property to an empty array - this will hold the tasks we'll be adding to it later. We have also declared 2 interesting properties: alerter and monitor. These will hold references to components - this will allow us to communicate with the components and alert them of any changes.

As mentioned before, we will introduce an alerting component and a monitor component. They register themselves in the service by calling its addListener method. This method simply assigns the component to the relevant property. Additionally, if the component in question is of type monitor, we will let it know of all tasks - thanks to this the component will be able to show all tasks even between route transitions. A component should also call destroyListener before it is destroyed, so that we don't notify nonexistent components.

A task is added to the service's queue by calling the runTask method. It accepts a task object, which should contain a run method - this method should return a promise, which will allow us to easily track its progress. Inside runTask the task is added to the queue through _taskStarted. We simply push the task into the current task list and notifies the task monitor component, if present.

After a task is finished, the _taskFinished method is called. It sets the task's finished property to true and notifies the alert component that a task has finished running. The task should have an alert object. Soon we'll see how this works in the actual components.

The Alerts Component

This component is intended to be inserted somewhere in the application template (we'll get to that later). Whenever a task finishes, the component will be notified and will display an alert. Let's look at the code:

// app/components/alerts-container.js
import Ember from 'ember';

export default Ember.Component.extend({  
  alerts: null,

  init() {
    this._super(...arguments);
    this.set('alerts', Ember.A());
    this.get('taskManager').addListener('alerter', this);
  },

  addAlert(alert) {
    this.get('alerts').pushObject(alert);
  },

  actions: {
    removeAlert(alert) {
      this.get('alerts').removeObject(alert);
    }
  }
});

There's not much happening here, really. On initialization we set the alerts property to be an empty array - this will be filled up with alerts later. Furthermore, the component registers itself in the Task Manager service by calling addListener. The service can now call the component's addAlert method, which simply pushes a task's alert into the alerts array. We've also added a removeAlert action, so that we can remove an alert from the view.

Here's what the component's template looks like:

// app/templates/components/alerts-container.hbs
{{#each alerts as |alert|}}
  <div class="alert alert-info alert-dismissible" role="alert">
    <button type="button" class="close" data-dismiss="alert" aria-label="Close" {{action 'removeAlert' alert}}><span aria-hidden="true">&times;</span></button>
    <div>{{alert.message}}</div>
  </div>
{{/each}}

We simply loop through all alerts and display their message. There's also a bit of styling (I've used bootstrap for quick prototyping) and a close button, which calls the previously mentioned removeAlert action.

The Task Monitor Component

This component won't be very different from the alerting component. In fact, this could probably be done in a more flexible fashion using some kind of mixin! That would also make the service a bit more reusable (we wouldn't need to declare special alerter and monitor containers), but I decided to leave that as an exercise to the reader.

Anyhow, here's our component:

// app/components/task-monitor.js
import Ember from 'ember';

export default Ember.Component.extend({  
  tasks: null,

  init() {
    this._super(...arguments);
    this.set('tasks', Ember.A());
    this.get('taskManager').addListener('monitor', this);
  },

  willDestroyElement() {
    this.set('tasks', Ember.A());
    this.get('taskManager').destroyListener('monitor');
  },

  addTask(task) {
    this.get('tasks').pushObject(task);
  }
});

As I said, this is very similar to the alerting component. The only difference is that this task monitor will empty its tasks array and unregister itself from the task manager service whenever it's about to be destroyed. This is necessary because we'll put this component into the index route later on and will be destroyed between route transitions.

This component simply displays a list of tasks (received from the service) and their current status:

// app/templates/components/task-monitor.hbs
<ul>  
  {{#each tasks as |task|}}
    <li>'{{task.name}}': {{if task.finished "finished!" "loading..."}}</li>
  {{/each}}
</ul>  

Tying it all together

We have the components and service, but now we need to actually start using them. In our example, we'll only show the task monitor in the index route, so we'll render it there:

// app/templates/index.hbs
{{task-monitor}}

In the beginning we've also created a test route. We will leave it empty, as we just want to be able to transition between routes and see that:

  • alerts render under all routes
  • the task monitor receives all current tasks on creation

Now let's go to the application route:

// app/routes/application.js
import Ember from 'ember';

function getRandomInt(min, max) {  
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

let Task = Ember.Object.extend({  
  finished: false,
  name: null,
  run: null,
  alert: null
});

let Alert = Ember.Object.extend({  
  message: null
});

export default Ember.Route.extend({  
  clicks: 0,

  _createTask: function(name) {
    let rand = getRandomInt(1, 10);

    return Task.create({
      name: name,
      run: function() {
        return new Ember.RSVP.Promise(resolve => {
          Ember.run.later(function() {
            resolve();
          }, rand * 1000);
        });
      },
      alert: Alert.create({
        message: 'Task ' + this.incrementProperty('clicks') + ' "' + name + '" finished after ' + rand + ' seconds.'
      })
    });
  },

  actions: {
    runTask: function() {
      let name = this.get('controller.name');
      this.get('taskManager').runTask(this._createTask(name));
    }
  }
});

Most of what you see here is only required for presentation purposes - we simply want to be able to demonstrate the service and components.

The getRandomInt function returns a random integer from a specified range. We will use this below when we're generating tasks.

We also define 2 objects: a task and an alert. These are the "models". A task will have a finished property, used to inform us of its status. It also has a name (can be arbitrary), an alert (an object sent to the alerting component after the task is finished) and a run method - our task manager service expects this to return a promise.

The application route has an action which will be triggered by clicking a button: runTask. This action will create a named task and send it to the task manager service for further handling.

Creating a sample task is handled inside _createTask. This, again, is just an example required for the sake of this tutorial. Our tasks can run a promise that will resolve in a random number of seconds. We will track the number of _createTask calls in the clicks property - in this example we'll be creating multiple tasks with possibly the same name, so we want to be able to show which successive task has finished. A task also receives an alert object, which contains a message on how long the task took to finish (the random number) and which task it was.

And finally there's the application template:

// app/templates/application.hbs
<h2 id="title">Task Manager Example</h2>

<div class="container">  
  <div class="row">
    {{link-to 'index' 'index'}}
    {{link-to 'test' 'test'}}
  </div>
  <div class="row">
    <div class="form-group">
      <label for="name">Task Name</label>
      {{input value=name class="form-control" id="name"}}
    </div>
    <button {{action "runTask"}} class="btn btn-default" role="button">Run Task</button>
  </div>
  <div class="row">
    <div class="col-xs-6">
      {{outlet}}
    </div>
    <div class="col-xs-6">
      {{alerts-container}}
    </div>
  </div>
</div>  

It contains links to our two routes: index and test, a simple input form for creating named tasks, an outlet and the alert component.

Run it

To see the example in action, we need to run Ember CLI's server:

$ ember serve

Now visit localhost:4200 and see the whole thing in action! Here's a gif of what it looks like:

Ember Task Manager GIF

We create a couple of tasks named "Test", each receiving a random number of seconds to execute. After a task is finished, an alert is displayed. While we're on the index route we're also presented with a list of all the tasks we've create with their current status.

Summary

As promised, we have created a service for managing background tasks (which could include file uploads or other long-running calls). The service is coupled with two components, so that we're able to monitor the current situation and be notified of a task finishing.

Of course, the code presented here is a very simple approach and may not be flexible enough for everyone's needs. I would like to encourage the reader to take this post as a starting point and try and improve on it as an exercise. Here's some pointers on what could be done better:

  • Track a Task's status internally, instead of setting it in the service
  • Create a mixin so that any component can register itself in the service
  • Improve the service so that any number of components can listen to its events
  • Actually hook up multiple types of actions and run them

The code for this tutorial is available in this repository and the app itself is available under http://kevinkucharczyk.github.io/ember-task-manager-example/.

I hope you've enjoyed this tutorial! Any questions or comments are, as always, very welcome.

Kevin P. Kucharczyk

Read more posts by this author.

Kraków, Poland