Before Ember services and advanced API for HTMLBars helpers were introduced, it was not easy to manage the global application state. By global state I mean the data essential for the most of applications out there - such as the current language or the current user info. Whenever this data changes every part of the UI needs to be updated to reflect the change. Previously, the easiest way to do this was the a reload of the current page. Of course, this was a bad, not user-friendly approach but one had very few other options. For example, users of the popular ember-i18n
project had the following problems:
ember-i18n
didn’t support bindings on the translations (till the version 3.1.1).ember-i18n
didn’t load translations so one had to do this on his/her one. Therefore, many people fetched translations in application initializers.
Another possibility to apply global changes was to use Route::refresh method. If you loaded translations in the model/beforeModel hooks, it was almost a perfect solution. But still the data that was entered on the page would be lost and the overall user experience was not great.
Ember services and self-recomputing helpers were introduced to overcome the problem. But first:
What are Services?
Services are singleton long-living objects that are instantiated when needed and that can be injected in any other Ember object. Also, one can inject other services into each other. The only limitation is that services are available after the application initialization meaning that one cannot inject a service into an initializer. This is not needed in the most of cases.
One defines a service by extending Ember.Service
class:
/**
* l10n service
* ember-cli users should put it in services/l10n.js
*/
App.L10nService = Ember.Service.extend({
locale: 'en',
t: function(key) {
const locale = this.get('locale');
const obj = TEXTs[key];
return obj[locale];
}
});
This is an example of a very basic translation facility. See the real service of ember-i18n v4+ here for more details.
So a service is always there, there is only one instance of it and it holds the state. This is a perfect place to manage global app state such as a user session, current language, theme etc.
What are Helpers?
In old versions of Ember, helpers were just functions. They still can be functions like this:
App.CurrencyHelper = Ember.Helper.helper(function(params, hash) {
let cents = params[0];
let currency = hash.currency;
return `${currency}${cents * 0.01}`;
});
But additionally, a helper can be an object with a state:
App.CurrencyHelper = Ember.Helper.extend({
param: 0.01, // state
compute(params, hash) {
let cents = params[0];
let currency = hash.currency;
let param = this.get('param');
return `${currency}${cents * param}`;
}
});
To define a helper as an object, one has to define a class by extending Ember.Helper
. The only mandatory method is compute
.
This is exactly like the old helper function but now it’s a method so it has the access to the context of a helper instance.
How to Use Helpers and Services Together?
The best part is that one can inject services into helpers:
App.THelper = Ember.Helper.extend({
l10n: Ember.inject.service(), // l10n service is injected
onLocaleChange: Ember.observer('l10n.locale', function() {
// recompute helper when the locale changes
this.recompute();
}),
compute(params, hash) {
// ask the service to provide a translation
return this.get('l10n').t(params[0]);
}
});
As you can see, it’s possible to bind to the service properties and react to changes using observers or computed properties. Here, the helper will re-compute itself whenever the current locale changes. Now it’s possible to translate the entire UI w/o any problems with a single set
on l10n.locale
. The same applies to the session information, user roles, application themes and so on. The the full demo in this jsbin: http://emberjs.jsbin.com/nakowavonu/1/edit?html,js,console,output.
In my opinion, the re-computing helpers and services is a huge improvement in Ember API. Now many of old problems can be easily solved.
Thanks for reading.