Components are objects that hold the properties of controllers and views in Ember. The concept behind components is to have reusable DOM elements that have their own context or scope. Components accept attributes to customize the content rendered inside their templates. Also, components can allow actions to be assigned to properties from a parent controller or parent route (Figure 25.1).
As you assess the architecture of your application, you will start to see elements grouped together with minor differences throughout many routes and templates. If a grouping of elements can be removed from the template and described with variables depicting the elements’ state, it is a prime candidate to become a component.
This chapter will show you relatively simple examples of wrapping DOM elements in JavaScript objects to be used across multiple routes. Much like the helpers you saw in Chapter 23, components take arguments in the form of attributes to produce HTML. They also have the added feature of having their own actions and scoped properties to update themselves upon user interaction.
This chapter will only show you the tip of the iceberg of scalable application development. Ember applications in production rely heavily on components to create consistent user interfaces and maintainable code bases that sometimes span hundreds of routes. However, the examples you see here will provide a foundation of understanding for future projects where you want to turn your route templates into reusable pages or single elements.
An example of grouped elements that can often become a component is elements rendered in
{{#each}}
iterators.
There may be a <div>
container
element with a particular
className
based on the iterator object, a
title, an image path, a button with an action, and/or styles based
on the object’s property states. To make these easily reusable, you can wrap all the DOM code in
a component template and create a component JavaScript file to
handle the decorators and actions that a controller would normally
handle.
Your sightings list is built with an {{#each}}
iterator,
and you are going to create your first component to represent a sighting list item.
Start by generating a component via Ember CLI in the terminal:
ember g component listing-item
This creates three files: app/components/listing-item.js, app/templates/components/listing-item.hbs, and the test file tests/integration/components/listing-item-test.js.
Now that the component is created, you need to find the code you want the component to replace. Open app/templates/sightings/index.hbs and locate this block of code. You will be moving part of this code to the component in a moment. For now, just take a look at it.
<div class="row"> {{#each model as |sighting|}} <div class="col-xs-12 col-sm-3 text-center"> <div class="media well"> <img class="media-object thumbnail" src="{{if sighting.cryptid.profileImg sighting.cryptid.profileImg 'assets/images/cryptids/blank_th.png'}}" alt="{{sighting.cryptid.name}}" width="100%" height="100%"> <div class="caption"> <h3>{{sighting.cryptid.name}}</h3> {{#if sighting.location}} <h3>{{sighting.location}}</h3> <p>{{moment-from sighting.sightedAt}}</p> {{else}} <h3 class="text-danger">Bogus Sighting</h3> {{/if}} </div> {{#link-to 'sighting.edit' sighting.id tagName="button" class="btn btn-success btn-block"}} Edit {{/link-to}} </div> </div> {{/each}} </div>
The container <div>
with the
col-xs-12
class (that is, the shaded <div>) needs
to be on this specific page. It is a visual container for the
layout, specifying the size of its content. If you
were to add this container to your sightings list component, the container size would
be fixed for every instance of the component across the site.
However, the <div class="media well">
container and
its contents can be moved to a component. This component can then be added
to any size of container while
holding the characteristics of a single item in a list. The component
will contain the major elements of the item being listed – mainly
the media
container and the image, title, and Edit button.
Open app/templates/components/listing-item.hbs and begin by adding the image and cryptid name:
<img class="media-object thumbnail" src="{{imagePath}}" alt="{{name}}" width="100%" height="100%"> <div class="caption"> <h3>{{name}}</h3> {{yield}} </div>
A component represents a single DOM element. Everything in the component
template, then, is a child of that DOM
element. By default, Ember’s HTMLBars
uses JavaScript to create
<div>
elements and renders the component
template inside them.
The code you just wrote adds an image and a caption with a name to
the <div>
element created by a
{{#listing-item}}
component. Just like other
templates, the component has dynamic portions that are variables
you will pass in. (We will discuss the {{yield}}
in a moment.)
When you add this component to the route template, it will render this:
<div> <img class="media-object thumbnail" src="[cryptid's imagePath string]" alt="[cryptid's name string]" width="100%" height="100%"> <div class="caption"> <h3>[cryptid's name string]</h3> {{yield}} </div> </div>
The dynamic portions of the template, {{name}}
and
{{imagePath}}
, will be attributes provided to the component.
The {{yield}}
is where you can pass child elements to the
component to render in a specific location of the DOM node structure. The component template file acts a layout or master set of elements,
and the {{yield}}
is for elements you
need to add with each instance of the component.
You will use it later in the chapter.
While this is not exactly the markup you are replacing in app/templates/sightings/index.hbs, it is a good starting point.
Replace your existing code in app/templates/sightings/index.hbs with the component.
<div class="row"> {{#each model as |sighting|}} <div class="col-xs-12 col-sm-3 text-center"><div class="media well"><img class="media-object thumbnail" src="{{if sighting.cryptid.profileImgsighting.cryptid.profileImg 'assets/images/cryptids/blank_th.png'}}"alt="{{sighting.cryptid.name}}" width="100%" height="100%"><div class="caption"><h3>{{sighting.cryptid.name}}</h3>{{#if sighting.location}}<h3>{{sighting.location}}</h3><p>{{moment-from sighting.sightedAt}}</p>{{else}}<h3 class="text-danger">Bogus Sighting</h3>{{/if}}</div>{{#link-to 'sighting.edit' sighting.id tagName="button"class="btn btn-success btn-block"}}Edit{{/link-to}}</div>{{#listing-item imagePath=sighting.cryptid.profileImg name=sighting.cryptid.name}} {{/listing-item}} </div> {{/each}} </div>
You have replaced a lot of code with a single line. Some functionality was
removed in the process, but you will fix that soon. The next
step is adding the container styles back on the component element.
It is missing the "media"
and "well"
Bootstrap styles.
Open app/components/listing-item.js and
add a classNames property to the listing-item
component:
import Ember from 'ember'; export default Ember.Component.extend({ classNames: ["media", "well"] });
The property classNames passes
its value to the classNames attribute of the <div>
element created by the component.
Now the rendered component looks like this:
<div class="media well"> <img class="media-object thumbnail" src="[imagePath string]" alt="[name string]" width="100%" height="100%"> <div class="caption"> <h3>[name string]</h3> {{yield}} </div> </div>
Next, you will add elements to the {{yield}}
section of the component template
to make the implementation of this component instance unique to
the sightings route. First, in app/templates/sightings/index.hbs, add the
component’s contextual content that will render in the
{{yield}}
:
... {{#listing-item imagePath=sighting.cryptid.profileImg name=sighting.cryptid.name}} {{#if sighting.location}} <h3>{{sighting.location}}</h3> <p>{{moment-from sighting.sightedAt}}</p> {{else}} <h3 class="text-danger">Bogus Sighting</h3> {{/if}} {{#link-to 'sighting.edit' sighting.id tagName="button" class="btn btn-success btn-block"}} Edit {{/link-to}} {{/listing-item}}
This code, which should look familiar, reinstates the sighting location and time as well as the Edit
button.
3.142.173.238