Many Ember apps need to pull in additional functionality beyond what’s provided out of the box. Often times, these additional dependencies don’t always fit neatly into the existing roles provided in the programming model.
Some examples of things I’ve needed to implement in my own apps:
- WebSocket initialization
- Geolocation
- Global config (for things like feature flags, etc.)
Most people end up putting this extra functionality in a controller, which is great, and works in many cases. For example, let’s say you need geolocation in controllers/nearby-tweets
:
export default Ember.ArrayController.extend({
needs: ['geolocation'],
fetchNearbyTweets: function() {
var geolocation = this.get('controllers.geolocation');
geolocation.getCurrentPosition().then(function() {
// ...
});
}
});
Because controllers in Ember have access to other controllers via needs
, this works great.
But what about if you need the functionality somewhere other than a controller? For example, you may want to make a shared WebSocket available to all of the models in your app. Or, you may have a {{#feature-flag}}
component, and you’d like to provide it with configuration information so it knows which flags are on or off.
Services
The idea behind services is to make conventional and declarative what many people are doing now imperatively via initializers and injections. I’ll provide API sketches using the ember-cli
-style resolver, but they should translate to globals just as well.
Defining a Service
Services are automatically detected and registered by placing them in the services
directory:
app/
services/
geolocation.js
web-socket.js
To implement a service, just export a subclass of Ember.Service
:
export default Ember.Service.extend();
Specifying Availability
While similar in idea to Angular’s services/providers/factories, we should try to avoid the same API mistakes that lead to the spaghetti code seen in most larger apps. In particular, many new developers use services to punch through architectural layering and share information between, say, the route and the template layer.
To help nudge users in the right direction, services should be opt-in rather than opt-out. Service authors explicitly say where their service is available:
// app/services/web-socket.js
export default Ember.Service.extend({
availableIn: 'models'
});
// app/services/geolocation.js
export default Ember.Service.extend({
availableIn: ['controllers', 'routes']
});
Using Services
Services are singletons, and the singleton is injected into every instantiated object that matches the service’s availableIn
criteria. The name of the service is used to determine which property to set on the new instance.
For example, here’s the nearby-tweets
controller, assuming a services/geolocation.js
service was available to controllers:
// app/controllers/nearby-tweets.js
export default Ember.ArrayController.extend({
fetchNearbyTweets: function() {
this.geolocation.getCurrentPosition().then(function() {
// ...
});
}
});
As you can see, the geolocation
property has been set to the geolocation service singleton.
Similarly, the services/web-socket.js
service would be injected as webSocket
.
You can customize the property used to inject the service by setting its name
property:
// app/services/web-socket.js
export default Ember.Service.extend({
name: 'socket',
availableIn: 'models'
});
In the above example, every model would have its socket
property set to the service.
It’s important to note that, because the services API uses the existing container injection API, testing an object that relies on services is as simple as mocking or stubbing a property on that object. This avoids the complication of DI systems like Angular’s, where users have to learn DI incantations just to test inter-object dependencies.
Next Steps
I’ve implemented this pattern in several of the apps I’ve worked on and believe it should be straightforward given the existing primitives. The most important part is making sure that the API is as intuitive as possible, without giving new developers just enough rope to hang themselves.
Does the above proposed API solve your use cases? Do you think having this would help you clean up your apps? Do the names make sense? Let the bikeshedding ensue!