Understanding Components in Ember 2

Lamin Sanneh
Share

This article was peer reviewed by Edwin Reynoso and Nilson Jacques. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Components are a vital part of an Ember application. They allow you to define your own, application-specific HTML tags and implement their behavior using JavaScript. As of Ember 2.x components will replace views and controllers (which have been deprecated) and are the recommended way to build an Ember application.

Ember’s implementation of components adheres as closely to the W3C’s Web Components specification as possible. Once Custom Elements become widely available in browsers, it should be easily to migrate Ember components to the W3C standard and have them usable by other frameworks.

If you’d like to find out more about why routable components are replacing controllers and views, then check out this short video by Ember core team members Yehuda Katz and Tom Dale.

The Tab Switcher Application

To get an in-depth understanding of Ember components, we will build a tab-switcher widget. This will comprise a set of tabs with associated content. Clicking on a tab will display that tab’s content and hide that of the other tabs. Simple enough? Lets begin.

As ever, you can find the code for this tutorial on our GitHub repo, or on this Ember Twiddle, if you’d like to experiment with the code in your browser.

The Anatomy of an Ember Component

An Ember component consists of a Handlebars template file and an accompanying Ember class. The implementation of this class is required only if we need extra interactivity with the component. A component is usable in a similar manner to an ordinary HTML tag. When we build our tab switcher component, we will be able to use it like so:

{{tab-switcher}}{{/tab-switcher}}

The template files for Ember components live in the directory app/templates/components. The class files live in app/components. We name Ember components using all lowercase letters with words separated by hyphens. This naming is by convention so we avoid name clashes with future HTML web components.

Our main Ember component will be tab-switcher. Notice I said main component because we will have several components. You can use components in conjunction with others. You can even have components nested inside another parent component. In the case of our tab-switcher, we will have one or more tab-item components like so:

