Ember.js: Handling undefined closure actions passed down as attributes

Created on 17 Dec 2015  路  8Comments  路  Source: emberjs/ember.js

Sometimes users may only want/need to handle a subset of the actions sent up by a component. For example, imagine a component that sends up actions click-a and click-b like so:

// controller.js
export default Ember.Controller.extend({
  actions: {
    handleClickOnA() {}
  }
});
<!-- template.bhs -->
{{my-component click-a=(action "handleClickOnA")}}
<!-- my-component.hbs -->
<button onclick={{action (action attrs.click-a)}}>Button A</button>
<button onclick={{action (action attrs.click-b)}}>Button B</button>

In this case, I only care about handling a click on button A, so I'm only passing the click-a attribute down into my component. However, Ember throws a vague error (Cannot read property 'INVOKE [id=ember12345...]' of null) when it encounters the action handler on button B in the template for the component, because attrs.click-b is undefined. (I've made an Ember Twiddle if you want to see this in action: https://ember-twiddle.com/b36807b311324b1be59a)

The two workarounds to this are both undesirable in my opinion. (1) I can specifiy click-b when instantiating the component in template.hbs and map it to an empty dummy action in my controller. This is less than ideal, because it's a bunch of useless and thus potentially misleading code. (2) I can handle the click on button B via an action in the component.js file itself. There, I'd check to see if attrs.click-b was specified, and call it if so. So I'm merely implementing a middle-man to forward the action up the chain, which gets tedious if I have many nested components, and is exactly what I thought closure actions were meant to prevent.

I guess the downside to allowing action handlers to be optional would be that users are now at risk of simply forgetting to define these action handlers. But could we just produce a warning in the console instead of throwing an error?

All 8 comments

Can you create a simple https://ember-twiddle.com/ to demonstrate? It would greatly speed our ability to check this out. Thanks!

@courthead the error is poor, but the idea that you cannot provide a null action is intentional for the reason you mention yourself. I am 100% in favor of improving the error.

Some options present today to get the optional action behavior you want:

Handle the optional action in your component, and there check to see if attrs.someAction is present before calling it.

You mention this, and IMO it is what I expect most people to reach for today.

{{! app/templates/component/my-component.hbs }}
<button onclick={{action 'clickB'}}>Button B</button>
// app/components/my-component.js
import Ember from 'ember';

export default Ember.Component.extend({
  actions: {
    'clickB': function() {
      let action = this.get('clickB');
      if (action) { return action(...arguments); }
    }
  }
});

Use a no-op helper

This is a bit lazy, but if you use the optional action pattern a lot perhaps is makes sense

{{! app/templates/component/my-component.hbs }}
<button onclick={{action (if attrs.clickB attrs.clickB no-op)}}>Button B</button>
// app/helpers/no-op.js
import Ember from 'ember';

export default Ember.Helper.helper(function(){
  return function() {};
});

Possible ways to make this better

  • Allow if in tag bodies. <button {{#if attrs.clickB}} onclick={{action attrs.clickB}} {{/if}}>Button B</button>. The trick here is that the pattern is not applicable to {{ components.
  • Add no-op or something like it as an Ember helper.
  • Allow null to be a no-op (I'm not crazy about this one myself, it would be painful w/ typos)
  • Quite hard, but make unbalanced templates work: {{#if attrs.clickB}}<button onclick={{action attrs.clickB}}{{else}}<button>{{/if}}Button B</button> This doesn't solve the use-case of a many optional listeners on an element, though it does read very clearly.
  • Add an option to {{action: <button onclick={{action attrs.clickB optional=true}}>Button B</button> Actions already have several options, and though a solution composing from current helpers would be more elegant this solution is pretty simple.

Hope this helps, I'll ponder it more. The use-case has been raised a few times now.

You can also make a noop function helper so that you can always use the action helper, but it doesn't error when attrs.foo isn't passed in.

Thanks, your suggestions have been super helpful! I'll go with the no-op helper for now. I like that it makes it explicit right in the template that the action is optional, and that it's a relatively small amount of code. Also, for similar reasons, of all the possible solutions suggested by @mixonic, I like the optional=true option for the action helper.

@mixonic FWIW I couldn't get the no-op helper to work when trying to allow optional actions on components (rather than on HTML elements), so I went with this:

{{! app/templates/components/some-thing.hbs }}
{{my-component click=(action (optional-function attrs.clickB))}}
// app/helpers/optional-function.js
import Ember from 'ember';

export default Ember.Helper.helper(optionalFunction(orderedArgs/*, namedArgs*/) {
  const actionFn = orderedArgs[0];
  return function (event) {
    return actionFn ? actionFn(event) : undefined;
  };
});

I think the noop helper is meant to be used as:

{{my-component click=(noop attrs.clickB)}}

Then, whenever you want to use the action you do this.attrs.click().

Also note if you're using Dockyard's ember-composable-helpers there is an optional action helper provided. https://github.com/DockYard/ember-composable-helpers#optional

I recognize this is pretty ancient, but is there a better way under octane/glimmer to handle this use-case? We use this approach alot (passing an action means the template item is clickable, otherwise just for display, etc.) - right now we no-op as described but wondering if there's now a better way.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fivetanley picture fivetanley  路  44Comments

olivia picture olivia  路  36Comments

ctataryn picture ctataryn  路  33Comments

QuantumKing picture QuantumKing  路  33Comments

robharper picture robharper  路  75Comments