Ember.js Recipes: Light-Weight Data Services Using Dependency Injection

Your application almost certainly uses some kind of data layer (whether it is Ember-Data, Emu, Ember-Model, Ember-Resource, or a solution you rolled yourself), but what do you do when you need to load some data asynchronously, when it isn’t quite important enough to be a model?

For example, when a user edits her profile, you may allow her to choose from a list of countries. This can be a rather long list that you’d rather not hard-code into your application. But it also may feel like overkill to create full-fledged models for this seldom-used, read-only list of data.

What can we do?

First, at what layer do we even initiate this request? Ember intends for us to use the route layer to load data if possible. The beforeModel hook is a good fit for this case. Returning a promise from this hook will pause the transition until the promise resolves, allowing us to fetch the list of countries before the route loads:

1
2
3
4
5
App.ProfileRoute = Ember.Route.extend({
  beforeModel: function() {
    return Ember.$.getJSON('/api/countries');
  }
});

But there’s a problem. What happens with the data resolved by our promise? Nothing. Instead, we probably want our ProfileController to have access to the list of countries when the route loads. We might think to assign the data to a property on the route, and then assign it to the controller in the setupController hook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// routes/profile.js
App.ProfileRoute = Ember.Route.extend({
  beforeModel: function() {
    var _this = this;
    return Ember.$.getJSON('/api/countries').then(function(countries) {
      _this.set('countries', countries);
    });
  },

  setupController: function(controller, model) {
    this._super(controller, model);
    controller.set('countries', this.get('countries'));
  }
});

Sure, this works, but let’s look at the problems:

  1. It’s very verbose. All we wanted to do is load some JSON!

  2. The request is made every time we enter the route. Chances are, once we load this data once, it won’t change again.

  3. It’s a nightmare to test. We’d need to use a library like mockjax to simulate an Ajax request every time we enter the route.

  4. What if we need to fetch this data for a few different routes? Will we duplicate all this code?

Obviously I think there’s a better way.

First let’s tackle the middle two problems by encapsulating the Ajax request and its resolved data in an object. By mixing Ember.PromiseProxyMixin into an array controller, we can define a kind of lazy-loaded array that is also a promise and, when resolved, populates itself. [1]

To use it, we just need to set its promise property. We don’t want this to happen at initialization, so let’s create a class that wraps the request.

1
2
3
4
5
6
7
8
9
10
11
12
13
// services/countries.js
App.CountriesService = Ember.Object.extend({
  promise: function() {
    return Ember.$.getJSON('/api/countries');
  }.property(),

  all: function() {
    var promise = this.get('promise');
    return Ember.ArrayController.extend(Ember.PromiseProxyMixin).create({
      promise: promise
    });
  }.property()
});

We can now instantiate a single CountriesService, and ask for its all property as many times as we want.

1
2
3
4
var countries = App.CountriesService.create();

countries.get('all'); // Makes a request and returns a promise proxy array
countries.get('all'); // Returns the same promise proxy without a second request

You can also probably see why this is easier to test. We could easily replace the real CountriesService object with a fake one that returns a static promise when testing:

1
2
3
App.FakeCountriesService = App.CountriesService.extend({
  promise: Ember.RSVP.resolve([ "Canada", "USA", "Australia" ])
});

Great, now how do we trigger this in our route, and make it available to the controller?

Unlike Angular, Ember doesn’t much advertise dependency injection as a user-facing feature, even though it uses it extensively behind the scenes. If you’ve ever wondered how every route and every controller in your application has a magical reference to this.store (if you’re using Ember-Data), dependency injection is the answer.

We can define factories using Application.register, and then inject them into other parts of the application using Application.inject. If we follow reasonable naming conventions, we usually don’t even have to register a factory. A class named App.FoosService will automatically be registered as a factory named service:foos. [2]

The below code will inject a singleton instance of our CountriesService into our profile route and controller, as an instance variable called countries.

1
2
3
4
// application.js
window.App = Ember.Application.create();
App.inject('route:profile',       'countries',  'service:countries');
App.inject('controller:profile',  'countries',  'service:countries');

Since the service will be injected into our controller automatically, we can greatly simplify the beforeModel hook we were using:

1
2
3
4
5
6
// routes/profile.js
App.ProfileRoute = Ember.Route.extend({
  beforeModel: function() {
    return this.countries.get('all');
  }
});

Since the countries service will be injected into our profile controller, we can also use it in our template.

{{view Ember.Select
  content=countries.all
  value=country
  prompt="What country do you live in?"}}

Here’s a JSBin. Have a happy hacking day. □


[1] This may remind you of the way hasMany relationships are loaded in Ember-Data. That’s because this is exactly how that feature is implemented.

[2] If you are using Ember-CLI or Ember-App-Kit, where the classes making up your application are defined using ES6 modules, you may be wondering what to do. Every Ember application uses a resolver to look up its bits. Ember-CLI defines its own resolver which performs lookups using ES6 modules, according to the naming conventions in that documentation.

Comments