Today, we are going to continue iterating on our contact-manager application that we started in the first week. You can clone a copy of that repo at baroquon/contact-manager . We are going to start by moving some of the code into a component and creating an action on that component that is passed to the controller.
If you cloned that repo and haven't run it yet, you will need to install the dependencies. From the root of your newly cloned app:
npm install && bower install
Now we are ready to begin by starting our server.
$ ember s
First, we'll create a component with an action for that
sortBy
queryParam
that
we set up last time.
$ ember g component update-query-button
Now, open
app/templates/contacts.hbs
and add our new component. Components,
like HTML elements, come in both inline and block varieties. Let's use the inline
version in this case.
{{update-query-button}}
You might remember that components in Ember are generally composed of two files.
The JavaScript portion of the component lives in
app/components/update-query-button.js
and the corresponding markup lives in
app/templates/components/update-query-button.hbs
. Let's open the template and
make it look like we want.
You'll notice upon opening
update-query-button.hbs
that we have a
{{yield}}
.
That yield will render anything that is placed between block level component
tags. Since we are using this component as an inline component we can delete
that and replace it with:
<button>Sort by First Name</button>
If you go to your browser you will see that we now have a button that says 'Sort
By First Name'. But, of course, clicking that button doesn't do anything. To
solve that, we'll need to add an action. Since the
queryParams
need to be
updated in the controller, that is where our action will live. At the bottom of
app/controllers/contacts.js
let's add a property called
actions
that is just
a hash. We will also add a function
sortByFirstName
.
actions: {
sortByFirstName(){
this.set('sortBy', 'firstName');
}
}
Now, we need to give our component access to that action. We do this by passing that function to the component and allowing it to be called from there.
Back in
app/templates/contacts.hbs
add the following:
{{update-query-button updateSort=(action 'sortByFirstName')}}
As a side note, invoking an action in this way is called a closure action. The really cool thing about it is that, as it is just a function, we can get a return value from the parent's function. This has all sorts of cool implications which we will look at in episodes to come.
Now, in
app/components/update-query-button.js
add the action here that will
call the controller's action. This will look essentially the same as the action
on the controller.
actions: {
sendUpdate(){
this.get('updateSort')();
}
}
Finally, we need to call that local action in the component's template. In
app/templates/components/updateSort.hbs
add:
<button {{action 'sendUpdate'}}>Sort By First Name</button>
Now, your button will update your
queryParams
to sort by the
firstName
.
There are a lot of things happening here, so let's walk through them.
As you can probably tell, the action in the controller is what is actually
updating our
sortBy
property. In the contacts template, when we create the
component we are making that action available to our component with this code:
updateSort=(action 'sortByFirstName')
. We are saying: "Set the
updateSort
property on the component equal to the action named
sortByFirstName
". In
effect, we are just passing the function in the controller to the component to
be used.
In the component's template file where we have
<button {{action
'sendUpdate'}}>Sort By First Name</button>
. We are giving our button an action
that needs to be defined in the component named
sendUpdate
. In our component's
JavaScript, we are defining that action:
actions: {
sendUpdate(){
this.get('updateSort')();
}
}
When this action is triggered by the button click, it calls the
updateSort
function that we passed in from the controller.
What we have now will work. But, the component is not at all reusable and it only does one thing (and it does that clumsily) . We need to make our component more flexible. To do that, we will need to pass in a parameter that identifies what property we want to use to sort our list.
Let's start by making the component display a passed in property rather than static text. In our contacts template let's add a property to the component where we can pass in what sort value we want to display.
In
app/templates/contacts.hbs
change our component call to:
{{update-query-button sortValue="firstName" updateSort=(action 'sortByFirstName')}}
Now, in our component's template let's display that value.
<button {{action 'sendUpdate'}}>Sort By {{sortValue}}</button>
We are now displaying a specific value, we need to update the
sortBy
on the
controller with the same property. To to this we will modify, first our
component's action; then we'll modify the action on the controller.
In
app/components/update-query-button.js
change the action to look like this:
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
sendUpdate(){
const sortValue = this.get('sortValue');
this.get('updateSort')(sortValue);
}
}
});
Here we are getting the
sortValue
that we passed in to the component and passing
that as a param to the
updateSort
function. Finally, we just need to change the
action on the controller to accept our new param.
In
app/controllers/contacts.js
change your action to include this new param.
actions: {
sortByFirstName(sortValue){
this.set('sortBy', sortValue);
}
}
If we go back to our browser, our action should work just as it did previously.
Let's add two more instances of that component passing in other
sortValue
s and
see if it all works.
In
app/templates/contacts.hbs
add three more instances of our component and
pass in
lastName
,
email
, and
null
to clear the params.
{{update-query-button sortValue="firstName" updateSort=(action 'sortByFirstName')}}
{{update-query-button sortValue="lastName" updateSort=(action 'sortByFirstName')}}
{{update-query-button sortValue="email" updateSort=(action 'sortByFirstName')}}
{{update-query-button sortValue="null" updateSort=(action 'sortByFirstName')}}
Now, this will work. But, since we have made our action a bit more flexible, we
should really rename it. First, let's change our contacts template from
sortByFirstName
to
updateSortBy
.
{{update-query-button sortValue="firstName" updateSort=(action 'updateSortBy')}}
{{update-query-button sortValue="lastName" updateSort=(action 'updateSortBy')}}
{{update-query-button sortValue="email" updateSort=(action 'updateSortBy')}}
{{update-query-button sortValue="null" updateSort=(action 'updateSortBy')}}
Now, update the action in the controller:
actions: {
updateSortBy(sortValue){
this.set('sortBy', sortValue);
}
}
Let's make one more revision to our component. In Ember, by default components
will be rendered as divs. However, you can specify what your component's tag
name should be by setting the
tagName
property on the component. Since we are
only using our component as a specialized button, let's just make that the
tagName
. If we do that, we also need to change the way we are handling the
action. In
app/components/update-query-button.js
:
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'button',
click(){
const sortValue = this.get('sortValue');
this.get('updateSort')(sortValue);
}
});
To handle events on Ember components we just have to implement the event as a
method on the component. Since we already have a button now, we need to remove
the
<button>
in our component template. Let's just remove everything that is
presently in the component's template and add the handlebars
yield
back. This
will let us use this component just like we would use an HTML button. In
app/templates/components/update-query-button.hbs
:
{{yield}}
Finally, in our contact template let's change those inline components to block components.
{{#update-query-button sortValue="firstName" updateSort=(action 'updateSortBy')}}
Sort by First Name
{{/update-query-button}}
{{#update-query-button sortValue="lastName" updateSort=(action 'updateSortBy')}}
Sort by Last Name
{{/update-query-button}}
{{#update-query-button sortValue="email" updateSort=(action 'updateSortBy')}}
Sort by Email
{{/update-query-button}}
{{#update-query-button sortValue="null" updateSort=(action 'updateSortBy')}}
Clear Sort
{{/update-query-button}}
Summary
That is it. Today we created a component and an action while demonstrating Ember's Redux inspired data flow story: "Data Down, Actions Up." We pass properties down to components, and send actions back up to modify data on the parent of our component.
Comments for Components and Actions