One of the chief advantages of having a front-end framework is being able to store and manipulate data entirely on the front-end, without needing to explicitly make AJAX requests. This is accomplished through a data layer, which for Ember is a library called ember-data. In this session, we'll look at how to use ember-data to set up front-end models and perform CRUD actions on them.
By now, you have already learned how to:
By the end of this session, you should be able to:
npm install
and bower install
.bin/rails server
ember server
In the past few days, you've seen a whole lot of Ember's 'view' layer - the system governing how Ember responds to user behavior and controls what HTML gets rendered in the browser.
While this is all very nice, it's meaningless without an API. That's where
Ember's 'data' layer comes in, as implemented by an add-on library to Ember
called ember-data
.
ember-data
provides several Ember Classes for handling the exchange of
information between the client and the API, most notably Models (which
represent back-end resources as Ember Objects) and Adapters (which manage the
actual interactions with the underlying API(s)).
We'll start with the solution from ember-components
.
lists
routeindex
route to lists
routelists
route from index
routeember generate route lists
list
model (for now, we'll leave items off)shopping-list/card
component as the top-level interface to listsshopping-list/card
(without any action)lists
route template to use shopping-list/card
lists
route model
method to use the ActiveModelAdapterember generate model list title:string hidden:boolean
This will create a new model.js
file inside app/list
. The README for the API
shows us the data we can expect at GET
/lists. Note that the
items returned are just ids. We specified the properties that we want the
ember-data
model to have. We could all of the properties from the API, but
we're leaving off items because we haven't created and item
model, yet.
DS.attr
is how we define attributes for our models. The default types are
'number', 'string', 'boolean', and 'date', but we can define our own if we
really need to. We can also use DS.attr
to specify a default value for a
given attribute by passing in an optional object as a second argument.
As we saw in the material on routing, each Route has a model
method that
exposes data to the template. Each Route also has a store
property which
refers to whatever data store our application is using (in this case,
ember-data), so to make the List model available in the lists
route, we
reference the store and query it for all instances.
export default Ember.Route.extend({
model () {
return this.get('store').findAll('list');
}
});
item
modellist
modelapp/router.js
for the single list routemodel
method to the list
routeshopping-list
component from the list
route templatelist
route from the shopping-list/card
templateember generate model item content:string done:boolean list:belongs-to:list
ember generate route list
export default DS.Model.extend({
title: DS.attr('string'),
hidden: DS.attr('boolean'),
+ items: DS.hasMany('item'),
});
Router.map(function () {
this.route('lists');
- this.route('list');
+ this.route('list', { path: '/lists/:list_id' });
});
export default Ember.Route.extend({
+ model (params) {
+ return this.get('store').findRecord('list', params.list_id);
+ },
});
Now that we've refactored ListR to use data from the API, we'll move on to persisting changes.
Now that we have models loaded in our Routes, it's finally time to tie all of this together.
Before talking about CRUD, though, we should start by talking about something
you touched on in the material on Components: 'actions'. 'Actions' are a special
class of trigger-able events that are handled by the Ember.ActionHandler
Ember
Class. Like normal events, actions 'bubble up', moving from the leaf (i.e.
Template) to the root (i.e. the 'application' Route) until they are met by a
matching handler.
In Ember 1, action handlers inside the Controller were used to perform CRUD on the model. This made sense, since the Controller was responsible for managing all of the business data in our application, and since it mirrored how responsibilities were broken out in Rails. An action could be triggered in a Template and bubble up to a Controller, where it would cause that Controller to manipulate the given Model.
However, with the shift towards Components in Ember 2, a lot of the
functionality of Controllers has been made redundant and moved into other
entities within the app. In this case, Components and Routes both incorporate
Ember.ActionHandler
, so we can instead set our action handlers there. For
simplicity's sake, we'll put all handlers related to Model CRUD into the Route;
any other action handlers can be placed in either place.
Defining Action handlers in a Route is very easy. Simply open up the route.js
file and make the following addition:
import Ember from 'ember';
export default Ember.Route.extend({
model: function(...){
...
},
actions: {
create () { ... },
update () { ... },
destroy () { ... },
// ... etc
}
});
To trigger an action, you can add an {{action ... }}
helper to an element
(usually a button) - this will cause that element to launch the action whenever
it executes its defaults behavior (in the case of a button, being clicked).
In Ember applications that use Components (which will soon be all of them) the generally recommended strategy is to follow a 'data down, actions up' design pattern, which essentially means two things:
shopping-list/item
component
shopping-list
component
toggleDone='toggleItemDone'
to invoking shopping-list/item
list
route
toggleItemDone='toggleItemDone'
to invoking shopping-list
export default Ember.Component.extend({
tagName: 'li',
classNameBindings: ['listItemCompleted'],
- listItemCompleted: false,
+ listItemCompleted: Ember.computed.alias('item.done'),
actions: {
toggleDone () {
- return this.toggleProperty('listItemCompleted');
+ return this.sendAction('toggleDone', this.get('item'));
},
},
});
classNameBindings: ['listDetailHidden'],
listDetailHidden: false,
actions: {
+ toggleItemDone (item) {
+ return this.sendAction('toggleItemDone', item);
+ },
+
toggleListDetail () {
return this.toggleProperty('listDetailHidden');
},
model (params) {
return this.get('store').findRecord('list', params.list_id);
},
+
+ actions: {
+ toggleItemDone (item) {
+ item.toggleProperty('done');
+ item.save();
+ },
+ },
});
shopping-list/item
component
{{action 'delete'}}
delete
action to send that action upshopping-list
component
delete='deleteItem'
to invoking shopping-list/item
deleteItem
action to send the action uplist
route
deleteItem='deleteItem'
to invoking shopping-list
shopping-list
component
each
with {{action "createItem" on="submit"}}
value=newItem.content
newItem
propertycreateItem
action to send the action uplist
route
createItem='createItem'
to invoking shopping-list
Does it work?
Unfortunately, no. The API uses a nested route for creating new list items.
This doesn't fit directly with ember-data
's modeling of APIs, so we have to do
some extra work.
We'll extend the default application adapter, included in ember-template
to
handle this case.