Helpers are becoming more-and-more used in Ember templates and as far as I can tell, there is not a great way to compose helpers together. As Edward Faulkner put it,
I think we need a story for composing helpers directly out of other helpers.
I agree. Because, let’s be honest, stuff like this is not easy to read (but super powerful!):
{{hash
name="parent"
children=(array
(hash
name="child1"
)
(hash
name="child2"
)
)
}}
And, with addons like ember-composable-helpers, the need for a better composability story only increases. The only real alternative is to use computed properties inside of your component, but invoking a component has quite a bit of overhead if the goal is only to call a pure function.
So, before opening an RFC, I’d like to see if there’s any feedback that this discussion forum can provide to my proposal.
I think there’s a few things we could call this: “Composable Helpers”, “Helper Macros”, “Partial Helpers”, “Block Helpers”, “Anonymous Helpers” or “Contextual Helpers”. For now, let’s go with “Contextual Helpers” because I think that how I would want to use these aligns fairly closely with how contextual components.
I recently opened up an issue against a promising new visualization library, Maximum Plaid, which shows an example of how we might use something like this. Say we have a set of helpers (fyi: this exists) that we can use to put together the d3 scales needed to construct a simple bar chart. That template might look like this:
<svg width={{width}} height={{height}}>
{{#with (hash
xScale=(band-scale people (append 0 width))
yScale=(linear-scale
(append 0 (max people 'age'))
(append 0 height)
accessor='age'
)
) as |scales|}}
{{#each people as |person|}}
<rect
x={{compute xScale person}}
y={{subtract height (compute yScale person)}}
width={{compute xScale.bandwidth}}
height={{compute yScale person}}
/>
{{/each}}
{{/with}}
</svg>
Because d3 scales are just functions, to actually invoke them later on, we can use the compute
helper from ember-composable-helpers
. Like so:
x={{compute xScale person}}
But, ideally, we should be able to express xScale as a helper itself. The above would become:
x={{xScale person}}
…but this is not currently possible with Ember (as far as I can tell!)
So there’s two parts to this: I would like to be able to return a helper from another helper. Also, the complexity of the logic inside the {{#with}}
is quite cumbersome and not reusable, even though this pattern might exist everywhere we want to create a bar chart. Instead, I would love to be able to see the template above expressed like this:
<svg width={{width}} height={{height}}>
{{#bar-scales height width people y-accessor='age' as |barDataMaker|}}
{{#each people as |person|}}
{{#with (barDataMaker person) as |barData|}}
<rect
x={{barData.x}}
y={{barData.y}}
width={{barData.width}}
height={{barData.height}}
/>
{{/with}}
{{/each}}
{{/bar-scales}}
</svg>
In this template:
bar-scales
is my Contextual Helper which, like{{with}}
is expressed as a block.bar-scales
yieldsbarDataMaker
, which is a new anonymous helper that is returned by thebar-scales
helper.
If we were to peek inside that bar-scales
helper, it might look something like this:
import Ember from 'ember';
import { ordinalScale } from 'ember-d3-scale'; // another helper
export function barScales([height, width, data], opts) {
let xScale = ordinalScale([data, [0, width]], opts);
// etc...
return Ember.Helper.helper(([itemContext]) => {
return {
x: xScale(itemContext),
// ...etc
};
});
}
export default Ember.Helper.helper(barScales);
An RFC for this would require some Drawbacks and Alternatives that I have not really though through just yet. I’d love to get some feedback on this before going that route.
So, please, lend me your ears (Robin Hood: Men in Tights style) and your thoughts. I’d be curious too if y’all have any other ideas for how composed helpers could be used.