This guide uses Ember version 1.13.
I recently started a new Ember 1.13 app to explore Google’s Material Design Lite style library. I use Ember in my day job, too, and something that has always stuck out to me as “should be easy but isn’t” in Ember is making a button component that calls an action.
I wanted a “continue button” component that…
- can be re-used throughout my app
- calls an action customized per route (ie: the continue button on the ‘basics’ route goes to the ‘location’ route, the continue button on the ‘location’ route goes to the ‘students’ route, etc)
This was surprisingly unintuitive in Ember. I expected to pass an action into the component from the parent template and have the component know where to find that action.
What not to do
I think many people who try to put an action on a component start with something like this. Note that this approach doesn’t actually work:
badExampleRoute.hbs
{{bad-example-button action=”wontWork”}}
badExampleRoute.js
actions: { wontWork() { console.log("you'll never see this!"); } }
What’s missing? The call to the route’s action has to come from the component itself.
The rest of this guide will show you what does work for getting a component to fire an action on the parent’s route in Ember.
Generating the button component
Do this step inside your Ember project directory (assuming you are using ember-cli):
ember generate component continue-button
In the route’s template
First, add the component into your .hbs template. The action it calls, “continue”, needs to be defined in the action hash in this route’s .js file.
basics.hbs
{{continue-button actionToCall="continue"}}
In the route’s js file
In your route’s .js file, create an action hash if you don’t have one already (actions: {…}) and put a “continue” method inside it. I like to stick a console log in here, too, on the first try so I have something to look for in Chrome to confirm it’s working.
basics.js
actions: { continue() { console.log("continuing on to location page"); this.replaceWith('location'); } }
Code for the button component
When you generated the button component you got an .hbs template file and a .js file.
continue-button.hbs
The important piece here is that the <button> tag contains {{action “doButtonThing”}}.
<button {{action "doButtonThing"}} class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent">Continue</button>
continue-button.js
doButtonThing is an action on the component’s own action hash. When you call doButtonThing, it’s going to fire a sendAction that references the actionToCall defined up on the component in the route template.
import Ember from 'ember'; export default Ember.Component.extend({ actions: { doButtonThing() { this.sendAction('actionToCall'); } } });
Yes, this is confusing as #$*! and I’ve not seen a better way to do this in my year or so of developing in Ember. It feels very much like a child (the component’s .js) is telling a parent (the route’s .js) what to do.
Another way to think of it
The {{continue-button …}} up in basics.hbs knows what to do when clicked (call “continue” action in basics.js) but it’s not going to do it until told to do it. It sits around listening for the “fire!” command.
When the user clicks the button, the code on the button component itself has its own action to call. That action yells “fire!” to the instance of the button waiting up there on the route. The {{continue-button …}} up in basics.hbs hears the fire command, delivered from the component itself in the form of a send-action.
There is no “passing” of an action into the component, it’s more like a pairing of broadcaster and listener.
The Ember community calls the larger concept at work here “data down, actions up” which took me a while to internalize. You can read more about DDAU here.