By Oleksii Rudenko October 27, 2015 5:26 PM
Ember Way to Format Data Using Dynamic CPs, Services and Helpers

In multi-language apps it’s often required to format various data according to the selected language or locale. The natural choice to format values before showing them to users is to use a helper that accepts a raw value and outputs a formatted one. There is one problem with this approach and you immediately notice it if you need to use formatted values inside your models, components, controllers or routes — there is no simple way to re-use a helper outside of templates.

I propose to follow the following pattern that allows re-using formatting functions in a nice way.

Solution

First, use simple functions to implement formatting logic. For example,

//format-utils.js
export var formatPhone = function(value, locale) {
  return locale + ':' + value;
}

or

//format-utils.js
export var formatDate = function(value, format, locale) {
  return locale + ':' + moment(value).locale(locale).format(format) + ' in ' + format;
}

Second, have a service that holds the global state that determines how formatting is done:

//services/locale.js

export default Ember.Service.extend({
  locale: 'en',
  setLocale(locale) {
    this.set('locale', locale);
  }
});

Third, add Ember helpers that use previously defined functions to format values:

// helpers/format-phone.js
var { observer } = Ember;
import { formatPhone } from './format-utils';

export default Ember.Helper.extend({
  localeService: Ember.inject.service('locale'),
  onLocaleChange: observer('localeService.locale', function() {
    this.recompute();
  }),
  compute(value) {
    return formatPhone(value, this.get('localeService.locale'));
  }
});

and

// helpers/format-date.js
var { observer } = Ember;
import { formatDate } from './format-utils';

export default Ember.Helper.extend({
  localeService: Ember.inject.service('locale'),
  onLocaleChange: observer('localeService.locale', function() {
    this.recompute();
  }),
  compute(value, format) {
    return formatDate(value, format, this.get('localeService.locale'));
  }
});

Fourth, create a custom macros similar to Ember.computed:

// format-macro.js
import { formatPhone as formatPhoneFunc } from './format-utils';
var { get, computed } = Ember;

// returns a computed property that is recomputed upon changes to locale or valueKey
export var formatPhone = function(valueKey) {
  let deps = [ 'localeService.locale', valueKey];
  return computed(...deps, function() {
    let locale = get(this, 'localeService.locale');
    return formatPhoneFunc(get(this, valueKey), locale);
  });
}

and

import { formatDate as formatDateFunc } from './format-utils';
var { get, computed } = Ember;

// returns a computed property that is recomputed upon changes to locale or valueKey
export var formatDate = function(valueKey, format) {
  let deps = [ 'localeService.locale', valueKey];
  return computed(...deps, function() {
    let locale = get(this, 'localeService.locale');
    return formatDateFunc(get(this, valueKey), format, locale);
  });
}

Usage

Use macro in your controllers/components/models/routes:

import { formatDate, formatPhone } from './format-macros';

export default Ember.Controller.extend({
  // the service that holds the global state needs to be injected too
  localeService: Ember.service.inject('locale'),
  formattedPhone: formatPhone('model.phone'),
  formattedDate: formatDate('model.date', 'L'), // L - long local format
});

And in your templates:

{{formattedPhone}}
{{formattedDate}}
{{formatPhone '555-555-555'}}
{{formatDate model.date 'L'}}

The nice bonus of this implementation is that whenever you change your global state (e.g. locale) all formatting is changed automatically everywhere in the app.

The live demo is available here: jsbin.

Thanks for reading.

P.S. You might not need your own service for handling locales if you use ember-i18n.

P.P.S. Related blog post: Services and Self-Recomputing Helpers in Ember 2.0

** The post has been updated on 2.11.2015 to remove redundant methods of the Locale service. Also see the discussion in the comments whether having so many entities for formatting is viable.**