In this fourth post in our series on Ember, we will continue to work from the
development environment that we worked on in the
first
three
posts.
This installment will focus on models, backed by RESTful storage, using
ember-data. First, a giant, red, warning: ember-data
is not production ready. It is under active development and changes often.
There are bugs and limitations and you’ll have to wait for them to be
fixed. We at Embedly, in our youthful exuberance, decided to ignore these
problems and blaze forward into the future! If you do decide to follow us, stay
up-to-date with breaking changes at
BREAKING_CHANGES.md.
As usual, you can find the complete code for the demo project at
github. We won’t cover every code change here, just the ones directly
related to ember models and ember-data.
If you remember, in our
last
installment, we created a team page. We used fixtures to provide data
to the template. Let’s switch out these fixtures for some data provided by
a RESTful api. We’ve built a simple api server in node.js to connect to. It is
only an example and does the bare minimum to get us up and running. Please
don’t consider using it for anything remotely serious.
Adding ember-data
We are using bower to manage our javascript dependencies.
There is an ember-data wrapper, called ember-data-shim, made
specifically for dependency management. If you use it, make sure it is
relatively up-to-date when you add it to your own project. Here is our new
component.json.
// bower.json
{
"name": "demo",
"version": "0.0.0",
"dependencies": {
"modernizr": "~2.6.2",
"jquery": "~1.9.1",
"handlebars": "1.0.0-rc.3",
"ember": "1.0.0-rc.3",
"ember-data-shim": "0.0.12"
},
"devDependencies": {}
}
Running bower install will install the dependency into our
app/components path. Now we just have to add
ember-data.js to our app/index.html.
// app/index.html
- snip -
<script src="components/ember/ember.js"></script>
<script src="components/ember-data-shim/ember-data.js"></script>
- snip -
If everything went as planned, we should be able to start the app, open it in
the browser, and check to see if there is a global DS object
available in the browser’s javascript console.
Configuring ember-data
There is a little bit of configuration to do to get ember-data
working. We’ll add it to app/scripts/main.js. The first part tells
ember-data what version we are planning on using. This is important since the
API has breaking changes between revisions. We don’t want to accidentally upgrade
before we are ready. The second section tells the RESTAdapter the
base URL for our RESTful api.
// app/scripts/main.js
- snip -
App.Store = DS.Store.extend({
revision: 12
});
DS.RESTAdapter.reopen({
url: '/rest/1'
});
- snip -
You may be asking yourself, what is the RESTAdapter, and what other adapters
are there? Ember-data comes packaged with a RESTAdapter and
FixtureAdapter. The FixtureAdapter provides an in memory browser
store and is mostly useful for testing and development.
FixtureAdapter was an option when writing this post, but
I also wanted to show the expected responses and results of the API calls,
so that when you go to implement your own RESTful service, you’ll know what
to expect.
Defining Our Model
Defining our model is very simple. First we’ll remove all our “Fixtures” from
app/scripts/modules/about.js, and replace them with our model
definitions in app/scripts/modules/models.js.
// app/scripts/modules/models.js
App.Org = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
members: DS.hasMany('App.Member')
});
App.Member = DS.Model.extend({
name: DS.attr('string'),
org: DS.belongsTo('App.Org')
});
// app/index.html
- snip -
<script src="scripts/modules/models.js"></script>
- snip -
Here we are only using the string attr type, but string, number,
boolean, and date are also available. It is also possible to define your own
attr types as well, but we won’t cover that here.
We are also defining a many-to-one relationship between Member and
Org. We really only have one Org, but who knows how
our site will grow in the future. It is possible to declare many-to-many
relationships, but let’s keep things simple.
Wiring up our Route and Controller
Let’s update the about page to use our new model, replacing our old fixture
code.
// app/scripts/main.js
App = Em.Application.create({
rootElement: $('#app'),
orgId: 0
});
- snip -
// app/scripts/modules/about.js
- snip -
App.AboutRoute = Em.Route.extend({
model: function() {
return App.Org.find(App.get('orgId'));
}
});
// app/templates/about.handlebars
- snip -
{{render "team" controller.members}}
We’ve configured our orgId in the App object. This is
a good place to put site-wide configuration. Next we replaced our fixture with
a call to App.Org.find() in our Route#model.
ember-data will return an empty object, then switch in the
Org when it’s ready. Since Ember.js is so wonderful, it will
re-render the relevant parts of the view on update. Finally, we replace the
members fixture in the about.handlebars with the
members field of the Org object, which is an Array
of organization members.
Now our application behaves exactly the same as it did before we implemented
ember-data, except that it is backed by a RESTful API. Pretty
simple, eh? But not very interesting, we’ll have to add some CRUD operations to
truly demonstrate ember-data’s power. Let’s take a look at,
what’s happening between the server and client before we move on to mutating
the data.
Retrieving records using ember-data is extremely simple. One
thing to keep in mind is that it’s asynchronous. Usually you can safely ignore
that fact, due to the way ember’s event system works, but sometimes it can
bite you, so keep it in mind. We retrieve our Org with the line
App.Org.find(App.get('orgId')). Let’s take a look at what our
server returns.
// Request
GET /rest/1/orgs/0
// Response
{
"org": {
"name": "jubarian.org",
"description": "A non-profit organization of people...",
"member_ids": [ 0, 1, 2, 3, 4, 5, 6 ]
},
"members": [{
"org_id": 0,
"name": "Nina",
"id": 0
}, {
"org_id": 0,
"name": "Kawan",
"id": 1
- snip -
}]
}
There are a few things to be aware of here. First, we can see that our API
puts the payload of the org object into a field called
org (singular). This is where ember-data will look for the
Org data. If we had made a more general query that returned a
list, it would be orgs in the plural form. Second, we are also
returning all the members data with the response. This is referred to as
side-loading, and it is optional. In our case, since we have the luxury of a
very small application with well defined requirements, we know that this is the
most efficient way to do things. If we didn’t side-load, ember-data
will make another call to retrieve the data when it’s needed.
Notice that the members field is plural, it’s a list, and ids
are included in the objects. Also notice the serialization of the data fields.
Ember expects all serialized field names to be underscore separated, even
though it expects camelcase in the javascript models. It also expects a
pluralized member_ids field for the one-to-many relationship, and
a singular org_id for the many-to-one relationship.
All of these behaviors can be modified. Ember-data is very customizable, but
unless you are dealing with a legacy API, it’s always best to conform with
the frameworks expectations.
Ember Data’s Protocol
Let’s give a brief overview of what ember-data expects to send and
receive during different transactions. Understanding this protocol will not
only help you implement your own persistent backend, but also is invaluable for
debugging ember-data errors, which are often the result of some
unexpected data. Many of these details can be modified by configuring the
RESTAdapter, but again, if possible, it’s better to stick with the
standard. Keep in mind that if you have an existing API that is much different,
it might be worth writing your own adapter. We won’t cover custom adapters in
this post.
GET one
- Call
find(:id)
- HTTP Method
- GET
- URL
- /<plural model>/:id
- Example Request
- GET /orgs/0
- Example Response
-
{
"org": {
"name": "jubarian.org",
"description": "A non-profit organization of people...",
"member_ids": [ 0, 1, 2, 3, 4, 5, 6 ]
}
}
GET many
- Call
find({:attr: :value}
- HTTP Method
- GET
- URL
- /<plural model>?:attr=:value
- Example Request
- GET /orgs?name=jubarian.org
- Example Response
-
{
"orgs": [
{
"id": 0,
"name": "jubarian.org",
"description": "A non-profit organization of people...",
"member_ids": [ 0, 1, 2, 3, 4, 5, 6 ]
}
]
}
With mutations, we need to commit our changes before they will be sent
to the server, either through the records transaction or the store. If
we change our minds before we commit, we can invoke rollback
instead of commit.
POST
- Call
-
var record = Model.createRecord({...});
record.get('store').commit();
- HTTP Method
- POST
- URL
- /<plural model>
- Example Request
- POST /members
- Example Body
-
{"member":{"name":"Billy","org_id":0}}
- Example Response
-
{"member":{"name":"Billy","org_id":0, "id": 7}}
PUT
- Call
-
var record = Model.find(:id);
record.set(:name, :value);
record.get('store').commit();
- HTTP Method
- PUT
- URL
- /<plural model>/:id
- Example Request
- PUT /members/7
- Example Body
-
{"member":{"name":"William","org_id":0}}
- Example Response
-
{"member":{"name":"William","org_id":0}}
One thing to note is that DELETE MUST return valid json, or you
will get an error. I consider this a bug, so hopefully it changes in the
future.
DELETE
- Call
-
var record = Model.find(:id);
record.deleteRecord();
record.get('store').commit();
- HTTP Method
- DELETE
- URL
- /<plural model>/:id
- Example Request
- DELETE /members/7
- Example Response
-
{}
Creating a Team Admin Page
Next, let’s put it all together and build a member management section for our
site. First let’s setup the routes. We’ll have three pages; an index, a new
member form and an edit member form.
// app/scripts/main.js
- snip -
App.Router.map(function(){
this.route('about');
this.resource('members', function() {
this.route('new');
this.route('edit', {path: '/edit/:id'});
});
});
- snip -
Now, let’s build the Routes and Controllers.
// app/scripts/modules/members.js
App.MembersIndexRoute = Em.Route.extend({
setupController: function(controller, model) {
App.Org.find(App.get('orgId')).then(function(org) {
controller.set('content', org.get('members'))
});
controller.set('content', Em.A());
}
})
App.MembersEditRoute = Em.Route.extend({
serialize: function(obj) {
return {id: obj.get('id')};
},
model: function(params) {
return App.Member.find(params.id);
}
})
App.MembersNewRoute = Em.Route.extend({
model: function(params) {
return Em.Object.create({org: App.Org.find(App.get('orgId'))});
}
})
/*
* Wrap member model objects in simple interface callable
* by the view.
*/
App.MemberController = Em.ObjectController.extend({
"delete": function() {
this.get('content').deleteRecord();
this.get('store').commit();
},
edit: function() {
this.transitionToRoute("members.edit", this);
},
save: function() {
this.get('store').commit();
this.transitionToRoute("members.index");
},
cancel: function() {
this.get('store.defaultTransaction').rollback();
this.transitionToRoute("members.index");
},
create: function() {
this.set('content', App.Member.createRecord(this.get('content')));
this.save();
},
cancelNew: function() {
this.transitionToRoute("members.index");
}
})
App.MembersIndexController = Em.ArrayController.extend({
itemController: "member"
})
App.MembersNewController = App.MemberController.extend({});
App.MembersEditController = App.MemberController.extend({});
// app/index.html
- snip -
<script src="scripts/modules/members.js"></script>
- snip -
If you’ll notice, in MembersIndexController we forgo using the
model callback in favor of the setupController
callback. This is because we are passing in a field of the result of an
asynchronous operation. MembersIndexController needs an
Array right away to set itself up. If we called something like
Members.find({org_id: 0}) then ember-data would have returned an
empty array right away and we would have been fine, but I wanted to showcase a
situation where the asynchronous nature of ember-data can leak through, so it’s
important to be aware of. Once the call has returned, we can safely swap the
result into the controllers content field and everything will be
updated as expected.
We’ve also introduced a new controller callback serialize, in
the MembersEditController. If your Route has parameters, it is
important to provide a way to map the object it represents to the URL
parameters. This way when we transition to the route and pass an object
directly to it, ember can form a linkable URL out of it. Our serialize method
maps the id attribute of the model to the id
parameter in the edit URL.
Our MembersIndexController introduces the concept of
itemController. itemController wraps each item in an
ArrayController, making it easier to perform actions on individual
items from the view. In our case, it will allow us to easily delete and edit
the individual items.
We used inheritance to get all the functionality of
MemberController into MembersEditController and
MembersNewController. Since we haven’t added any view logic, we
are using the default generated views. Let’s finish up by adding templates for
our member management pages, and a link from the about page.
// app/templates/about.handlebars
- snip -
<p>
<a href="#" {{action toggleTeam}}>{{toggleTeamLabel}}</a>
{{#linkTo members.index}}manage team{{/linkTo}}
</p>
- snip -
// app/templates/members.handlebars
<p>{{#linkTo "about"}}about{{/linkTo}}</p>
<p>{{outlet}}</p>
// app/templates/members/index.handlebars
<p>List Members</p>
<ul>
{{#each controller}}
<li>
{{name}} <a href="#" {{action delete}}>delete</a> <a href="#" {{action edit}}>edit</a>
</li>
{{/each}}
</ul>
{{#linkTo "members.new"}}add member{{/linkTo}}
// app/templates/members/edit.handlebars
<p>Edit Member</p>
<p><label>
Name
{{view Em.TextField valueBinding="name"}}
</label></p>
<p>
<a href="#" {{action save}}>save</a>
<a href="#" {{action cancel}}>cancel</a>
</p>
// app/templates/members/new.handlebars
<p>Create Member</p>
<p><label>
Name
{{view Em.TextField valueBinding="name"}}
</label></p>
<p>
<a href="#" {{action create}}>save</a>
<a href="#" {{action cancelNew}}>cancel</a>
</p>
Security
Obviously, if this were a real application we wouldn’t want just anybody to
be able to modify our member list. We’d have to implement some security, but
that is outside the scope of this series of articles. One thing to be aware of
is that your RESTful API is publicly available, so make sure your security is
implemented there, and not on the frontend. Your ember app should be aware
of security, but your API should enforce it.
Error Handling
Error handling in ember-data leaves much to be desired, and
this is where the fact that it’s a beta project is really obvious. It’s
possible to build some error handling via callbacks and states on model
objects, but it’s difficult and ever changing. Even finding documentation on
the relevant callbacks and states is difficult. It usually involves reading the
ember-data code base. We’ve chosen to skip it in this article. It would be of
little lasting value, since we expect it to mature rather quickly.
Conclusion
I hope this article, and this series has been able to give you a good high
level understanding of ember and ember-data and get you quickly started on
the right path. Even if you choose not to use ember-data and
roll-your-own, I hope this article has given you some ideas on how to build
it. This wraps up our series on Ember at Embedly. Now go create something
awesome!