What is Ember.js?
Ember is a JavaScript framework for creating ambitious web applications
that eliminates boilerplate and provides a standard application
architecture.
Eliminate Boilerplate
There are some tasks that are common to every web application. For example,
taking data from the server, rendering it to the screen, then updating that
information when it changes.
Since the tools provided to do this by the browser are quite primitive, you
end up writing the same code over and over. Ember.js provides tools that let
you focus on your app instead of writing the same code you've written a hundred
times.
Because we've built dozens of applications ourselves, we've gone beyond the
obvious low-level event-driven abstractions, eliminating much of the
boilerplate associated with propagating changes throughout your application,
and especially into the DOM itself.
To help manage changes in the view, Ember.js comes with a templating engine
that will automatically update the DOM when the underlying objects change.
For a simple example, consider this template of a Person:
|
{{person.name}} is {{person.age}}.
|
As with any templating system, when the template is initially rendered, it
will reflect the current state of the person. To avoid boilerplate, though,
Ember.js will also update the DOM automatically for you if the person's name
or age changes.
You specify your template once, and Ember.js makes sure it's always up to date.
Provides Architecture
Since web applications evolved from web pages, which were nothing more than
static documents, browsers give you just enough rope to hang yourself with.
Ember makes it easy to divide your application into models, views, and controllers,
which improves testability, makes code more modular, and helps new developers
on the project quickly understand how everything fits together. The days of
callback spaghetti are over.
Ember also supplies built-in support for state management, so you'll have
a way to describe how your application moves through various nested states
(like signed-out, signed-in, viewing-post, and viewing-comment) out of the box.
How is Ember.js Different?
Traditional web applications make the user download a new page every time
they interact with the server. This means that every interaction is never faster
than the latency between you and the user, and usually slower. Using AJAX to
replace only parts of the page helps somewhat, but still requires a roundtrip to
your server every time your UI needs to update. And if multiple parts of the
page need to update all at once, most developers just resort to loading the page
over again, since keeping everything in sync is tricky.
Ember.js, like some other modern JavaScript frameworks, works a little differently.
Instead of the majority of your application's logic living on the server, an
Ember.js application downloads everything it needs to run in the initial page
load. That means that while your user is using your app, she never has to load
a new page and your UI responds quickly to their interaction.
One advantage of this architecture is that your web application uses the same
REST API as your native apps or third-party clients. Back-end developers can
focus on building a fast, reliable, and secure API server, and don't have to be
front-end experts, too.
Ember.js at a Glance
These are the three features that make Ember a joy to use:
- Bindings
- Computed properties
- Auto-updating templates
Bindings
Use bindings to keep properties between two different objects in sync. You just
declare a binding once, and Ember will make sure changes get propagated in either
direction.
Here's how you create a binding between two objects:
1
2
3
4
5
6
7
8
9
10
11
|
MyApp.president = Ember.Object.create({
name: "Barack Obama"
});
MyApp.country = Ember.Object.create({
presidentNameBinding: 'MyApp.president.name'
});
MyApp.country.get('presidentName');
|
Bindings allow you to architect your application using the MVC (Model-View-Controller)
pattern, then rest easy knowing that data will always flow correctly from layer to layer.
Computed Properties
Computed properties allow you to treat a function like a property:
1
2
3
4
5
6
7
8
9
10
|
MyApp.president = Ember.Object.create({
firstName: "Barack",
lastName: "Obama",
fullName: function() {
return this.get('firstName') + ' ' + this.get('lastName');
}.property()
});
MyApp.president.get('fullName');
|
Computed properties are useful because they can work with bindings, just
like any other property.
Many computed properties have dependencies on other properties. For example, in the above
example, the
fullName
property depends on
firstName
and
lastName
to determine its value.
You can tell Ember about these dependencies like this:
1
2
3
4
5
6
7
8
9
|
MyApp.president = Ember.Object.create({
firstName: "Barack",
lastName: "Obama",
fullName: function() {
return this.get('firstName') + ' ' + this.get('lastName');
}.property('firstName', 'lastName')
});
|
Make sure you list these dependencies so Ember knows when to update bindings that connect
to a computed property.
Auto-updating Templates
Ember uses Handlebars, a semantic templating library. To take data from your JavaScript application
and put it into the DOM, create a
<script>
tag and put it into your HTML, wherever you'd like the
value to appear:
1
2
3
|
<script type="text/x-handlebars">
The President of the United States is {{MyApp.president.fullName}}.
</script>
|
Here's the best part: templates are bindings-aware. That means that if you ever change the value of
the property that you told us to display, we'll update it for you automatically. And because you've
specified dependencies, changes to
those properties are reflected as well.
Hopefully you can see how all three of these powerful tools work together: start with some primitive
properties, then start building up more sophisticated properties and their dependencies using computed
properties. Once you've described the data, you only have to say how it gets displayed once, and Ember
takes care of the rest. It doesn't matter how the underlying data changes, whether from an XHR request
or the user performing an action; your user interface always stays up-to-date. This eliminates entire
categories of edge cases that developers struggle with every day.
Getting Started
Depending on your needs, there are several ways to get started creating your
first Ember.js app.
If your needs are simple or you're interested in just playing around, you can
download the Ember.js Starter Kit. The Starter Kit is based on
HTML5
Boilerplate and does not require any build tools
or other dependencies. To begin, download the Starter Kit and unzip it. You can
edit the Handlebars templates directly inside the
index.html
file, and the
Ember.js app itself lives in
javascripts/app.js
.
For larger apps, you may want to consider using Ruby on Rails. Rails helps you
manage your source code and other assets, while also providing the REST API
that your application will talk to.
The Ember Object Model
Ember enhances the simple JavaScript object model to support bindings
and observers, as well as to support a more powerful mixin-based
approach to code sharing.
At its most basic, you create a new Ember class by using the
extend
method on
Ember.Object
.
1
2
3
4
5
|
Person = Ember.Object.extend({
say: function(thing) {
alert(thing);
}
});
|
Once you have built a new class, you can create new instances of the
class by using the
create
method. Any properties defined on the class
will be available to instances.
1
2
|
var person = Person.create();
person.say("Hello")
|
When creating an instance, you can also add additional properties to the
instance by passing in an object.
1
2
3
4
5
6
7
|
var tom = Person.create({
name: "Tom Dale",
helloWorld: function() {
this.say("Hi my name is " + this.get('name'));
}
});
tom.helloWorld()
|
Because of Ember's support for bindings and observers, you will always
access properties using the
get
method, and set properties using the
set
method.
When creating a new instance of an object, you can also override any
properties or methods defined on the class. For instance, in this case,
you could override the
say
method from the
Person
class.
1
2
3
4
5
6
7
|
var yehuda = Person.create({
name: "Yehuda Katz",
say: function(thing) {
var name = this.get('name');
this._super(name + " says: " + thing);
}
});
|
You can use the
_super
method on the object (
super
is a reserved
word in JavaScript) to call the original method you overrode.
Subclassing Classes
You can also create subclasses of classes you create by using the
extend
method. In fact, when we created a new class above by calling
extend
on
Ember.Object
, we were
subclassing Ember.Object
.
1
2
3
4
5
|
var LoudPerson = Person.extend({
say: function(thing) {
this._super(thing.toUpperCase());
}
});
|
When subclassing, you can use
this._super
to invoke methods you are
overriding.
Reopening Classes and Instances
You don't need to define a class all at once. You can reopen a class and
define new properties using the
reopen
method.
1
2
3
4
|
Person.reopen({
isPerson: true
});
Person.create().get('isPerson')
|
When using
reopen
, you can also override existing methods and
call
this._super
.
1
2
3
4
5
6
|
Person.reopen({
say: function(thing) {
this._super(thing + "!");
}
});
|
As you can see,
reopen
is used to add properties and methods to an instance.
But when you need to create class method or add the properties to the class itself you can use
reopenClass
.
1
2
3
4
5
6
|
Person.reopenClass({
createMan: function() {
return Person.create({isMan: true})
}
});
Person.createMan().get('isMan')
|
Computed Properties (Getters)
Often, you will want a property that is computed based on other
properties. Ember's object model allows you to define computed
properties easily in a normal class definition.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Person = Ember.Object.extend({
firstName: null,
lastName: null,
fullName: function() {
var firstName = this.get('firstName');
var lastName = this.get('lastName');
return firstName + ' ' + lastName;
}.property('firstName', 'lastName')
});
var tom = Person.create({
firstName: "Tom",
lastName: "Dale"
});
tom.get('fullName')
|
The
property
method defines the function as a computed property, and
defines its dependencies. Those dependencies will come into play later
when we discuss bindings and observers.
When subclassing a class or creating a new instance, you can override
any computed properties.
Computed Properties (Setters)
You can also define what Ember should do when setting a computed
property. If you try to set a computed property, it will be invoked with
the key and value you want to set it to.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
Person = Ember.Object.extend({
firstName: null,
lastName: null,
fullName: function(key, value) {
if (arguments.length === 1) {
var firstName = this.get('firstName');
var lastName = this.get('lastName');
return firstName + ' ' + lastName;
} else {
var name = value.split(" ");
this.set('firstName', name[0]);
this.set('lastName', name[1]);
return value;
}
}.property('firstName', 'lastName')
});
var person = Person.create();
person.set('fullName', "Peter Wagenet");
person.get('firstName')
person.get('lastName')
|
Ember will call the computed property for both setters and getters, and
you can check the number of arguments to determine whether it is being called
as a getter or a setter.
Observers
Ember supports observing any property, including computed properties.
You can set up an observer on an object by using the
addObserver
method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
Person = Ember.Object.extend({
firstName: null,
lastName: null,
fullName: function() {
var firstName = this.get('firstName');
var lastName = this.get('lastName');
return firstName + ' ' + lastName;
}.property('firstName', 'lastName')
});
var person = Person.create({
firstName: "Yehuda",
lastName: "Katz"
});
person.addObserver('fullName', function() {
});
person.set('firstName', "Brohuda");
|
Because the
fullName
computed property depends on
firstName
,
updating
firstName
will fire observers on
fullName
as well.
Because observers are so common, Ember provides a way to define
observers inline in class definitions.
1
2
3
4
5
|
Person.reopen({
fullNameChanged: function() {
}.observes('fullName')
});
|
You can define inline observers by using the
Ember.observer
method if you
are using Ember without prototype extensions:
1
2
3
4
5
|
Person.reopen({
fullNameChanged: Ember.observer(function() {
}, 'fullName')
});
|
Changes in Arrays
Often, you may have a computed property that relies on all of the items in an
array to determine its value. For example, you may want to count all of the
todo items in a controller to determine how many of them are completed.
Here's what that computed property might look like:
1
2
3
4
5
6
7
8
9
|
App.todosController = Ember.Object.create({
todos: [
Ember.Object.create({ isDone: false })
],
remaining: function() {
var todos = this.get('todos');
return todos.filterProperty('isDone', false).get('length');
}.property('todos.@each.isDone')
});
|
Note here that the dependent key (
todos.@each.isDone
) contains the special
key
@each
. This instructs Ember.js to update bindings and fire observers for
this computed property when one of the following four events occurs:
- The
isDone
property of any of the objects in the todos
array changes.
- An item is added to the
todos
array.
- An item is removed from the
todos
array.
- The
todos
property of the controller is changed to a different array.
In the example above, the
remaining
count is
1
:
1
2
|
App.todosController.get('remaining');
|
If we change the todo's
isDone
property, the
remaining
property is updated
automatically:
1
2
3
4
5
6
7
8
9
|
var todos = App.todosController.get('todos');
var todo = todos.objectAt(0);
todo.set('isDone', true);
App.todosController.get('remaining');
todo = Ember.Object.create({ isDone: false });
todos.pushObject(todo);
App.todosController.get('remaining');
|
Bindings
A binding creates a link between two properties such that when one changes, the
other one is updated to the new value automatically. Bindings can connect
properties on the same object, or across two different objects. Unlike most other
frameworks that include some sort of binding implementation, bindings in
Ember.js can be used with any object, not just between views and models.
The easiest way to create a two-way binding is by creating a new property
with the string
Binding
at the end, then specifying a path from the global scope:
1
2
3
4
5
6
7
8
9
10
|
App.wife = Ember.Object.create({
householdIncome: 80000
});
App.husband = Ember.Object.create({
householdIncomeBinding: 'App.wife.householdIncome'
});
App.husband.get('householdIncome');
App.husband.set('householdIncome', 90000);
App.wife.get('householdIncome');
|
Note that bindings don't update immediately. Ember waits until all of your
application code has finished running before synchronizing changes, so you can
change a bound property as many times as you'd like without worrying about the
overhead of syncing bindings when values are transient.
One-Way Bindings
A one-way binding only propagates changes in one direction. Usually, one-way
bindings are just a performance optimization and you can safely use
the more concise two-way binding syntax (as, of course, two-way bindings are
de facto one-way bindings if you only ever change one side).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
App.user = Ember.Object.create({
fullName: "Kara Gates"
});
App.userView = Ember.View.create({
userNameBinding: Ember.Binding.oneWay('App.user.fullName')
});
App.user.set('fullName', "Krang Gates");
App.userView.set('userName', "Truckasaurus Gates");
App.user.get('fullName');
|
What Do I Use When?
Sometimes new users are confused about when to use computed properties,
bindings and observers. Here are some guidelines to help:
- Use computed properties to build a new property by synthesizing other
properties. Computed properties should not contain application behavior, and
should generally not cause any side-effects when called. Except in rare cases,
multiple calls to the same computed property should always return the same
value (unless the properties it depends on have changed, of course.)
- Observers should contain behavior that reacts to changes in another
property. Observers are especially useful when you need to perform some
behavior after a binding has finished synchronizing.
- Bindings are most often used to ensure objects in two different layers
are always in sync. For example, you bind your views to your controller using
Handlebars. You may often bind between two objects in the same layer. For
example, you might have an
App.selectedContactController
that binds to the
selectedContact
property of App.contactsController
.
Creating a Namespace
Every Ember app should have an instance of
Ember.Application
. This object
will serve as the globally-accessible namespace for all of the other classes
and instances in your app. Additionally, it sets up event listeners on the page
so that your views receive events when users interact with your user interface
(which you'll learn about later.)
Here's an example of an application:
|
window.App = Ember.Application.create();
|
You can call your namespace whatever you'd like, but it must begin
with a capital letter in order for bindings to find it.
If you are embedding an Ember application into an existing site, you can
have event listeners set up for a specific element by providing a
rootElement
property:
1
2
3
|
window.App = Ember.Application.create({
rootElement: '#sidebar'
});
|
Describing Your UI with Handlebars
Handlebars
Ember supports templates for rendering views. The default ( and best ingegrated ) for Ember is
Handlebars, a semantic templating language. These templates look like regular HTML, with embedded expressions.
Ember does not come with Handlebars, but you can easily download Handlebars and include it with your Ember application. Ember and Handlebars work great together, but you can substitute other rendering engines by pointing the Ember.View's template property at a function that will use the Ember.View's context to produce a string of html. For now, let's cover the great integration between Ember and Handlebars.
You should store your Handlebars templates inside your application's HTML file. At runtime, Ember will compile these templates so they are available for you to use in your views.
To immediately insert a template into your document, place it inside a
<script>
tag within your
<body>
tag:
1
2
3
4
5
6
7
|
<html>
<body>
<script type="text/x-handlebars" data-template-name="application">
Hello, <b>{{MyApp.name}}</b>
</script>
</body>
</html>
|
Adding a
data-template-name
attribute makes the template available to use later:
1
2
3
4
5
6
7
|
<html>
<head>
<script type="text/x-handlebars" data-template-name="say-hello">
Hello, <b>{{MyApp.name}}</b>
</script>
</head>
</html>
|
Ember.View
You can use
Ember.View
to render a Handlebars template and insert it into the DOM.
To tell the view which template to use, set its
templateName
property. For example, if I had a
<script>
tag like this:
1
2
3
4
5
6
7
|
<html>
<head>
<script type="text/x-handlebars" data-template-name="say-hello">
Hello, <b>{{name}}</b>
</script>
</head>
</html>
|
I would set the
templateName
property to
"say-hello"
.
1
2
3
4
|
var view = Ember.View.create({
templateName: 'say-hello',
name: "Bob"
});
|
Note: For the remainder of the guide, the
templateName
property will be omitted from most examples. You can assume that if we show a code sample that includes an Ember.View and a Handlebars template, the view has been configured to display that template via the
templateName
property.
You can append views to the document by calling
appendTo
:
|
view.appendTo('#container');
|
As a shorthand, you can append a view to the document body by calling
append
:
To remove a view from the document, call
remove
:
Handlebars Basics
As you've already seen, you can print the value of a property by enclosing it in a Handlebars expression, or a series of braces, like this:
This will look up and print the View's
color
property. For example, if your view looks like this:
1
2
3
|
App.CarView = Ember.View.extend({
color: 'blue'
});
|
Your view would appear in the browser like this:
You can also specify global paths:
|
My new car is {{App.carController.color}}.
|
(Ember determines whether a path is global or relative to the view by checking whether the first letter is capitalized,
which is why your
Ember.Application
instance should start with a capital letter.)
All of the features described in this guide are
bindings aware. That means that if the values used by your templates ever change, your HTML will be updated automatically. It's like magic.
In order to know which part of your HTML to update when an underlying property changes, Handlebars will insert marker elements with a unique ID. If you look at your application while it's running, you might notice these extra elements:
1
2
3
4
|
My new car is
<script id="metamorph-0-start" type="text/x-placeholder"></script>
blue
<script id="metamorph-0-end" type="text/x-placeholder"></script>.
|
Because all Handlebars expressions are wrapped in these markers, make sure each HTML tag stays inside the same block. For example, you shouldn't do this:
1
2
|
<div {{#if isUrgent}}class="urgent"{{/if}}>
|
If you want to avoid your property output getting wrapped in these markers, use the
unbound
helper:
|
My new car is {{unbound color}}.
|
Your output will be free of markers, but be careful, because the output won't be automatically updated!
{{#if}}, {{else}}, and {{#unless}}
Sometimes you may only want to display part of your template if a property
exists. For example, let's say we have a view with a
person
property that
contains an object with
firstName
and
lastName
fields:
1
2
3
4
5
6
|
App.SayHelloView = Ember.View.extend({
person: Ember.Object.create({
firstName: "Joy",
lastName: "Clojure"
})
});
|
In order to display part of the template only if the
person
object exists, we
can use the
{{#if}}
helper to conditionally render a block:
1
2
3
|
{{#if person}}
Welcome back, <b>{{person.firstName}} {{person.lastName}}</b>!
{{/if}}
|
Handlebars will not render the block if the argument passed evaluates to
false
,
undefined
,
null
or
[]
(i.e., any "falsy" value).
If the expression evaluates to falsy, we can also display an alternate template
using
{{else}}
:
1
2
3
4
5
|
{{#if person}}
Welcome back, <b>{{person.firstName}} {{person.lastName}}</b>!
{{else}}
Please log in.
{{/if}}
|
To only render a block if a value is falsy, use
{{#unless}}
:
1
2
3
|
{{#unless hasPaid}}
You owe: ${{total}}
{{/unless}}
|
{{#if}}
and
{{#unless}}
are examples of block expressions. These allow you
to invoke a helper with a portion of your template. Block expressions look like
normal expressions except that they contain a hash (#) before the helper name,
and require a closing expression.
{{#with}}
Sometimes you may want to invoke a section of your template with a context
different than the Ember.View. For example, we can clean up the above template by
using the
{{#with}}
helper:
1
2
3
|
{{#with person}}
Welcome back, <b>{{firstName}} {{lastName}}</b>!
{{/with}}
|
{{#with}}
changes the
context of the block you pass to it. The context
is the object on which properties are looked up. By default, the context is the
Ember.View to which the template belongs.
Binding Element Attributes with {{bindAttr}}
In addition to text, you may also want your templates to dictate the attributes
of your HTML elements. For example, imagine a view that contains a URL:
1
2
3
|
App.LogoView = Ember.View.extend({
logoUrl: 'http://www.mycorp.com/images/logo.png'
});
|
The best way to display the URL as an image in Handlebars is like this:
1
2
3
|
<div id="logo">
<img {{bindAttr src="logoUrl"}} alt="Logo">
</div>
|
This generates the following HTML:
1
2
3
|
<div id="logo">
<img src="http://www.mycorp.com/images/logo.png" alt="Logo">
</div>
|
If you use
{{bindAttr}}
with a Boolean value, it will add or remove the specified attribute. For example, given this Ember view:
1
2
3
|
App.InputView = Ember.View.extend({
isDisabled: true
});
|
And this template:
|
<input type="checkbox" {{bindAttr disabled="isDisabled"}}>
|
Handlebars will produce the following HTML element:
|
<input type="checkbox" disabled>
|
Binding Class Names with {{bindAttr}}
The
class
attribute can be bound like any other attribute, but it also has some additional special behavior. The default behavior works like you'd expect:
1
2
3
4
|
App.AlertView = Ember.View.extend({
priority: "p4",
isUrgent: true
});
|
1
2
3
|
<div {{bindAttr class="priority"}}>
Warning!
</div>
|
This template will emit the following HTML:
1
2
3
|
<div class="p4">
Warning!
</div>
|
If the value to which you bind is a Boolean, however, the dasherized version of that property will be applied as a class:
1
2
3
|
<div {{bindAttr class="isUrgent"}}>
Warning!
</div>
|
This emits the following HTML:
1
2
3
|
<div class="is-urgent">
Warning!
</div>
|
Unlike other attributes, you can also bind multiple classes:
1
2
3
|
<div {{bindAttr class="isUrgent priority"}}>
Warning!
</div>
|
You can also specify an alternate class name to use, instead of just
dasherizing.
1
2
3
|
<div {{bindAttr class="isUrgent:urgent"}}>
Warning!
</div>
|
In this case, if the
isUrgent
property is true, the
urgent
class
will be added. If it is false, the
urgent
class will be removed.
You can also specify a class name which shall be used when the property is
false
:
1
2
3
|
<div {{bindAttr class="isEnabled:enabled:disabled"}}>
Warning!
</div>
|
In this case, if the
isEnabled
property is true, the
enabled
class will be added. If the property is false, the class
disabled
will be added.
This syntax allows the shorthand for only adding a class when a property is
false
, so this:
1
2
3
|
<div {{bindAttr class="isEnabled::disabled"}}>
Warning!
</div>
|
Will add the class
disabled
when
isEnabled
is
false
and add no class if
isEnabled
is
true
.
Handling Events with {{action}}
Use the
{{action}}
helper to attach a handler in your view class to an event triggered on an element.
To attach an element's
click
event to the
edit()
handler in the current view:
|
<a href="#" {{action "edit" on="click"}}>Edit</a>
|
Because the default event is
click
, this could be written more concisely as:
|
<a href="#" {{action "edit"}}>Edit</a>
|
Although the view containing the
{{action}}
helper will be targeted by default, it is possible to target a different view:
|
<a href="#" {{action "edit" target="parentView"}}>Edit</a>
|
The action handler can optionally accept a jQuery event object, which will be extended to include
view
and
context
properties. These properties can be useful when targeting a different view with your action. For instance:
1
2
3
4
5
6
|
App.ListingView = Ember.View.extend({
templateName: 'listing',
edit: function(event) {
event.view.set('isEditing', true);
}
});
|
Any of the templates discussed above will produce an HTML element like this:
|
<a href="#" data-ember-action="3">Edit</a>
|
Ember will delegate the event you specified to your target view's handler based upon the internally assigned
data-ember-action
id.
Building a View Hierarchy
So far, we've discussed writing templates for a single view. However, as your application grows, you will often want to create a hierarchy of views to encapsulate different areas on the page. Each view is responsible for handling events and maintaining the properties needed to display it.
{{view}}
To add a child view to a parent, use the
{{view}}
helper, which takes a path to a view class.
1
2
3
4
5
6
7
8
9
10
11
12
|
App.UserView = Ember.View.extend({
templateName: 'user',
firstName: "Albert",
lastName: "Hofmann"
});
App.InfoView = Ember.View.extend({
templateName: 'info',
posts: 25,
hobbies: "Riding bicycles"
});
|
1
2
|
User: {{firstName}} {{lastName}}
{{view App.InfoView}}
|
1
2
3
|
<b>Posts:</b> {{view.posts}}
<br>
<b>Hobbies:</b> {{view.hobbies}}
|
If we were to create an instance of
App.UserView
and render it, we would get
a DOM representation like this:
1
2
3
4
5
6
|
User: Albert Hofmann
<div>
<b>Posts:</b> 25
<br>
<b>Hobbies:</b> Riding bicycles
</div>
|
Relative Paths
Instead of specifying an absolute path, you can also specify which view class
to use relative to the parent view. For example, we could nest the above view
hierarchy like this:
1
2
3
4
5
6
7
8
9
10
|
App.UserView = Ember.View.extend({
templateName: 'user',
firstName: "Albert",
lastName: "Hofmann",
infoView: Ember.View.extend({
templateName: 'info',
posts: 25,
hobbies: "Riding bicycles"
})
});
|
1
2
|
User: {{firstName}} {{lastName}}
{{view infoView}}
|
When nesting a view class like this, make sure to use a lowercase
letter, as Ember will interpret a property with a capital letter as a
global property.
Setting Child View Templates
If you'd like to specify the template your child views use inline in
the main template, you can use the block form of the
{{view}}
helper.
We might rewrite the above example like this:
1
2
3
4
5
6
7
8
9
|
App.UserView = Ember.View.extend({
templateName: 'user',
firstName: "Albert",
lastName: "Hofmann"
});
App.InfoView = Ember.View.extend({
posts: 25,
hobbies: "Riding bicycles"
});
|
1
2
3
4
5
6
|
User: {{firstName}} {{lastName}}
{{#view App.InfoView}}
<b>Posts:</b> {{view.posts}}
<br>
<b>Hobbies:</b> {{view.hobbies}}
{{/view}}
|
When you do this, it may be helpful to think of it as assigning views to
portions of the page. This allows you to encapsulate event handling for just
that part of the page.
Setting Up Bindings
So far in our examples, we have been setting static values directly on the
views. But to best implement an MVC architecture, we should actually be binding
the properties of our views to the controller layer.
Let's set up a controller to represent our user data:
1
2
3
4
5
6
7
8
|
App.userController = Ember.Object.create({
content: Ember.Object.create({
firstName: "Albert",
lastName: "Hofmann",
posts: 25,
hobbies: "Riding bicycles"
})
});
|
Now let's update
App.UserView
to bind to
App.userController
:
1
2
3
4
5
|
App.UserView = Ember.View.extend({
templateName: 'user',
firstNameBinding: 'App.userController.content.firstName',
lastNameBinding: 'App.userController.content.lastName'
});
|
When we only have a few bindings to configure, like with
App.UserView
, it is
sometimes useful to be able to declare those bindings in the template. You can
do that by passing additional arguments to the
{{#view}}
helper. If all
you're doing is configuring bindings, this often allows you to bypass having to
create a new subclass.
1
2
3
4
5
6
7
|
User: {{firstName}} {{lastName}}
{{#view App.UserView postsBinding="App.userController.content.posts"
hobbiesBinding="App.userController.content.hobbies"}}
<b>Posts:</b> {{view.posts}}
<br>
<b>Hobbies:</b> {{view.hobbies}}
{{/view}}
|
NOTE: You can actually pass
any property as a parameter to {{view}}, not
just bindings. However, if you are doing anything other than setting up
bindings, it is generally a good idea to create a new subclass.
Modifying a View's HTML
When you append a view, it creates a new HTML element that holds its content.
If your view has any child views, they will also be displayed as child nodes
of the parent's HTML element.
By default, new instances of
Ember.View
create a
<div>
element. You can
override this by passing a
tagName
parameter:
|
{{view App.InfoView tagName="span"}}
|
You can also assign an ID attribute to the view's HTML element by passing an
id
parameter:
|
{{view App.InfoView id="info-view"}}
|
This makes it easy to style using CSS ID selectors:
1
2
3
4
|
#info-view {
background-color: red;
}
|
You can assign class names similarly:
|
{{view App.InfoView class="info urgent"}}
|
You can bind class names to a property of the view by using
classBinding
instead of
class
. The same behavior as described in
bindAttr
applies:
1
2
3
4
|
App.AlertView = Ember.View.extend({
priority: "p4",
isUrgent: true
});
|
|
{{view App.AlertView classBinding="isUrgent priority"}}
|
This yields a view wrapper that will look something like this:
|
<div id="sc420" class="sc-view is-urgent p4"></div>
|
Displaying a List of Items
If you need to enumerate over a list of objects, use Handlebar's
{{#each}}
helper:
1
2
3
4
|
App.PeopleView = Ember.View.extend({
people: [ { name: 'Yehuda' },
{ name: 'Tom' } ]
});
|
1
2
3
4
5
|
<ul>
{{#each people}}
<li>Hello, {{name}}!</li>
{{/each}}
</ul>
|
This will print a list like this:
1
2
3
4
|
<ul>
<li>Hello, Yehuda!</li>
<li>Hello, Tom!</li>
</ul>
|
If you want to create a view for every item in a list, just set it up as
follows:
1
2
3
4
5
|
{{#each App.peopleController}}
{{#view App.PersonView}}
{{firstName}} {{lastName}}
{{/view}}
{{/each}}
|
Writing Custom Helpers
Sometimes, you may use the same HTML in your application multiple times. In those case, you can register a custom helper that can be invoked from any Handlebars template.
For example, imagine you are frequently wrapping certain values in a
<span>
tag with a custom class. You can register a helper from your JavaScript like this:
1
2
3
4
|
Handlebars.registerHelper('highlight', function(property, options) {
var value = Ember.Handlebars.get(this, property, options);
return new Handlebars.SafeString('<span class="highlight">'+value+'</span>');
});
|
If you return HTML from a helper, and you don't want it to be escaped,
make sure to return a new
SafeString
.
Anywhere in your Handlebars templates, you can now invoke this helper:
and it will output the following:
|
<span class="highlight">Peter</span>
|
NOTE: Parameters to helper functions are passed as names, not their current values. This allows you to optionally set up observers on the values. To get the current value of the parameter, use
Ember.get
, as shown above.
Included Views
Ember comes pre-packaged with a set of views for building a few basic controls like text inputs, check boxes, and select lists.
They are:
Ember.Checkbox
1
2
3
4
|
<label>
{{view Ember.Checkbox checkedBinding="content.isDone"}}
{{content.title}}
</label>
|
Ember.TextField
1
2
3
4
5
6
|
App.MyText = Ember.TextField.extend({
formBlurredBinding: 'App.adminController.formBlurred',
change: function(evt) {
this.set('formBlurred', true);
}
});
|
Ember.Select
1
2
3
4
5
6
|
{{view Ember.Select viewName="select"
contentBinding="App.peopleController"
optionLabelPath="content.fullName"
optionValuePath="content.id"
prompt="Pick a person:"
selectionBinding="App.selectedPersonController.person"}}
|
Ember.TextArea
1
2
3
|
var textArea = Ember.TextArea.create({
valueBinding: 'TestObject.value'
});
|
If you would like to add one of these controls to your view, you are encouraged to extend from these controls.
Events do not bubble from a subview to a parent view so extending these views is the only way to capture those events.
Example:
1
2
3
4
5
6
|
App.MyText = Ember.TextField.extend({
formBlurredBinding: 'App.adminController.formBlurred',
change: function(evt) {
this.set('formBlurred', true);
}
});
|
You can then use this view as a sub view and capture the events. In the following example, a change to the Name input would blurr the form and cause the save button to appear.
1
2
3
4
5
6
7
8
9
10
11
12
|
<script id="formDetail" data-template-name='formDetail' type="text/x-handlebars">
<form>
<fieldset>
<legend>Info:</legend>
{{view App.MyText name="Name" id="Name" valueBinding="myObj.Name"}}
<label for="Name">Name</label><br/>
{{#if formBlurred}}
<a href="#" {{action "syncData" on="click"}}>Save</a>
{{/if}}
</fieldset>
</form>
</script>
|
Views In-Depth
Now that you're familiar with using Handlebars, let's go more in-depth on
how to both handle events, and customize views to your needs.
Handling Events
Instead of having to register event listeners on elements you'd like to
respond to, simply implement the name of the event you want to respond to
as a method on your view.
For example, imagine we have a template like this:
1
2
3
|
{{#view App.ClickableView}}
This is a clickable area!
{{/view}}
|
Let's implement App.ClickableView such that when it is
clicked, an alert is displayed:
1
2
3
4
5
|
App.ClickableView = Ember.View.extend({
click: function(evt) {
alert("ClickableView was clicked!");
}
});
|
Events bubble up from the target view to each parent view in
succession, until the root view. These values are read-only. If you want to manually manage views in JavaScript (instead of creating them
using the {{view}} helper in Handlebars), see the Ember.ContainerView documentation below.
Manually Managed Views with Ember.ContainerView
Usually, views create their child views by using the
{{view}}
helper. Sometimes it is useful to manually manage a view's
child views. If you create an instance of
Ember.ContainerView
, the
childViews
array is editable. Views that you add
are rendered to the page, and views that you remove are removed from the DOM.
1
2
3
4
5
|
var container = Ember.ContainerView.create();
container.append();
var coolView = App.CoolView.create(),
childViews = container.get('childViews');
childViews.pushObject(coolView);
|
As a shorthand, you can specify the child views as properties and the child views as a list of keys. When the
container view is created, these views will be instantiated and added to the child views array:
1
2
3
4
5
|
var container = Ember.ContainerView.create({
childViews: ['firstView', 'secondView'],
firstView: App.FirstView,
secondView: App.SecondView
});
|
Render Pipeline
Before your views are turned into DOM elements, they first exist as a string representation. As views render, they turn
each of their child views into strings and concatenate them together.
If you'd like to use something other than Handlebars, you can override a view's
render
method to generate a custom
string of HTML.
1
2
3
4
5
|
App.CoolView = Ember.View.create({
render: function(buffer) {
buffer.push("<b>This view is so cool!</b>");
}
});
|
This makes it easy to support template engines other than Handlebars; though do note that if you override rendering,
values will not update automatically. Any updates will be your responsibility.
Customizing the HTML Element
A view is represented by a single DOM element on the page. You can change what kind of element is created by
changing the
tagName
property.
1
2
3
|
App.MyView = Ember.View.extend({
tagName: 'span'
});
|
You can also specify which class names are applied to the view by setting its
classNames
property to an array of strings:
1
2
3
|
App.MyView = Ember.View.extend({
classNames: ['my-view']
});
|
If you want class names to be determined by the state of properties on the view, you can use class name bindings. If you bind to
a Boolean property, the class name will be added or removed depending on the value:
1
2
3
4
|
App.MyView = Ember.View.extend({
classNameBindings: ['isUrgent'],
isUrgent: true
});
|
This would render a view like this:
|
<div class="ember-view is-urgent">
|
If isUrgent is changed to false, then the
is-urgent
class name will be removed.
By default, the name of the Boolean property is dasherized. You can customize the class name
applied by delimiting it with a colon:
1
2
3
4
|
App.MyView = Ember.View.extend({
classNameBindings: ['isUrgent:urgent'],
isUrgent: true
});
|
This would render this HTML:
|
<div class="ember-view urgent">
|
Besides the custom class name for the value being
true
, you can also specify a class name which is used when the value is
false
:
1
2
3
4
|
App.MyView = Ember.View.extend({
classNameBindings: ['isEnabled:enabled:disabled'],
isEnabled: false
});
|
This would render this HTML:
|
<div class="ember-view disabled">
|
You can also specify to only add a class when the property is
false
by declaring
classNameBindings
like this:
1
2
3
4
|
App.MyView = Ember.View.extend({
classNameBindings: ['isEnabled::disabled'],
isEnabled: false
});
|
This would render this HTML:
|
<div class="ember-view disabled">
|
If the
isEnabled
property is set to
true
, no class name is added:
If the bound value is a string, that value will be added as a class name without
modification:
1
2
3
4
|
App.MyView = Ember.View.extend({
classNameBindings: ['priority'],
priority: 'highestPriority'
});
|
This would render this HTML:
|
<div class="ember-view highestPriority">
|
Attribute Bindings on a View
You can bind attributes to the DOM element that represents a view by using
attributeBindings
:
1
2
3
4
5
|
App.MyView = Ember.View.extend({
tagName: 'a',
attributeBindings: ['href'],
href: "http://emberjs.com"
});
|
The Ember Enumerable API
What Are Enumerables?
In Ember, an Enumerable is any object that contains a number of child objects, and which allows you to work with those children using the Enumerable interface. The most basic Enumerable is the built-in JavaScript Array.
For instance, all Enumerables support the standard
forEach
method:
1
2
3
|
[1,2,3].forEach(function(item) {
console.log(item);
});
|
In general, Enumerable methods, like
forEach
, take an optional second parameter, which will become the value of
this
in the callback function:
1
2
3
4
|
var array = [1,2,3];
array.forEach(function(item) {
console.log(item, this.indexOf(item));
}, array)
|
Among other reasons, you will find this useful when using another Enumerable method as a callback to
forEach
:
1
2
|
var array = [1,2,3];
array.forEach(array.removeObject, array);
|
NOTE: This second parameter helps work around a limitation of JavaScript which sets the value of
this
to
window
in methods used this way.
Enumerables in Ember
In general, Ember objects that represent lists implement the Enumerable interface. Some examples:
- Array: Ember extends the native JavaScript Array with the Enumerable interface.
- ArrayProxy: A construct that wraps a native Array and adds additional functionality for the view layer.
- Set: An object that can quickly answer whether it includes an object.
The Enumerable Interface
Parameters
The callbacks to Enumerable methods take three arguments:
- item: the item for the current iteration.
- index: an Integer, counting up from 0.
- self: the Enumerable itself.
Enumeration
To enumerate all the values of an enumerable object, use the
forEach
method:
1
2
3
|
enumerable.forEach(function(item, index, self) {
console.log(item);
});
|
To invoke some method on each element of an enumerable object, use the
invoke
method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Person = Ember.Object.extend({
sayHello: function() {
console.log("Hello from " + this.get('name'));
}
});
var people = [
Person.create({ name: "Juan" }),
Person.create({ name: "Charles" }),
Person.create({ name: "Majd" })
]
people.invoke('sayHello');
|
First and Last
You can get the first or last object from an Enumerable by getting
firstObject
or
lastObject
.
1
2
|
[1,2,3].get('firstObject')
[1,2,3].get('lastObject')
|
Converting to Array
This one is simple. To convert an Enumerable into an Array, simply call its
toArray
method.
You can transform an Enumerable into a derived Array by using the
map
method:
1
2
3
4
|
['Goodbye', 'cruel', 'world'].map(function(item, index, self) {
return item + "!";
});
|
Setting and Getting on Each Object
A very common use of
forEach
and
map
is to get (or set) a property on each element. You can use the
getEach
and
setEach
methods to accomplish these goals.
1
2
3
4
|
var arr = [Ember.Object.create(), Ember.Object.create()];
arr.setEach('name', 'unknown');
arr.getEach('name')
|
Filtering
Another common task to perform on an Enumerable is to take the Enumerable as input, and return an Array after filtering it based on some criteria.
For arbitrary filtering, use the (you guessed it)
filter
method. The filter method expects the callback to return
true
if Ember should include it in the final Array, and
false
or
undefined
if Ember should not.
1
2
3
4
5
|
var arr = [1,2,3,4,5];
arr.filter(function(item, index, self) {
if (item < 4) { return true; }
})
|
When working with a collection of Ember objects, you will often want to filter a set of objects based upon the value of some property. The
filterProperty
method provides a shortcut.
1
2
3
4
5
6
7
8
9
10
|
Todo = Ember.Object.extend({
title: null,
isDone: false
});
todos = [
Todo.create({ title: 'Write code', isDone: true }),
Todo.create({ title: 'Go to sleep' })
];
todos.filterProperty('isDone', true);
|
If you want to return just the first matched value, rather than an Array containing all of the matched values, you can use
find
and
findProperty
, which work just like
filter
and
filterProperty
, but return only one item.
If you want to find out whether every item in an Enumerable matches some condition, you can use the
every
method:
1
2
3
4
5
6
7
8
9
10
11
12
|
Person = Ember.Object.extend({
name: null,
isHappy: false
});
var people = [
Person.create({ name: 'Yehuda', isHappy: true }),
Person.create({ name: 'Majd', isHappy: false })
];
people.every(function(person, index, self) {
if(person.get('isHappy')) { return true; }
});
|
If you want to find out whether at least one item in an Enumerable matches some conditions, you can use the
some
method:
1
2
3
4
|
people.some(function(person, index, self) {
if(person.get('isHappy')) { return true; }
});
|
Just like the filtering methods, the
every
and
some
methods have analogous
everyProperty
and
someProperty
methods.
1
2
|
people.everyProperty('isHappy', true)
people.someProperty('isHappy', true)
|
Comments
Post a Comment