Brief history of passing actions in components

The ability to pass actions to components is not a new feature for component use. The previous action system used bubbling to pass user behavior to a parent scope. For example, a user clicks a button within a component, this then triggers an action which bubbles through several controllers to finally be handled on a route. This way of bubbling actions was challenging to debug and required a good amount of boilerplate code. Looking forward, Ember was geared to become more component-driven and delivered a cleaner and more flexible way to pass functions with the Ember 1.13.0 Closure Actions.

First let’s take a look at how we used to bubble actions

  // /user/route.js

    count: 1,

    actions: {
      addOne() {
        this.set('count', this.get('count') + 1);
      }
    }

In our user route we have a property count, and an action addAction to increment that property.

  
  // /user/template.hbs

    {{done-button add='addOne' count=count }}
    

Here we call our component and bind add to the addOne function and bind our count property object to count that property.

  
    // /components/done-button/template.hbs

      <h3> Total {{count}} </h3>
      <button {{action 'add'}}> Add One </button>
  

Our Done button will now trigger the add action within our done-button/component.js.

  // /components/done-button/component.js

  actions: {
    add() {
      this.sendAction('add');
    }
  }

Here add will send our bound action addOne and bubble until finding the action location and trigger within the user/route.js. Because we have to bubble these actions up through each layer of the application (until we reached the top), nested layers of your application required a lot of boilerplate code. Every component requires an action to be defined so that it could simply be sent to another action. Now let’s compare this to using closure actions.

Closure actions

With closure actions, we will maintain our addOne action in our route and pass that action into the component such that the current scope of the action gets passed down to the component as well. Then, we can trigger the invocation of this action directly from the component. We can do this by accessing the attrs property of the component. The attrs contain all of the attributes passed into a component, including closure actions.

Our user/route.js and our done-button/template.hbs will remain the same, however, we will need to modify our user/template.hbs and done-button/component.js.

  
  // /user/template.hbs

    {{done-button add=(action 'addOne') count=count }}
  

Our new action helper takes the name of our action as a parameter, in our case addOne. The helper returns the addOne function that we defined in the user/route.js, wrapped in the current scope. Basically we are giving our done-button component an attribute called add and setting it equal to the addOne function and an object attribute of count set equal to the count property we defined in the user route. The done-button component now has access to the addOne action in our user/route.js and the scope that the addOne action has access to in the parent template.

We can now access these attributes within our component by calling this.attrs. Let’s take a closer look.

  • this is the done-button component itself.
  • .attrs are the attributes of count and add that we assigned in our done-button component call in our parent template user/template.js.

If we were to debug into our this.attrs, here is what we would see: our count object attribute and our addOne function attribute.

  
  > this.attrs
  Object {count: Object}
  count: Object
  addOne: ()
  __proto__: Object
  

Let’s update our add function to take advantage of our new attrs. Now when the Done button is clicked and we enter the add action, we will execute the passed-in action function addOne directly from the child component.

  // /components/done-button/component.js

  actions: {
    add() {
      this.attrs.addOne();
    }
  }

Closure actions allow us to define actions and use them across components. They help us avoid boilerplate code and expose isolated components to the context of parent templates, allowing actions to be passed down through a series of nested components and triggered through this.attrs.

NOTE: All code examples use the --pod structure. Actions are handled within routes.