Getting Started with Ember.js

11 Feb 2013

Getting Started With Ember.js

Update - this article has been updated for Ember 1.0.0-rc.1

Prologue

This quick-start guide is aimed at developers who are familiar with javascript and want to get a small Ember app going without plowing through pages and pages of documentation. Along the way, I will explain basic Ember concepts you will need for a basic understanding of the Ember framework.

For an argument on why you should use Ember, please see Trek Glowacki's Advice on & Instruction in the Use of Ember.js. For comprehensive documentation on Ember, please see Emberjs.com. I hope that neither Trek nor the authors of the documentation at Emberjs.com mind that I shamelessly stole a bunch of their material for this guide. Errors, omissions, and general stupidity are all mine. Please send corrections to twbrandt [at] gmail.com.

I have based this guide on Ember 1.0.0-rc.1, Handlebars 1.0.0-rc.3, and JQuery 1.9. Ember 1.0.0-rc.1 is the most recent release at the time I wrote this. Please note that Ember 1.0.0-rc.1 requires Handlebars 1.0.0-rc.3 or greater.

Concepts

If you are familiar with Rails or other server-side MVC frameworks, most of the concepts used in Ember will look familiar to you, but keep in mind there necessarily are differences in client-side and server-side MVC. Here are the core concepts you need to know:

Templates specify your application's user interface. They are built using the Handlebars templating language. They can contain HTML, plus:

  • expressions, which take information from controllers and model objects and place them in the page.
  • outlets, which are placeholders for other templates.
  • views, which handle user events.

Views take user events, like clicks, and translates them into events implemented by your application. For example, a user could click a New user button, and the view would translate that to a newUser event implemented by your application.

Controllers store application state. Templates connect to controllers and render state information from the controllers into HTML. Controllers can pass model object properties onto the template, and can store properties which do not belong to any model object.

Models are objects that store persistent state. Model objects are typically loaded from the server, acted on by your application's user, and stored back to the server.

The router is the object which manages application state. As your application's user moves around in the app, the router makes sure the right templates are displayed and connected to correct controllers and model objects. The router keeps the URL up-to-date, so your users can save the URL and come back to the application later in the state in which they left it.

Setup

For this demo, the top-level directory (the root) contains the index.html file and a subdirectory called /js. The /js directory has four files:

  • ember.js - contains Ember 1.0.0-pre.4 downloaded from Emberjs.com.
  • handlebars.js - contains Handlebars 1.2.rc.2, downloaded from Handlebarsjs.com.
  • jquery.js - contains jQuery 1.9.1, downloaded from jQuery.com.
  • app.js - the Ember code we are writing lives here.

This is index.html with the libraries loaded in the right order:

<!DOCTYPE html>
<html>
<head>
  <title>EmberDemo</title>
  <script src="/js/jquery.js" type="text/javascript"></script>
  <script src="/js/handlebars.js" type="text/javascript"></script>
  <script src="/js/ember.js" type="text/javascript"></script>
  <script src="/js/app.js" type="text/javascript"></script>
</head>
<body>
</body>
</html>

You may want to fire up your favorite webserver at this point and make sure this loads without errors showing up on the console.

Creating the Ember app

Instantiate the Ember app in js/app.js:

this.App = Ember.Application.create();

This creates an Ember application object called App, but you can call it whatever you want. This:

  • sets up the application's namespace in which all your Ember classes will be defined.
  • adds event listeners to the document.
  • renders the application template.
  • creates a router.

The application template is the top-level, default template rendered when you start your app. You can put headers and footers in it. It must have at least one {{outlet}}, a placeholder used by the router to place other templates based on the application's state.

You can put the application template in the html document, either in the head or the body:

<script type="text/x-handlebars" data-template-name="application">
  <div>
    {{outlet}}
  </div>
</script>

The application template does not require a data-template-name attribute, but all other templates do. The {{outlet}} is where the router will place the other templates our little app will use.

The application controller is instantiated automatically for us, so we don't have to explicitly do so. But if we want to pass things on to the application template, we can define the application controller class ourselves by extending the Controller class and defining some properties:

App.ApplicationController = Ember.Controller.extend({
  firstName: "Tom",
  lastName: "Brandt"
});

and rendering them in our application template like this:

<script type="text/x-handlebars" data-template-name="application">
  Hello, {{firstName}} {{lastName}}<br />
  <div>
    {{outlet}}
  </div>
</script>

Routes

The router is responsible for loading templates, connecting them to right controller, and maintaining application state. It does this by matching the current URL to the routes defined in our app.

This defines a route called contributors:

App.Router.map(function(){
  this.route('contributors');
});

Defining a route sets up a route handler, which ties together the URL, template, and controller for that route. The route's controller is automatically set up by Ember.

The default route /, called index, is set up automatically by Ember. When our user visits /, Ember renders the index template. When our user visits /contributors, Ember renders the contributors template.

We can modify the default behavior of a route by defining our own route subclass. For example, we can change what would normally happen when our user visits / by defining an App.IndexRoute:

App.IndexRoute = Ember.Route.extend({
  setupController: function(controller) {
    // This sets the IndexController's title property:
    controller.set('title', "The is the index controller");
  }
});

We can now display the title in our index template:

<script type="text/x-handlebars" data-template-name="index">
  <div>{{title}}</div>
</script>

Models

A model object stores persistent state; that is, data that is not destroyed when we shut down our app. Objects are usually created from json streamed from a server, and saved back to the server if the user changes them.

In our little demo, we are going to create a Contributor class and instantiate a collection of contributor objects, each of which represents a contributor to the Ember project. Then, we are going to tie the model objects to the contributors route and display them on the page.

