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 |
|
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 |
|
Sure, this works, but let’s look at the problems:
It’s very verbose. All we wanted to do is load some JSON!
The request is made every time we enter the route. Chances are, once we load this data once, it won’t change again.
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.
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 |
|
We can now instantiate a single CountriesService
, and ask for its all
property as many times as we want.
1 2 3 4 |
|
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 |
|
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 |
|
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 |
|
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.