Subscribe now

Components and Actions [09.12.2016]

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 sortValues 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.