App.Contributor = Ember.Object.extend();
App.Contributor.reopenClass({
  allContributors: [],
  all: function(){
    this.allContributors = [];
    $.ajax({
      url: 'https://api.github.com/repos/emberjs/ember.js/contributors',
      dataType: 'jsonp',
      context: this,
      success: function(response){
        response.data.forEach(function(contributor){
          this.allContributors.addObject(App.Contributor.create(contributor))
        }, this)
      }
    })
    return this.allContributors;
  }
});

Let's unpack this. The extend method creates a subclass of a class. The above creates the Contributor class, which is actually a subclass of Ember.Object and inherits from it.

Next, we add some methods to the class via the reopenClass method. The first thing we do is add an array, allContributors, to our class which will hold the individual objects returned from the server.

Next, we define an all method. It:

  • empties the allContributors array. If we don't do this, the next time this method is invoked the data we get back from the server is appended to the data we retrieved previously.
  • performs an ajax call which invokes a github api call, creates contributor objects from the response, and appends them to the array.
  • returns the array as the content property of the model.

To tie the model to the contributors route, we define a route subclass:

App.ContributorsRoute = Ember.Route.extend({
  model: function() {
    return App.Contributor.all();
  }
});

The model hook in the subclass associates the Contributor class and all function with the contributors route.

Now, let's set up a template to list the contributors:

<script type="text/x-handlebars" data-template-name="contributors">
  <ul>
    {{#each contributor in controller}}
      <li>{{contributor.login}}</li>
    {{/each}}
    </ul>
</script>

The #each iterates over the array returned by the model via the controller, and renders a list item containing the login id of each contributor.

So our user does not have to manually type /contributors to see the list, we are going to add a link to our application template:

<script type="text/x-handlebars" data-template-name="application">
  Hello, {{firstName}} {{lastName}}<br />
  <nav>
    {{#linkTo "index"}}Home{{/linkTo}}
    {{#linkTo "contributors"}}Contributors{{/linkTo}}
  </nav>
  <div>
    {{outlet}}
  </div>
</script>

The argument following #linkto is the name of the route to navigate to.

Dynamic Segments

We may want to display something about each contributor in the list. Ember gives us a nice way of doing that via dynamic segments.

A dynamic segment is a part of a URL that starts with a : and is followed by an identifier.

App.Router.map(function(){
  this.route('contributors');
  this.route('contributor', {path: '/contributors/:contributor_id'});
});

Here, we have set up a route called contributor. If the id of the contributor we want is 4, the URL would be /contributors/4.

We can modify our contributors template to add a link to each individual contributor as follows:

<script type="text/x-handlebars" data-template-name="contributors">
  <ul>
    {{#each contributor in controller}}
      <li>{{#linkTo contributor contributor}} {{contributor.login}} {{/linkTo}}</li>
    {{/each}}
  </ul>
</script>

The first argument to #linkTo specifies the route, the second specifies the id of the contributor object.

Now, we define a contributor template to display various properties of the contributor.

<script type="text/x-handlebars" data-template-name="contributor">
  Contributor: {{login}}, contributions: {{contributions}}
</script>

That's it.

Epilogue

This little guide just skims the suface of Ember and omits a great deal of important stuff, but I hope gives you an idea of how powerful Ember is. Please do read Trek's article, and get familiar with the Ember docs to get a much better idea of Ember.

Source

index.html

<!DOCTYPE html>
<html>
<head>
  <title>EmberDemo</title>
  <script src="/js/jquery.js" type="text/javascript"></script>
  <script src="/js/handlebars.js" type="text/javascript"></script>
  <script src="/js/ember.js" type="text/javascript"></script>
  <script src="/js/app.js" type="text/javascript"></script>

  <script type="text/x-handlebars" data-template-name="application">
    Hello, {{firstName}} {{lastName}}<br />
    <nav>
      {{#linkTo "index"}}Home{{/linkTo}}
      {{#linkTo "contributors"}}Contributors{{/linkTo}}
      </nav>
    <div>
      {{outlet}}
    </div>
  </script>

  <script type="text/x-handlebars" data-template-name="index">
    <div>{{title}}</div>
  </script>
  
  <script type="text/x-handlebars" data-template-name="contributors">
    <ul>
      {{#each contributor in controller}}
        <li>{{#linkTo contributor contributor}} {{contributor.login}} {{/linkTo}}</li>
      {{/each}}
      </ul>
  </script>
  
  <script type="text/x-handlebars" data-template-name="contributor">
    Contributor: {{login}}, contributions: {{contributions}}
  </script>
</head>
<body>
</body>
</html>

app.js

this.App = Ember.Application.create();

App.ApplicationController = Ember.Controller.extend({
  firstName: "Tom",
  lastName: "Brandt"
});

App.Router.map(function(){
  this.route('contributors');
  this.route('contributor', {path: '/contributors/:contributor_id'});
});

App.IndexRoute = Ember.Route.extend({
  setupController: function(controller) {
    controller.set('title', "The is the index controller");
  }
});

App.ContributorsRoute = Ember.Route.extend({
  model: function() {
    return App.Contributor.all();
  }
});

App.Contributor = Ember.Object.extend();
App.Contributor.reopenClass({
  allContributors: [],
  all: function(){
    this.allContributors = [];
    $.ajax({
      url: 'https://api.github.com/repos/emberjs/ember.js/contributors',
      dataType: 'jsonp',
      context: this,
      success: function(response){
        response.data.forEach(function(contributor){
          this.allContributors.addObject(App.Contributor.create(contributor))
        }, this)
      }
    })
    return this.allContributors;
  }
});
By Tom Brandt © 2013