{{#each tabItems as |tabItem| }}
  {{tab-item item=tabItem 
             setSelectedTabItemAction="setSelectedTabItem" }}
{{/each}}

As you can see, components can also have attributes just like native HTML elements.

Create an Ember 2.x Project

To follow along with this tutoial, you’ll need to create an EMber 2.x project. Here’s how:

Ember is installed using npm. For a tutorial on npm, you can see here.

npm install -g ember-cli

At the time of writing this will pull in version 1.13

ember -v
=> version: 1.13.8

Next, create a new Ember app:

ember new tabswitcher

Navigate to that directory and edit the bower.json file to include the latest version of the Ember, ember-data and ember-load-initializers:

{
  "name": "hello-world",
  "dependencies": {
    "ember": "^2.1.0",
    "ember-data": "^2.1.0",
    "ember-load-initializers": "^ember-cli/ember-load-initializers#0.1.7",
    ...
  }
}

Back in the terminal run:

bower install

Bower might prompt you for a version resolution for Ember. Select the 2.1 version from the list provided and prefix it with an exclamation mark to persist the resolution to bower.json.

Next start Ember CLI’s development server:

ember server

Finally navigate to http://localhost:4200/ and check the version your browser’s console.

Creating the Tab Switcher Component

Let’s create a tab switcher component using Ember’s built in generator:

ember generate component tab-switcher

This will create three new files. One is a Handlebars file for our HTML (app/templates/components/tab-switcher.hbs), the second is a JavaScript file for our component class (app/components/tab-switcher.js), the final one is a test file (tests/integration/components/tab-switcher-test.js). Testing the component is beyond the scope of this tutorial, but you can read more about that on the Ember site.

Now run ember server to load up the server and navigate to http://localhost:4200/. You should see a welcome message titled “Welcome to Ember”. So why isn’t our component showing up? Well, we haven’t used it yet, so lets do so now.

Using the Component

Open the application template app/templates/application.hbs. Add in the following after the h2 tag to use the component.

{{tab-switcher}}

In Ember, components are usable in two ways. The first way, called inline form, is to use them without any content inside. This is what we’ve done here. The second way is called block form and allows the component to be passed a Handlebars template that is rendered inside the component’s template wherever the {{yield}} expression appears. We will be sticking with the inline form throughout this tutorial.

This still isn’t displaying any content on the screen, though. This is because, the component itself doesn’t have any content to show. We can change this by adding the following line to the component’s template file (app/templates/components/tab-switcher.hbs):

<p>This is some content coming from our tab switcher component</p>

Now when the page reloads (which should happen automatically), you will see the above text displayed. Exciting times!

Create a Tab Item Component

Now that we have setup our main tab-switcher component, let’s create some tab-item components to nest inside it. We can create a new tab-item component like so:

ember generate component tab-item

Now change the handlebars file for the new component (app/templates/components/tab-item.hbs) to:

<span>Tab Item Title</span>
{{yield}}

Next, let’s nest three tab-items inside our main tab-switcher component. Change the tab-switcher template file (app/templates/components/tab-switcher.hbs) to:

<p>This is some content coming from our tab switcher component</p>

{{tab-item}}
{{tab-item}}
{{tab-item}}

{{yield}}

As mentioned above, the yield helper will render any Handlebars template that is passed in to our component. However, this is only useful if we use the tab-switcher in its block form. Since we are not, we can delete the yield helper altogether.

Now when we view the browser we will see three tab-item components, all saying “Tab Items Title”. Our component is rather static right now, so lets add in some dynamic data.

Adding Dynamic Data

When an Ember application starts, the router is responsible for displaying templates, loading data, and otherwise setting up application state. It does so by matching the current URL to the routes that you’ve defined. Let’s create a route for our application:

ember generate route application

Answer “no” to the command line question to avoid overwriting the existing application.hbs file. This will also generate a file app/routes/application.js. Open this up and add a model property:

export default Ember.Route.extend({
  model: function(){
  });
});

A model is an object that represents the underlying data that your application presents to the user. Anything that the user expects to see should be represented by a model. In this case we will add the contents of our tabs to our model. To do this alter the file like so:

import Ember from 'ember';

export default Ember.Route.extend({
  model: function(){
    var tabItems = [
      {
        title: 'Tab 1',
        content: 'Some exciting content for the tab 1'
      },
      {
        title: 'Tab 2',
        content: 'Some awesome content for the tab 2'
      },
      {
        title: 'Tab 3',
        content: 'Some stupendous content for the tab 3'
      }
    ];
    return tabItems;
  }
});

Then change the tab-switcher template file (app/templates/components/tab-switcher.hbs) to:

{{#each tabItems as |tabItem| }}
  {{tab-item item=tabItem }}
{{/each}}

Next, change the content of the tab-item template file (app/templates/components/tab-item.hbs) to:

<span>{{item.title}}</span>
{{yield}}

Finally change the tab-switcher usage in the application.hbs file to:

{{tab-switcher tabItems=model}}

This demonstrates how to pass properties to a component. We have made the item property accessible to the tab-item component template. After a page refresh, you should now see the tab item titles reflecting data from the models.

Adding Interactions Using Actions

Now let’s make sure that when a user clicks on a tab-item title, we display the content for that tab-item. Change the tab-switcher template file (app/templates/components/tab-switcher.hbs) to:

{{#each tabItems as |tabItem| }}
  {{tab-item item=tabItem setSelectedTabItemAction="setSelectedTabItem" }}
{{/each}}

<div class="item-content">
  {{selectedTabItem.content}}
</div>

This change assumes that we have a tabItem property on the tab-switcher component. This property represents the currently selected tab-item. We don’t currently have any such property so lets deal with that.

Inside a regular template, an action bubbles up to a controller. Inside a component template, the action bubbles up to the class of the component. It does not bubble any further up the hierarchy.

We need a way to send click actions to the tab-switcher component. This should happen after clicking any of its child tab-item components. Remember I said that actions get sent to the class of the component and not further up the hierarchy.

So it seems impossible that any actions coming from child components will reach the parent. Do not worry because this is just the default behavior of components and there is a workaround to circumvent it.

The simple workaround is to add an action to the tab-switcher template (app/templates/components/tab-switcher.hbs) like so:

{{#each tabItems as |tabItem| }}
  <div {{action "setSelectedTabItem" tabItem}} >
    {{tab-item item=tabItem setSelectedTabItemAction="setSelectedTabItem" }}
  </div>
{{/each}}

<div class="item-content">
  {{selectedTabItem.content}}
</div>

And to change the tab-switcher class file (app/components/tab-switcher.js) to look like

export default Ember.Component.extend({
  actions: {
    setSelectedTabItem: function(tabItem){
      this.set('selectedTabItem', tabItem);
    }
  }
});

At this point if you view our app in the browser, it will work as expected.

However, this workaround does not address the fact an action only bubbles up to the class of the component, so let’s do it in a way that does. Keep the changes in app/components/tab-switcher.js, but revert app/templates/components/tab-switcher.hbs back to its previous state:

<div class="item-content">
  {{selectedTabItem.content}}
</div>

{{#each tabItems as |tabItem| }}
  {{tab-item item=tabItem setSelectedTabItemAction="setSelectedTabItem" }}
{{/each}}

Now let’s change the tab-item template to:

<span {{action "clicked" item }}>{{item.title}}</span>
{{yield}}

And the tab-item class file to:

export default Ember.Component.extend({
  actions:{
    clicked: function(tabItem){
      this.sendAction("setSelectedTabItemAction", tabItem);
    }
  }
});

Here, you can see that we have added an action handler to deal with clicks on the tab-item title. This sends an action from the tab-item component to its parent, the tab-switcher component. The action bubbles up the hierarchy along with a parameter, namely the tabItem which we clicked on. This is so that it can be set as the current tab-item on the parent component.

Notice that we are using the property setSelectedTabItemAction as the action to send. This isn’t the actual action name that gets sent but the value contained in the property — in this case setSelectedTabItem, which is the handler on the parent component.

Conclusion

And that brings us to the end of this introduction to Ember components. I hope you enjoyed it. The productivity benefits of using reusable components throughout your Ember projects cannot be understated (and indeed throughout your projects in general). Why not give it a try? The source code for this tutorial is available on GitHub.

Are you using components in Ember already? What have been your experiences so far? I’d love to hear from you in the comments.