Contextual Helpers, Composable Helpers, or Helper Macros (!?)


#1

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 yields barDataMaker, which is a new anonymous helper that is returned by the bar-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.


#2

In my tweet above I was talking about a slightly different issue, which is invoking a helper from within another helper’s implementation. For example, we could make a this.invokeHelper method that is only callable from within a helper’s compute method. By tracking which helpers get invoked during a given run, we could maintain full visibility of the data flow graph in order to do invalidations.


#3

We also definitely want locally scoped helpers (and components), which I believe is already covered within the latest pods rfc.


#4

Perhaps what I suggested wasn’t totally clear — but I think that’s what I’m getting at. So, my example above would become something like:

export default Ember.Helper.extend({
  compute([height, width, data], opts) {
    let xScale = this.invokeHelper('ordinal-scale', [data, [0, width]], opts);
    // ...etc
  }
});

Is that about what you’re describing?


#5

Also, locally scoped helpers is certainly one thing — but what about the notion of an anonymous/curried helper as described above? (aka: a helper that returns/yields another helper). Then, we could do something like this:

{{#with (add-number 3) as |adds3|}}
  {{adds3 5}} {{!-- prints 8 --}}
{{/with}}

…alternatively…

{{#add-number 3 as |adds3|}}
  {{adds3 5}} {{!-- prints 8 --}}
{{/add-number}}

Obviously, this is a pretty trivial example but would be tremendously useful in creating a really robust template-driven composable visualization library.


#6

It sounds like you’re thinking of invokeHelper as analogous to the existing component helper for currying. I was actually suggesting it does the invocation and returns the helper’s output.

Contextual helpers would be good too, but for consistency with contextual components, I think they should be done the same way, like {{yield (helper "my-helper" foo=bar) }}. This would compose nicely with local lookup, letting you grab a locally scoped helper and curry some state into it.

Arguably the component keyword could be made to do double duty and not care about the distinction between helpers and components. That is mostly an API design question.


#7

So, I think I get where you’re at with this.invokeHelper and with the contextual helpers being more or less the same as contextual components. But I guess as I go back to my original example, what’s still missing is still the ability to construct a contextual helper from within another helper.

If the helper helper could accept a Helper class rather than a string for a named helper, then I think we’re pretty darn close to what I think would work…because, then I could do something like this to return a contextual helper from a helper:

// helpers/foo.js
export default Helper.extend({
  compute([foo]) {
    let oof = foo.reverse();
    return this.invokeHelper('helper', [
      Helper.helper(([bar]) => {
        return `${oof} - ${bar}`;
      })
    ]);
  }
});

And, invoking would be:

{{#with (foo 'notlimah') as |hamilFoo|}}
  {{hamilFoo 'the musical'}} {{!-- prints: 'hamilton - the musical' --}}
  {{hamilFoo 'is awesome'}} {{!-- prints: 'hamilton - is awesome' --}}
{{/with}}

…and I suppose this could go both ways where invokeHelper could also accept a contextual helper as its first argument? For example (though, not really sure what the use case for this might be):

{{foo 'foo1' bar=(helper 'baz' 'foo2')}}
// helpers/foo.js
export default Helper.extend({
  compute([text], { bar }) {
    return this.invokeHelper(bar, [text]);
  }
});

Is this on the right track and or useful?


#8

Yes, I think you’re on the right track.

I’m not sure that it’s worth supporting anonymous helpers like Helper.helper(([bar]) => { return{oof} - {bar}; }) in your example. That could just as easily be a locally scoped helper with a regular name, in its own file.

And the benefit of doing it that way is that it keeps the implementation of helpers safely decoupled from the public API. It is intentional that today you can’t instantiate a component or helper “by hand” and then pass it into the templating system. That leaves us free to make radical changes to the implementation (like Glimmer 2) without breaking public API.


#9

The only reason we do not have contextual helpers is the tricky API design.

To understand the weird bit, lets look at how contextual components were designed. Before contextual components invocation of a component looked like this:

{{component-name}}
{{component 'component-name'}}
{{component bindingOfComponentName}}

Conveniently, none of these cases are a case where a component is passed to something else. Thus adding these cases where a component is passed made a lot of sense:

{{yield (component 'component-name')}}
{{other-component header=(component bindingOfComponentName)}}

In these cases we were permitted to use the component helper since there was no possible confusion. If used as {{component we must mean invocation, if used as (component we must mean a contextual component.

So let’s look at contextual hepers.

Current invocations looks like this:

{{name-of-helper 'foo'}}
{{(name-of-helper)}} {{! w/o arg you need parens, but IMO this is a bug, I'll file an issue if it is still an issue today }}
{{other-component age=(name-of-helper)}}

Components can do two things not possible with helpers: They can be invoked dynamically, and they can be contextualized. Unfortunately when we add these two APIs to a (helper method the designs are in conflict.

For example invocation would need to provide helper invocation support for these cases:

{{helper 'helper-name'}}
{{helper bindingOfHelperName}}
{{other-helper age=(helper 'helper-name')}} {{! helper-name result passed as age }}
{{other-helper age=(helper bindingOfHelper)}}

However a contextual helper would also be valid in these cases:

{{other-helper age=(helper 'helper-name')}} {{! helper-name is closed over, contextual }}
{{other-helper age=(helper bindingOfHelper)}}

So how we do decide which behavior to use when? There are obviously a number of ways to slice it, several different compromises or tradeoffs that could be made. I would love to open an RFC for this soon so we can land it around the time local lookup becomes available!

I’m much less wild about invokeHelper, but lets keep up the brainstorming there.


#10

Awesome — yea, thinking it through the ways I’d want to use invokeHelper, you’re right: a separate named helper would be sufficient.


#11

Yea — In thinking through this a bit, I had the same thought and question. Perhaps a less than ideal solution would be to create separate “helper helpers” (<— heh.) — one for invocation, another for closing over.

i.e.:

{{helper 'helper-name'}} {{! invokes }}
{{partial-helper 'helper-name'}} {{! closes over }}

The problem here of course is that invoking partial-helper directly like this would appear to do something, but would do almost nothing. I can see how that would lead to some confusion when trying to teach what to use when.

Another idea might be to instead leave it up to the helper class to determine when it should be closed over vs. invoked by checking wether the interface is fully satisfied. This probably has some implications for the Helper class that might be too extreme, but maybe worth a thought. So, for example (consider this pseudo-code-ish):

export default Helper.extend({
  compute: Helper.requires('a', 'b', 'c', 
    ([a, b], { c, d }) => { /* do the thing */}
  )
});

Then — this helper would only compute a, b, & c have all been provided through whatever combination of partials. While this might be a little harder to reason about, it at least becomes a opt-in for behavior that goes against the standard guarantees in the template.


I’m curious too why you’re less wild about the invokeHelper?

Maybe this is a larger conversation and maybe unrelated to how you’re thinking about it, but I’ve been seeing a lot of folks say something to the effect of: “Use helpers, but if they’re getting gnarly, use computed macros in your component”.

I think this is a very reasonable recommendation…I already have a few places where my helpers have gotten out of hand and I needed to factor it into my component just for sake of readability.

But, I think this is becoming increasingly common and I feel unsatisfied when the solution is “just make it a computed” — especially because that might mean that I will throw away the addons I’ve been using and find an entirely different solution.

I think the helper is super approachable, enforces good side-effect-free practices, is easily testable, etc. I think it checks off a ton of boxes in the learning department…except that it just doesn’t seem to scale like the other parts of Ember. And, frankly, computed macros are a much harder concept for people to learn.

Anyway, any insight might be valuable here as I think this might keep coming up as more and more folks embrace the helper!


#12

@Spencer_Price you’re basically voicing the challenge of (helper really well :slight_smile: I think I’ll have some time post-EmberConf and would like to move this forward. Keep thinking it over.

I’m not too interested in debating the merits of invokeHelper yet- It seems good to let the idea breathe and get more feedback :thumbsup: I’ll be thinking about it while I use helpers at work this week! Definitely interesting.


#13

I had similar thoughts (though I was thinking wrap-helper for the lazily invoked version).


#14

Cool beans. Well, hopefully there are some others who have feelings one way or the other who might have some ideas or thoughts to contribute so there’s a better sense of the scope of what the use cases for something like this might be.

That’s right, you, Ms./Mr. Thread Lurker, please, share your thoughts!


#15

Toran just added a blog post to ember-redux addon that I think is relevant to his conversation — he’s invoking Dan Abranov’s blog from a while ago about the two types of Components: Presentation and Container Components.

When I read this, it made me think that I’m thinking of Helpers as the pure function breed of Container Components. Maybe this becomes moot if/when the performance overhead for creating a component instance goes away with Glimmer 2 (I think that’s what Yehuda said was part of the plan?).

But, for now, the cost of creating a component that has no presentation concerns is too high, especially with 1,000s of components. Helpers don’t seem to have the same overhead which is why I’ve reached for them — but they also don’t have the same amount of flexibility (see above).

So, maybe what I’m looking for is not a more composable helper, but a light-weight, presentation-less component.


#16

There is now an RFC opened on this topic: https://github.com/emberjs/rfcs/pull/208