Designed as an analogue for the forthcoming Web Components spec, components combine the responsibilities of a view and controller into one object. A component object is both the scope of its template and responsible for event and lifecycle management. Components are a powerful tool behind a strict interface. They have limited access to other scopes, making them highly re-usable.
The template for a component is written in Handlebars and paired with an instance of the Ember.Component class. To distinguish components from helpers (the syntax for using them in Handlebars is the same), component names must include a hyphen.
The very simplest component consists of a Handlebars template. Let’s say we want an ember-badge component that displays Ember’s mascot and a link to emberjs.com. All we need do is create a template named components/ember-badge:
<script type="text/x-handlebars" id="components/ember-badge">
<a href="http://emberjs.com">
<img src="path/to/tomster.png">
</a>
</script>
To use this component anywhere in our app we add {{ember-badge}}:
<script type="text/x-handlebars" id="index">
<h2>About the App</h2>
<p>Built with:</p>
{{ember-badge}}
</script>
Which will result in:
<h2>About the App</h2>
<p>Built with:</p>
<div id="..." class="ember-view">
<a href="http://emberjs.com">
<img src="path/to/tomster.png">
</a>
</div>
Let’s say we also want the ability to add a custom caption to ember-badge. This can be achieved by using the yield helper and the block form of the component. First, we add the {{yield}} expression where we want the yielded content to appear:
<script type="text/x-handlebars" id="components/ember-badge">
<a href="http://emberjs.com">
<img src="path/to/tomster.png">
<span class="caption">{{yield}}</span>
</a>
</script>
Then we can use the block form of the component to customise our caption:
<script type="text/x-handlebars" id="index">
<p>Built with:</p>
{{#ember-badge}}Since 2013{{/ember-badge}}
</script>
Resulting in:
<h2>About the App</h2>
<p>Built with:</p>
<div id="..." class="ember-view">
<a href="http://emberjs.com">
<img src="path/to/tomster.png">
<span class="caption">Since 2013</span>
</a>
</div>
Note that the default element for components is a generic div. We’ll shortly see how to customize this and every other aspect of a component’s element. In order to do that, we must first meet Component Classes.
Imagine we’re adding a new embedded maps feature to an app we’ve built. We want to easily place maps powered by Leaflet.js in our templates without any overhead. It would great if we had a <leaflet-map> element that would do the work for us. Components allow us to do just that.
NOTE: The following examples assume we’ve already added Leaflet’s dependencies leaflet.js and leaflet.css.
Every component needs a template, so let’s start with a blank one:
<script type="text/x-handlebars" id="components/leaflet-map">
</script>
Now we’ll add the class to go with it. Classes are looked up automatically based on the naming convention of CamelCasedName + Component. So in this case LeafletMapComponent:
App.LeafletMapComponent = Ember.Component.extend();
Now that we have a home for our component’s code, we can take advantage of Ember’s lifecycle hooks to instantiate our map:
App.LeafletMapComponent = Ember.Component.extend({
didInsertElement: function() {
var map = L.map(this.get('element'));
this.set('map', map);
map.setView([0, 0], 1);
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
},
willRemoveElement: function() {
var map = this.get('map');
if (map) map.remove();
}
});
The didInsertElement hook will be called immediately after the component’s element is inserted into the DOM. The element is a property of the component which we access via this.get('element') and pass to Leaflet’s L.map constructor. We’re also taking advantage of the willRemoveElement hook to ensure the Leaflet map instance is properly torn down.
Our map is being instantiated, but right now the page doesn’t display much of anything, and that’s because Leaflet maps need a width and a height. Let’s tell our component to bind the style attribute to the style property:
App.LeafletMapComponent = Ember.Component.extend({
attributeBindings: ['style'],
style: 'width: 600px; height: 400px',
// ...
});
Now we’ve bound our style attribute, we can easily set it on a case-by-case basis:
<script type="text/x-handlebars">
<h1>Location</h1>
{{leaflet-map style="width: 600px; height: 400px"}}
<h2>Other Location</h2>
{{leaflet-map style="width: 150px; height: 150px"}}
</script>
This isn’t particularly semantic though. We’d much rather be able to apply width and height as separate properties:
<script type="text/x-handlebars">
<h1>Location</h1>
{{leaflet-map width="600px" height="400px"}}
</script>
We can easily implement this by turning style into a computed property:
App.LeafletMapComponent = Ember.Component.extend({
attributeBindings: ['style'],
width: '600px',
height: '400px',
style: function() {
return [
'width:' + this.get('width'),
'height:' + this.get('height')
].join(';');
}.property('width', 'height'),
// ...
});
Let’s apply some of this to our earlier ember-badge example. We want to get rid of that unnecessary div, so let’s start by assuming the containing element will be an <a> tag and reworking our template accordingly:
<script type="text/x-handlebars" id="components/ember-badge">
<img src="path/to/tomster.png">
<span class="caption">{{yield}}</span>
</script>
Now we’ll set tagName and href properties of our component class, remembering to tell Ember to bind the href property to the href attribute. We’ll also add a class for good measure (more on this later):
App.EmberBadgeComponent = Ember.Component.extend({
tagName: 'a',
attributeBindings: ['href'],
href: 'http://emberjs.com',
classNames: ['ember-badge']
});
Now our component renders like this:
<a href="http://emberjs.com" class="ember-view ember-badge">
<img src="path/to/tomster.png">
<span class="caption">Since 2013</span> </a>
So far, the mechanics of Components will seem very similar to those of Ember Views. Indeed, Components are implemented in terms of Views under the hood. Conceptually though, they differ in the way they isolate behaviour and data. Unlike Views, which have access to their surrounding context, Components are their own mini-universe, aware only of the data we explicitly pass in to them. This characteristic enforces re-usable design and separation of concerns. A component should be able to do its job no matter where it’s placed or what is passing the data in. With this in mind, let’s see a few examples of binding data to components.
In our earlier leaflet-map example, we set the dimensions of the map via width and height properties, which were bound to the map’s style attribute. What if we wanted to allow users to adjust the coordinates and zoom level of a map? Let’s start by sketching out our template:
<script type="text/x-handlebars">
<p class="controls">
Lat: {{input value=latitude size=8}}
Lon: {{input value=longitude size=8}}
Zoom: {{input value=zoom size=1}}
</p>
{{leaflet-map width="600px"
height="400px"
latitude=latitude
longitude=longitude
zoom=zoom}}
</script>
We’ll want to give latitude, longitude and zoom initial values, so let’s set those up in the controller. In this case, we’re in the application template, so we’ll define them in ApplicationController:
App.ApplicationController = Ember.Controller.extend({
latitude: 0,
longitude: 0,
zoom: 1
});
Now let’s adapt our LeafletMapComponent so it knows what to do with these new properties. First up, we’ll add an observer method that can apply the values of these properties to our Leaflet map object:
App.LeafletMapComponent = Ember.Component.extend({
// ...
setView: function() {
var map = this.get('map'),
center = [this.get('latitude'), this.get('longitude')],
zoom = this.get('zoom');
map.setView(center, zoom);
}.observes('latitude', 'longitude', 'zoom'),
// ...
});
Now every time one of these three properties changes, we’ll be telling our Leaflet map instance to setView to the new location.
Then we’ll make sure this method is called on instantiation:
App.LeafletMapComponent = Ember.Component.extend({
// ...
didInsertElement: function() {
var map = L.map(this.get('element'));
this.set('map', map);
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
this.setView(); // <-- set the initial view
},
// ...
});
By this point, our application should be looking something like this:
And when we adjust the values in our three inputs the map updates accordingly:
This is a good start, but right now dragging and zooming the map with the mouse doesn't have any effect on the properties outside of our component — the inputs stay just as we left them. This component is currently something of a one-way street for data. It would be way more satisfying if our leaflet-map actually controls the data it’s bound to.
Fortunately, most of the groundwork is already laid. All we need is to listen out for changes in the map and update our properties accordingly. Leaflet maps fire various events and the most interesting to us is move. Let’s add a listener method for move that propagates the changes to our bound properties:
App.LeafletComponent = Ember.Component.extend({
// ...
didInsertElement: function() {
// ...
map.on('move', this.mapDidMove, this);
},
// ...
mapDidMove: function() {
Ember.run(this, function(){
var map = this.get('map'),
center = map.getCenter(),
zoom = map.getZoom();
this.setProperties({
latitude: center.lat,
longitude: center.lng,
zoom: zoom
});
});
}
});
Now our leaflet-map component works in both directions:
To demonstrate the power this gives us, let’s try adding a second map component bound to the same data:
<script type="text/x-handlebars">
<p class="controls">
Lat: {{input value=latitude size=8}}
Lon: {{input value=longitude size=8}}
Zoom: {{input value=zoom size=1}}
</p>
{{leaflet-map width="600px"
height="400px"
latitude=latitude
longitude=longitude
zoom=zoom}}
<br>
{{leaflet-map width="600px"
height="400px"
latitude=latitude
longitude=longitude
zoom=zoom}}
</script>
Try dragging either map around and we find that they move in synchrony! This is not simply a happy accident, it is Ember’s data bindings at work. We can think of our UI components as essentially representation and controls for our underlying data. The data always holds the truth, but with components we can provide the perfect interface for that data.
We said previously that components are intrinsically self-contained — are their own mini-universes — and this applies equally to action handling. Elsewhere in this book we’ve seen actions sent to controllers and bubble up to routes. Within components however, actions are sent only to the component itself.
Let’s say we have a rating-widget component that allows readers to rate articles from 1–5 stars. We’ll work from the outside-in to best define our interface, starting with the article template in which we’ll be using our component:
<script type="text/x-handlebars" id="article">
<h1>{{title}}</h1>
<div role="main">
{{body}}
</div>
<p>Your rating: {{rating}}</p>
{{rating-widget value=rating}}
</script>
Now the component’s template:
<script type="text/x-handlebars" id="components/rating-widget">
<button {{action "rate" 1}}
{{bindAttr disabled=isRatedOne}}>1</button>
<button {{action "rate" 2}}
{{bindAttr disabled=isRatedTwo}}>2</button>
<button {{action "rate" 3}}
{{bindAttr disabled=isRatedThree}}>3</button>
<button {{action "rate" 4}}
{{bindAttr disabled=isRatedFour}}>4</button>
<button {{action "rate" 5}}
{{bindAttr disabled=isRatedFive}}>5</button>
</script>
This perhaps isn’t the most elegant template, but it illustrates very clearly the action and data we’ll be sending, and the properties we’ll be depending upon. Now for the components class:
App.RatingWidgetComponent = Ember.Component.extend({
isRatedOne: Ember.computed.equal('value', 1),
isRatedTwo: Ember.computed.equal('value', 2),
isRatedThree: Ember.computed.equal('value', 3),
isRatedFour: Ember.computed.equal('value', 4),
isRatedFive: Ember.computed.equal('value', 5),
actions: {
rate: function(value) {
this.set('value', value);
}
}
});
With these elements in place, we have a functioning rating widget that will update the article’s rating property:
Let’s re-jig this example a bit. Now, instead of live-updating the rating value, we want rating to behave as a one-shot submission process:
<script type="text/x-handlebars" id="article">
<h1>{{title}}</h1>
<div role="main">
{{body}} </div>
<div class="rating">
{{#if isRated}}
Your rating: {{rating}}
{{else}}
{{rating-widget action="rate"}}
{{/if}}
</div>
</script>
Note the change in our component. Now, rather than binding the article’s rating property, we’re instead telling the component what application-level action we want to be triggered when a rating is selected. It is up to us when and where we send this action, so let’s place it inside the internal action handler:
App.RatingWidgetComponent = Ember.Component.extend({
// ...
actions: {
rate: function(value) {
this.set('value', value);
this.sendAction('action', value);
}
}
});
Now let’s actually write some code higher up in the application to handle this rating. ArticleController is the natural choice in this instance:
App.ArticleController = Ember.ObjectController.extend({
isRated: Ember.computed.notEmpty('rating'),
actions: {
rate: function(rating) {
this.set('rating', rating);
}
}
});
This all works great, but let’s take a closer look at that sendAction method. Recall that we define the "primary" action like this:
{{rating-widget action="rate"}}
And then send that action like so:
this.sendAction('action', value);
This says: send the action name stored in my action property with value as an argument. By default, sendAction will send whatever action is named in the action property. So, if we weren’t sending any arguments we could invoke it simply as:
this.sendAction();
Which is equivalent to:
this.sendAction('action');
Our components can send as many actions as we want. For example, if our rating-widget had an "abstain" button, we could wire that up to a different action like so:
<script type="text/x-handlebars">
{{rating-widget action="rate" abstain="hideRating"}}
</script>
<script type="text/x-handlebars" id="components/rating-widget">
{{! ... }}
<button {{action "abstain"}}>Abstain</button>
</script>
App.RatingWidgetComponents = Ember.Component.extend({
// ...
actions: {
rate: function(value) {
this.sendAction('action', value);
},
abstain: function() {
this.sendAction('abstain');
}
}
});
As previously mentioned, it’s up to us when and where we send actions out of a component. We’ve seen how this can be achieved with the Handlebars action helper within the component template, but we can also use event handlers within the component class:
App.TurboButtonComponent = Ember.Component.extend({
tagName: 'button',
classNames: ['turbo-button'],
click: function() {
this.sendAction();
},
doubleClick: function() {
this.sendAction('turboAction');
}
});
<script type="text/x-handlebars">
{{turbo-button action="accelerate" turboAction="enableTurbo"}}
</script>
A full list of these event handlers can be found in the Ember API documentation.
By thinking of our UI in terms of self-contained components with single responsibilities, we greatly improve our chances of building flexible, maintainable applications. The aim is to have a UI composed of isolated components that can be swapped out and reconfigured easily and seamlessly.
When designing components it pays to think of them as black boxes, presenting the simplest possible interface to the rest of the application. Components can effectively hide DOM elements and events behind a clean semantic interface, much like how HTML5 <video> and <audio> components encapsulate complex behavior and UI without introducing complexity to the elements around them.
In the next chapter, we will review the building blocks of a data layer in JavaScript and see how Ember-Data implements those basics.
There has been error in communication with Booktype server. Not sure right now where is the problem.
You should refresh this page.