Using simple functions as helpers in Ember.js

13 November 2023

Possibly one of the least known and most useful things in Ember is the ability to use simple functions as template helpers which was introduced in Ember 4.5.

Before, if you wanted to use a function in the template, you needed to create a helper after which you could use it in all of your templates. If you defined a format-currency helper, you could call it as {{format-currency value}} from everywhere.

Handy, but I can think of two downsides. First, you need a special kind of function, a helper which is an Emberism. Second, and I think this is the bigger downside, it's hard to say where a certain helper comes from. It could've been created by one of the developers of the app in which case you'd find the helper in app/helpers. But it also could be in any of your dependencies and good luck searching through all of them.

Let's see an example of the modern, more explicit way.

A simple currency converter

Let's say we want to display currencies, select a base currency, and display the value of each in the base currency.

We start by just having the conversion rates (compared to the EUR), and listing the currencies and a dropdown to change the base currency:

 1import Component from '@glimmer/component';
 2
 3export default class CurrencyConverterComponent extends Component {
 4  currencies = {
 5    EUR: 1,
 6    USD: 1.06,
 7    GBP: 0.87,
 8    CHF: 0.95,
 9    BTC: 0.000036,
10  };
11}
 1<div class="currencies-container">
 2  <h1 class="header">
 3    <label for="selected-currency">Currencies</label>
 4    <select
 5      id="selected-currency"
 6      name="selected-currency"
 7      class="currency-dropdown"
 8    >
 9    {{#each-in this.currencies as |symbol|}}
10      <option>{{symbol}}</option>
11    {{/each-in}}
12    </select>
13  </h1>
14
15  <ul class="currencies">
16    {{#each-in this.currencies as |symbol rate|}}
17      <li class="currency-price">
18        {{symbol}}
19        <span>{{rate}}</span>
20      </li>
21    {{/each-in}}
22  </ul>
23</div>

Simple list of currencies

Simple enough, but we're missing the ability to change the base currency, so let's make some changes.

Allowing to change the base currency

 1export default class CurrencyConverterComponent extends Component {
 2  eurRates = {
 3    EUR: 1,
 4    USD: 1.06,
 5    GBP: 0.87,
 6    CHF: 0.95,
 7    BTC: 0.000036,
 8  };
 9
10  @tracked baseCurrency = 'EUR';
11
12  get currencyRates() {
13    return Object.entries(this.eurRates).map(([currency, rate]) => {
14      return {
15        symbol: currency,
16        rate: rate / this.eurRates[this.baseCurrency],
17      };
18    });
19  }
20
21  @action
22  updateBaseCurrency(event) {
23    this.baseCurrency = event.target.value;
24  }
25}

Before we'd assumed the base currency would always be the EUR but if it can be changed, we need to show the rates compared to the base currency which is what currencyRates does.

The template also needs to change, though only slightly:

 1<div class="currencies-container">
 2  <h1 class="header">
 3    <label for="selected-currency">Currencies</label>
 4    <select
 5      id="selected-currency"
 6      name="selected-currency"
 7      class="currency-dropdown"
 8      {{on "change" this.updateBaseCurrency}}
 9    >
10    {{#each this.currencyRates as |currency|}}
11      <option>{{currency.symbol}}</option>
12    {{/each}}
13    </select>
14  </h1>
15
16  <ul class="currencies">
17    {{#each this.currencyRates as |currency|}}
18      <li class="currency-price">
19        {{currency.symbol}}
20        <span>{{currency.rate}}</span>
21      </li>
22    {{/each}}
23  </ul>
24</div>

We want to bind the selected option of the dropdown to the base currency. The classic way to do this would be using the eq helper which you can either use from ember-truth-helpers or write yourself.

However, there is a more descriptive way (and this is a blog post about using simple functions as helpers, so let me have it my way), by writing a local helper function:

 1export default class CurrencyConverterComponent extends Component {
 2  eurRates = {
 3    EUR: 1,
 4    USD: 1.06,
 5    GBP: 0.87,
 6    CHF: 0.95,
 7    BTC: 0.000036,
 8  };
 9
10  @tracked baseCurrency = 'EUR';
11
12  isBaseCurrency = (currency) => {
13   return currency === this.baseCurrency;
14  };
15
16  // (...)
17}
 1<select
 2  id="selected-currency"
 3  name="selected-currency"
 4  class="currency-dropdown"
 5  {{on "change" this.updateBaseCurrency}}
 6>
 7  {{#each this.currencyRates as |currency|}}
 8    <option selected={{this.isBaseCurrency currency.symbol}}>
 9      {{currency.symbol}}
10    </option>
11  {{/each}}
12</select>

We use a local function as a helper so we need to use the this prefix, just as when rendering a property or calling an action.

Formatting currencies (well, numbers)

The currencies I listed are all close to each other (with one exception) in value so it's not trivial to see but when we switch the base currency to BTC, we can see that bigger values are somewhat hard to read:

Big numbers, unformatted

We should add a formatter (helper) function to present the numbers is a nicer way:

1export default class CurrencyConverterComponent extends Component {
2
3  formatCurrency = (value) => {
4    // Format the value to two decimals and separate thousands by commas
5    return value.toFixed(2).replace(/\d(?=(?:\d{3})+\.)/g, '$&,');
6  };
7  // (...)
8}

There is some regex magic to add the commas between the thousands but that's not our focus today.

We now have a function we can call from the template:

1{{#each this.currencyRates as |currency|}}
2  <li class="currency-price">
3    {{currency.symbol}}
4    <span>{{this.formatCurrency currency.rate}}</span>
5  </li>
6{{/each}}

Note we call the formatCurrency method with this as it's defined on the component – it's not a global helper.

Nicely formatted currency values

Extracting the function to be reused

You might justifiedly ask: "all right, but what if I need the formatCurrency helper in other places of the app, too? Before, I could just define it once as a helper and then use it everywhere." Good point, you don't want to redefine the formatter every time you need it – but you don't have to.

The helper is just a regular function, so you can define it in a module and then import it each time you need it:

1// app/helpers/format-currency.js
2export function formatCurrency(value) {
3  return value.toFixed(2).replace(/\d(?=(?:\d{3})+\.)/g, '$&,');
4}
1import { formatCurrency } from 'currencies/helpers/currency';
2
3export default class CurrencyConverterComponent extends Component {
4  formatCurrency = formatCurrency;
5}

Assigning the imported function to a property on the component is needed because you can't simply use formatCurrency in the template.

At least not in current Ember, Octane. With single-file components, in Polaris, you can: give it a whirl using the ember-template-imports add-on.

Is there still a place for "classic" template helpers?

Before we could use simple functions as helpers, there were two types of Ember helpers: functional and class-based ones.

Functional helpers

Above I showed how to use a simple function in a helper/util module by importing it, assigning it to a component property and calling it from a template.

It's true this is more ceremony than defining a special, global helper but I think it's worth it because it is explicit, and it's just JavaScript. With the exception of needing to assign the function to a component property, you don't have to do anything about Ember to use this technique.

So, unless the helper is already defined in the project (via libraries like ember-truth-helpers, ember-composable-helpers, etc.), I see no reason to create and use global helpers.

Class-based helpers

We need a class-based global helper when we need some state to create the output. Some property that we can't, or don't want to pass in to the helper every time.

A translation helper, like t in ember-intl is a good example. You don't want to pass in the current locale needed for the translation every time you call the helper – the class implementing the helper can store it instead.

It's also a good example because under the hood, the helper makes use of a service, so we can do the same.

Let's see how that would be done on the previous example.

We define a session service that keeps track of the currently selected base currency throughout the application. All operations related to the management of this base currency will live on the service:

 1import Service from '@ember/service';
 2import { action } from '@ember/object';
 3import { tracked } from '@glimmer/tracking';
 4
 5export default class SessionService extends Service {
 6  @tracked currency = 'EUR';
 7
 8  formatCurrency(value) {
 9    const formatted = value.toFixed(2).replace(/\d(?=(?:\d{3})+\.)/g, '$&,');
10    return `${this.currency} ${formatted}`;
11  }
12
13  @action
14  setCurrency(currency) {
15    this.currency = currency;
16  }
17}

The formatCurrency helper can now use the base currency (this.currency) which is defined on the service itself.

Because previously this responsibility was implemented in the component, we now need to pass the "value + updater" pair into the component:

1<CurrencyConverter
2  @baseCurrency={{get (service "session") "currency"}}
3  @onChange={{get (service "session") "setCurrency"}}
4/>

(I used the very handy ember-service-helper add-on so as not to create a controller for the service injection.)

The component class needs to change to not manage the base currency itself:

 1import Component from '@glimmer/component';
 2import { action } from '@ember/object';
 3import { service } from '@ember/service';
 4
 5export default class CurrencyConverterComponent extends Component {
 6  @service session;
 7
 8  eurRates = {
 9    EUR: 1,
10    USD: 1.06,
11    GBP: 0.87,
12    CHF: 0.95,
13    BTC: 0.000036,
14  };
15
16  isBaseCurrency = (currency) => {
17    return currency === this.args.baseCurrency;
18  };
19
20  formatCurrency = (value) => {
21    return this.session.formatCurrency(value);
22  };
23
24  get currencyRates() {
25    return Object.entries(this.eurRates).map(([currency, rate]) => {
26      return {
27        symbol: currency,
28        rate: rate / this.eurRates[this.args.baseCurrency],
29      };
30    });
31  }
32
33  @action
34  updateBaseCurrency(event) {
35    this.args.onChange(event.target.value);
36  }
37}

Inserting the base currency for no good reason

(The example is contrived because there is no place for the base currency to be included in each value. A better example would be an app for finding flight tickets where the user can set a currency preference and wants to see all prices in that currency.)

Why do we still need the formatCurrency method on the component? Couldn't we use this.session.formatCurrency directly in the template?

No, we couldn't: this.session.formatCurrency is a function and its context is not bound to the component instance so the this in the service would be undefined during the call.

A note on currency formatting

One of my readers, Johan Roed, points out that we could use Intl.NumberFormat to do the currency formatting. I prefer this solution to using a regex indeed, as it's simpler and also adjusts to user preferences. Thanks, Johan!

Which one to use?

The decision to use a global, class-based Ember helper or a method on a service and a thin wrapper around it is harder to make than in the functional helper case. I'm still learning toward the latter because of its explicitness and the fact that you don't have to learn about a special Ember thing.

When single-file components become the norm in Polaris, we'll have to import all the helpers anyway so the decision will be an easy one to make :)

Share on Twitter