I hope that you're starting to see that Ember.js is a powerful, yet opinionated, framework. We've only scratched its surface; there's more to learn before we can build something truly useful! We'll continue using the Ember Starter Kit. In this portion of the series, we'll review accessing and managing data within Ember.
Playing with Data
In the last article, we worked with a static set of color names that were defined within a controller:
App.IndexRoute = Ember.Route.extend({ setupController: function(controller) { controller.set('content', ['red', 'yellow', 'blue']); } });
This allowed the controller to expose the data to the index template. That's cute for a demo, but in real life, our data source will not be a hard-coded array.
This is where models comes in. Models are object representations of the data your application uses. It could be a simple array or data dynamically retrieved from a RESTful JSON API. The data itself is accessed by referencing the model's attributes. So, if we look at a result like this:
{ "login": "rey", "id": 1, "age": 45, "gender": "male" }
The attributes exposed in the model are:
- login
- id
- age
- gender
Data itself is accessed by referencing the model’s attributes.
As you see from the code above, you could define a static store, but you'll use Ember.Object for defining your models most of the time. By subclassing Ember.Object
, you'll be able to return data (e.g.: via an Ajax call) and define your model. While you can explicitly set data within a controller, it's always recommended that you create a model in order to adhere to separation of concerns and code organization best practices.
Alternatively, you could use a sister framework called Ember Data. It is an ORM-like API and persistence store, but I need to stress that it is in a state of flux as of this writing. It has a lot of potential, but using Ember.Object
is much safer at this time. Robin Ward, co-founder of Discourse, wrote a great blog post on using Ember without Ember Data. It outlines their process, which I'll break down for you.
Defining your Models
In the following example, I'm going to use the unofficial Hacker News API to pull JSON-based data from the news resource. This data will be stored in my model and later used by a controller to fill a template. If we look at the data returned from the API, we can understand the properties we'll be working with:
{ "nextId": null, "items": [{ "title": "Docker, the Linux container runtime: now open-source", "url": "http://docker.io", "id": 5445387, "commentCount": 39, "points": 146, "postedAgo": "2 hours ago", "postedBy": "shykes" }, { "title": "What\u0027s Actually Wrong with Yahoo\u0027s Purchase of Summly", "url": "http://hackingdistributed.com/2013/03/26/summly/", "id": 5445159, "commentCount": 99, "points": 133, "postedAgo": "2 hours ago", "postedBy": "hoonose" }, ], "version": "1.0", "cachedOnUTC": "\/Date(1364333188244)\/" }
I want to work with the items
property, which contains all of the headlines and story information. If you've worked with SQL databases, think of each element of items
as a record and the property names (i.e.: title
, url
, id
, etc.) as field names. It's important to grok the layout because these property names will be used as the attributes of our model object—which is a perfect segue into creating the model.
Ember.Object
is the main base class for all Ember objects, and we'll subclass it to create our model using itsextend()
method.
To do this, we'll add the following code to js/app.js immediately after the code that defines App.IndexRoute
:
App.Item = Ember.Object.extend();
App.Item
serves as the model class for the Hacker News data, but it has no methods to retrieve or manipulate that data. So, we'll need to define those:
App.Item.reopenClass({ all: function() { return $.getJSON("http://api.ihackernews.com/page?format=jsonp&callback=?").then(function(response) { var items = []; response.items.forEach( function (item) { items.push( App.Item.create(item) ); }); return items; }); } });
Let's break down this code. First, we use Ember's reopenClass()
method to add our new methods to the App.Item
class, and you pass it an object that contains our desired methods. For this example, we only need one method called all()
: it returns all of the headlines from the Hacker News frontpage. Because jQuery is part of the deal with Ember, we have its simple Ajax API at our disposal. The API uses JSONP to return JSON data; so, I can just use $.getJSON()
to make the request to:
$.getJSON("http://api.ihackernews.com/page?format=jsonp&callback=?")
The "callback=?" tells jQuery that this is a JSONP request, and the data (once it's retrieved) is passed to an anonymous callback handler defined using jQuery's promises functionality:
.then(function(response) {...});
I can easily pump in my JSON data into an Ember object.
The response
parameter contains the JSON data, allowing you to loop over the records and update the local items
array with instances of App.Item
. Lastly, we return the newly populated array when all()
executes. That's a lot of words, so let me summarize:
- Create your new model class by subclassing
Ember.Object
usingextend()
. - Add your model methods using
reopenClass()
. - Make an Ajax call to retrieve your data.
- Loop over your data, creating an
Item
object and pushing it into an array. - Return the array when the method executes.
If you refresh index.html, you'll see nothing has changed. This makes sense because the model has only been defined; we haven't accessed it.
Exposing Your Data
Controllers act like proxies, giving you access to the model's attributes and allowing templates to access them in order to dynamically render the display. In addition to accessing attributes from an associated model, controllers can also store other application properties that need to persist without saving to a server.
Currently, our app has the following controller (the one that defines a static data set):
App.IndexRoute = Ember.Route.extend({ setupController: function(controller) { controller.set('content', ['red', 'yellow', 'blue']); } });
We can directly associate our model with App.IndexRoute
using the model
method (AKA the model hook):
App.IndexRoute = Ember.Route.extend({ model: function() { return App.Item.all(); } });
Remember that Ember defines your controller if you don't explicitly define it yourself, and that is what's happening in this case.
Behind the scenes, Ember creates
IndexController
as an instance ofEmber.ArrayController
, and it uses the model specified in themodel
method.
Now we just need to update the index template to access the new attributes. Opening index.html, we can see the following Handlebars template code:
{{#each item in model}} <li>{{item}}</li> {{/each}}
With one small change (adding the title
property), we can immediately see the titles returned from the Hacker News API:
{{item.title}}
If you refresh your browser now, you should see something similar to the following:
<h3>Welcome to Ember.js</h3> <ul><li>Persona is distributed. Today.</li> <li>21 graphs that show America's health-care prices are ludicrous</li> <li>10 000 concurrent real-time connections to Django</li> <li>Docker, the Linux container runtime: now open-source</li> <li>Let's Say FeedBurner Shuts Down…</li></ul>
If you want to display more information, simply add more properties:
{{item.title}} - {{item.postedAgo}} by {{item.postedBy}}
Refresh to see the updates you've made. That's the beauty of Handlebars; it makes it trivial to add new data elements to the user interface.
As I mentioned before, controllers can also be used to define static attributes that need to persist throughout the life of your application. For example, I may want to persist certain static content, like this:
App.IndexController = Ember.ObjectController.extend({ headerName: 'Welcome to the Hacker News App', appVersion: 2.1 });
Here, I subclass Ember.ObjectController
to create a new controller for my index route and template to work with. I can now go to index.html and update my template to replace the following:
<h2>Welcome to Ember.js</h2>
with:
<h2>{{headerName}}</h2>
Models are object representations of the data your application uses.
Handlebars will take the specified attributes in my controller and dynamically replace the {{headerName}}
placeholder with its namesake value. It's important to reinforce two things:
- By adhering to Ember's naming conventions, I didn't have to do any wiring to be able to use the controller with the index template.
- Even though I explicitly created an
IndexController
, Ember is smart enough not to overwrite the existing model that's been associated via the route.
That's pretty powerful and flexible stuff!
Next up...Templates
Working with data in Ember isn't difficult. In actuality, the hardest part is working with the various APIs that abound on the web.
The fact that I can easily pump in my JSON data into an Ember object makes management substantially easier—although I've never been a big fan of large data sets on the client-side, especially when represented as objects.
It's something I'll have to do more testing on, and I hope that Ember Data makes all of this trivial.
With that said, I briefly touched on templates in this article. They're very important... so much so that I want to tackle this topic in its own article. So in the next article, we'll go over how to leverage Handelbars to build your user interface and drill-down into the various directives the templating framework offers.
Comments