--- title: Compose Helpers for Reusable Logic impact: MEDIUM-HIGH impactDescription: Better code reuse and testability tags: helpers, composition, functions, pipes, reusability --- ## Compose Helpers for Reusable Logic Compose helpers to create reusable, testable logic that can be combined in templates and components. **Incorrect (logic duplicated in templates):** ```glimmer-js // app/components/user-profile.gjs {{uppercase (truncate @user.name 20)}} {{#if (and @user.isActive (not @user.isDeleted))}} Active {{/if}} {{lowercase @user.email}} {{#if (gt @user.posts.length 0)}} Posts: {{@user.posts.length}} {{/if}} ``` **Correct (composed helpers):** ```javascript // app/helpers/display-name.js export function displayName(name, { maxLength = 20 } = {}) { if (!name) return ''; const truncated = name.length > maxLength ? name.slice(0, maxLength) + '...' : name; return truncated.toUpperCase(); } ``` ```javascript // app/helpers/is-visible-user.js export function isVisibleUser(user) { return user && user.isActive && !user.isDeleted; } ``` ```javascript // app/helpers/format-email.js export function formatEmail(email) { return email?.toLowerCase() || ''; } ``` ```glimmer-js // app/components/user-profile.gjs import { displayName } from '../helpers/display-name'; import { isVisibleUser } from '../helpers/is-visible-user'; import { formatEmail } from '../helpers/format-email'; {{displayName @user.name}} {{#if (isVisibleUser @user)}} Active {{/if}} {{formatEmail @user.email}} {{#if (gt @user.posts.length 0)}} Posts: {{@user.posts.length}} {{/if}} ``` **Functional composition with pipe helper:** ```javascript // app/helpers/pipe.js export function pipe(...fns) { return (value) => fns.reduce((acc, fn) => fn(acc), value); } ``` **Or use a compose helper:** ```javascript // app/helpers/compose.js export function compose(...helperFns) { return (value) => helperFns.reduceRight((acc, fn) => fn(acc), value); } ``` **Usage:** ```glimmer-js // app/components/text-processor.gjs import { fn } from '@ember/helper'; // Individual helpers const uppercase = (str) => str?.toUpperCase() || ''; const trim = (str) => str?.trim() || ''; const truncate = (str, length = 20) => str?.slice(0, length) || ''; {{! Compose multiple transformations }} {{pipe @text (fn trim) (fn uppercase) (fn truncate 50)}} ``` **Higher-order helpers:** ```javascript // app/helpers/partial-apply.js export function partialApply(fn, ...args) { return (...moreArgs) => fn(...args, ...moreArgs); } ``` ```javascript // app/helpers/map-by.js export function mapBy(array, property) { return array?.map((item) => item[property]) || []; } ``` ```glimmer-js // Usage in template import { mapBy } from '../helpers/map-by'; import { partialApply } from '../helpers/partial-apply'; {{! Extract property from array }} {{#each (mapBy @users "name") as |name|}} {{name}} {{/each}} {{! Partial application }} {{#let (partialApply @formatNumber 2) as |formatTwoDecimals|}} Price: {{formatTwoDecimals @price}} {{/let}} ``` **Chainable transformation helpers:** ```javascript // app/helpers/transform.js class Transform { constructor(value) { this.value = value; } filter(fn) { this.value = this.value?.filter(fn) || []; return this; } map(fn) { this.value = this.value?.map(fn) || []; return this; } sort(fn) { this.value = [...(this.value || [])].sort(fn); return this; } take(n) { this.value = this.value?.slice(0, n) || []; return this; } get result() { return this.value; } } export function transform(value) { return new Transform(value); } ``` ```glimmer-js // Usage import { transform } from '../helpers/transform'; function filter(items) { return items .filter((item) => item.active) .sort((a, b) => a.name.localeCompare(b.name)) .take(10).result; } {{#let (transform @items) as |t|}} {{#each (filter t) as |item|}} {{item.name}} {{/each}} {{/let}} ``` **Conditional composition:** ```javascript // app/helpers/when.js export function when(condition, trueFn, falseFn) { return condition ? trueFn() : falseFn ? falseFn() : null; } ``` ```javascript // app/helpers/unless.js export function unless(condition, falseFn, trueFn) { return !condition ? falseFn() : trueFn ? trueFn() : null; } ``` **Testing composed helpers:** ```javascript // tests/helpers/display-name-test.js import { module, test } from 'qunit'; import { displayName } from 'my-app/helpers/display-name'; module('Unit | Helper | display-name', function () { test('it formats name correctly', function (assert) { assert.strictEqual(displayName('John Doe'), 'JOHN DOE'); }); test('it truncates long names', function (assert) { assert.strictEqual( displayName('A Very Long Name That Should Be Truncated', { maxLength: 10 }), 'A VERY LON...', ); }); test('it handles null', function (assert) { assert.strictEqual(displayName(null), ''); }); }); ``` Composed helpers provide testable, reusable logic that keeps templates clean and components focused on behavior rather than data transformation. Reference: [Ember Helpers](https://guides.emberjs.com/release/components/helper-functions/)
{{lowercase @user.email}}
{{formatEmail @user.email}}