I'm working on an Ember application where there are a set of Widget instances and a dashboard to
show information about the Widgets. The dashboard contains some controls to filter the stats. One
of the filters is by widget_id. This filter contains a special entry, "All Widgets" (the identity
filter).
I initially accomplished this with the simplest solution I could think of:
// app/models/widget.js
export ALL_WIDGETS = {
id: 'all',
name: 'All Widgets'
};
export default DS.Model.extend({
name: DS.attr.string()
});
// app/dashboard/route.js
import { ALL_WIDGETS } from '../models/widget'
export default Ember.Route.extend({
model() {
return this.get('store').find('widget').then(function(widgets) {
return Ember.A([ ALL_WIDGETS ]).concat(widgets);
});
}
});The dashboard template receives a collection of { id, name } objects, not caring whether
they're Widgets. It shows the name and uses the ID in some action.
The problem is that the text is hard-coded. When the user selects a language other than English, the text should change.
My first attempt was to create an initializer that inserts fake Widget data into the Ember-Data
store:
// app/instance-initializers/all-widgets.js:
export default {
name: 'All Widgets',
initialize({ container }) {
const name = container.lookup('service:i18n').t('txt.widgets.all');
container.lookup('service:store').pushPayload('widget', { widgets: [ { id: 'all', name } ] });
}
};Now the language is correct when the user boots the app. But if they change language after boot, it won't update.
To solve this, I moved the all widgets logic back into the model, but made it an Ember.Object instace instead.
The problem is that the instance relies on service:i18n for the name. Therefore, the access to the object had
to take an i18n parameter:
// app/models/widget.js:
let _allWidgetsInstance = null;
export function allWidgetsInstance(i18n) {
if (allWidgetsInstance != null) return _allWidgetsInstance;
return allWidgetsInstance = Ember.Object.createWithMixins({
i18n: i18n,
id: 'all',
name: Ember.computed('i18n.locale', function() {
return this.get('i18n').t('txt.widgets.all');
}
});
}
//...The problem here is that we store the filter state in the URL. That means the dashboard.index route had code like
import { ALL_WIDGETS } from '../models/widget';
export default Ember.Route.extend({
beforeModel() {
this.replaceWith('dashboard.show', { id: ALL_WIDGETS.id });
}
});With iteration 3, finding the ID becomes dependent upon having the I18n service. We can solve this by exporting not just the singleton-lookup function, but also the ID, which is static.
I'm content with iteration 3. I'm also considering some Ember-injection-based solutions:
- just making the
ALL_WIDGETSobject anEmber.Serviceand letting clientsallWidgets: Ember.inject.service(). It's not really a Service, but this gets me lots of work for free. - creating a
Ember.Objectinstance in theinstance-initializerand injecting it into the places that need it. This requires moving the answer to does this object need allWidgets? out of the class file and into the initializer, which is sad.
I made simple style changes (mostly adding
jsto code snippet so you get syntax highlighting & commenting out file names in code where it was not) in a fork. I think it's a good post. I do think that it might be worth it to let the readers know that "this blog post will walk the reader through design choices made to create an internationalized widget" or somesuch.This might be super nitpicky but have you considered iteration n instead of solution n where n is an integer for the sub-headings. Given that it's an emergent design, iteration has a better feel to it but it might just be me. I also think Solution 3 is wanting to be a conclusion. Maybe a what's next section name better communicates it's intent?