Day 19: Ember--The Missing EmberJS Tutorial

So far in this series we have looked at Bower, AngularJS, GruntJS, PhoneGap, and MeteorJS JavaScript technologies. Today for my 30 day challenge, I decided to learn a framework called Ember. In this blog post, we will learn how to build a single page social bookmarking site using Ember. This tutorial will be covered in two posts -- first post will cover the client side and persist data to HTML 5 Local Storage and in the second post we will use a RESTful backend deployed on OpenShift. I will write the second post in next few days.

EmberJS

Application Usecase

In this blog, we will develop a social bookmarking application which allows users to post and share links. You can view the live application here. The application can do the following :

  • When a user goes to the '/' url of the application, then the user will see a list of stories sorted by their submission date.

GetBookMarks Home Page

  • When a user clicks on any story i.e. #/stories/d6p88, the user can view the content of the story like who submitted the story, when the story was submitted, and an excerpt of the story.

GetBookmarks Story View

  • Finally, a user can submit a new story by navigating to #/story/new. This will store the story in the user's browser local storage.

GetBookmarks Story Submit

What is Ember?

Ember is a client side JavaScript MV* framework for building ambitious web applications. Ember has dependencies on the jQuery and Handlebars libraries. If you have worked with Backbone, then you will find Ember as an opinionated Backbone or Backbone++. Ember can get lot of things done for you if you follow its naming conventions. Ember.js strongly follows naming conventions. So, if we have a url route /stories in the app, then we will have the following:

  • a stories template
  • a StoriesRoute
  • a StoriesController

To understand Ember naming conventions, please refer to the documentation on naming conventions.

Ember Core Concepts

In this section we will cover the four main core concepts in EmberJS which we will use in today's demo application.

  1. Model : Models represents the application domain object which we present to the user. In the application use case discussed above, a story represents a model. Story along with its attributes like title, url, etc. makes the model. Models can be retrieved and updated either by using jQuery to load JSON data from the server or apps can use Ember Data. Ember Data is the client side ORM implementation which make it easy to perform CRUD operations on the underlying persistent storage. Ember Data provides a repository interface, which can be configured with a range of provided Adapters. The two core adapters provided with Ember Data are RESTAdapter and FixtureAdapter. In this blog, we will use LocalStorage adapter which persists data into HTML 5 LocalStorage. Please refer to the documentation for more details.

  2. Router and Route : A Router is used to specify all the application routes. Router maps a url to a route. For example, when user goes to '/#/story/new', then newstory template will be rendered. The newstory template represents an HTML form. Users can customise the routes by creating an Ember.Route subclass. In the above mentioned example, suppose when the user navigates to '/#/story/new', user want to render a default model in newstory template. The NewStoryRoute will responsible for giving a default model to the newstory template. Please refer to the documentation for more details.

  3. Controller : Controller can do two things -- first it can decorate model returned by the route and second it can listen to actions performed by user. For example, when a user submit the story form with the relevant data, then the NewStoryController will be responsible for persisting the story data into the underlying storage using Ember Data API. Please refer to the documentation for more details.

  4. Template : Templates represent the user interface of the application. Every application has one default template called application. This will be rendered when the application starts. The header, footer, navigation, and other common content should be placed in this template. Ember.js uses the Handlebars templating library to power the application user interface.

Ember Chrome Extension

EmberJS also provides a chrome extension which makes it very easy to debug the ember application. The extension is available in the chrome web store. To learn more about chrome extension, please refer to these short videos by the Ember team.

Github Repository

The code for today's demo application is available on github: day19-emberjs-demo.

Step 1 : Download the starter kit

The ember framework provides a starter kit which makes it very easy to get started with the framework. The starter kit contains required javascript files(ember-*.js, jquery-*.js, and handlerbars-*.js) and a sample application. Download the starter kit, then unzip it, and finally rename the folder to getbookmarks as shown below. The getbookmarks is the name of the application.

$ wget https://github.com/emberjs/starter-kit/archive/v1.1.2.zip
$ unzip v1.1.2.zip 
$ mv starter-kit-1.1.2/ getbookmarks

Open the index.html in your favourite modern browser(Chrome or Firefox), and you will see the sample application.

Step 2 : Enable GruntJS Watch(Optional)

This step is optional but if you follow it, then it will make your life awesome. If you decide to not follow this step, then open the index.html in your browser and ever time we will make a change please reload the browser. In day 7 blog, I talked about using GruntJS live reload functionality to automatically reload the changes. I did not found any auto reload functionality in EmberJS so I decided to use GruntJS livereload to make me more productive. You will need Node, NPM , Grunt-CLI installed on your machine. Please refer to my day 5 and day 7 blogs where I covered them in detail.

Create a new file called package.json in getbookmarks folder and paste the following content into it.

{
  "name": "getbookmarks",
  "version": "0.0.1",
  "description": "GetBookMarks application",
  "devDependencies": {
    "grunt": "~0.4.1",
    "grunt-contrib-watch": "~0.5.3"
  }
}

Create another file called Gruntfile.js in getbookmarks folder and paste the following content into it.

module.exports = function(grunt) {
 
 
  grunt.initConfig({
 
    watch :{
      scripts :{
        files : ['js/app.js','css/*.css','index.html'],
        options : {
          livereload : 9090,
        }
      }
    }
 
  });
 
  grunt.loadNpmTasks('grunt-contrib-watch');
 
  grunt.registerTask('default', []);  
};

Install the dependencies using npm.

$ npm install grunt --save-dev
$ npm install grunt-contrib-watch --save-dev

In the index.html, add the <script src="http://localhost:9090/livereload.js"></script> to the head of html.

Then invoke the grunt watch command, and open the index.html in your favourite modern browser.

$ grunt watch
Running "watch" task
Waiting...OK

Make a change to index.html and without any browser reload you will see the change applied.

Step 3 : Understand starter template application

In the starter template, there are two application related files(apart from css) -- index.html and app.js. To understand what template application does, we have to understand the app.js file.

App = Ember.Application.create();
 
App.Router.map(function() {
  // put your routes here
});
 
App.IndexRoute = Ember.Route.extend({
  model: function() {
    return ['red', 'yellow', 'blue'];
  }
});

The code shown above does the following:

  1. The first line App = Ember.Application.create(); creates an instance of the Ember application. This will create a new instance of Ember.Application and make it available as a variable within the browser's JavaScript environment.

  2. The App.Router.map is used to define the application route. Every Ember application has one default route called Index which is available at '/' url. So, when the index '/' route is invoked then index template will be rendered. Based on the application url template is rendered. Lot of convention over configuration. The index template is defined in index.html.

  3. In Ember, every template is backed by a model. A Route responsible for specifying which template should be backed by which model. In the app.js shown above, IndexRoute returns a String array as the model for index template. The index template just iterates over this array and renders a list.

Step 4 : Remove starter template code

Please remove all the content from js/app.js file and paste the following content to it.

App = Ember.Application.create();
 
App.Router.map(function() {
  // put your routes here
});

Similarly replace the index.html with the following content.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GetBookMarks -- Share your favorite links online</title>
  <link rel="stylesheet" href="css/normalize.css">
  <link rel="stylesheet" href="css/style.css">
  <script src="http://localhost:9090/livereload.js"></script>
</head>
<body>
 
  <script type="text/x-handlebars">
    {{outlet}}
  </script>
 
  <script type="text/x-handlebars" data-template-name="index">
 
  </script>
 
  <script src="js/libs/jquery-1.9.1.js"></script>
  <script src="js/libs/handlebars-1.0.0.js"></script>
  <script src="js/libs/ember-1.1.2.js"></script>
  <script src="js/app.js"></script>
 
</body>
</html>

Step 5 : Adding Twitter Bootstrap

We will use twitter bootstrap to style the application. Download the latest twitter bootstrap package from the official website, and copy the bootstrap.css to css folder and fonts folder to the getbookmarks folder.

Next we will add the bootstrap.css stylesheet to the index.html, and use fixed navigation bar on the top of the page.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>GetBookMarks -- Share your favorite links online</title>
  <link rel="stylesheet" href="css/normalize.css">
  <link rel="stylesheet" type="text/css" href="css/bootstrap.css">
  <link rel="stylesheet" href="css/style.css">
  <script src="http://localhost:9090/livereload.js"></script>
</head>
<body>
 
  <script type="text/x-handlebars">
    <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="#">GetBookMarks</a>
        </div>
 
 
      </div>
    </nav>
    <div id="main" class="container">
      {{outlet}}
    </div>
  </script>
 
  <script type="text/x-handlebars" data-template-name="index">
 
  </script>
 
  <script src="js/libs/jquery-1.9.1.js"></script>
  <script src="js/libs/handlebars-1.0.0.js"></script>
  <script src="js/libs/ember-1.1.2.js"></script>
  <script src="js/app.js"></script>
 
</body>
</html>

In the html shown above, the <script type="text/x-handlebars"> represents our application template. The application template has {{outlet}} tag that holds all other templates and will change depending on the url.

Also add following css to css/style.css. This will add a top padding of 40px for body. This is required to properly render the body with fixed top navigation bar.

body{
    padding-top: 40px;
}

Step 5 : Submit New Story

We will start by implementing the submit new story functionality. In Ember, it is recommended that you think in terms of URL(s). When a user views the '#/story/new' url, then a form should be displayed to the user.

Add a new route in App.Router.Map for '#/story/new' as shown below:

App.Router.map(function() {
  this.resource('newstory' , {path : 'story/new'});
});

Next we will add a 'newstory' template in index.html to render a form.

<script type="text/x-handlebars" id="newstory">
    <form class="form-horizontal" role="form">
      <div class="form-group">
        <label for="title" class="col-sm-2 control-label">Title</label>
        <div class="col-sm-10">
          <input type="title" class="form-control" id="title" name="title" placeholder="Title of the link" required>
        </div>
      </div>
      <div class="form-group">
        <label for="excerpt" class="col-sm-2 control-label">Excerpt</label>
        <div class="col-sm-10">
          <textarea class="form-control" id="excerpt" name="excerpt" placeholder="Short description of the link" required></textarea>
        </div>
      </div>
 
      <div class="form-group">
        <label for="url" class="col-sm-2 control-label">Url</label>
        <div class="col-sm-10">
          <input type="url" class="form-control" id="url" name="url" placeholder="Url of the link" required>
        </div>
      </div>
      <div class="form-group">
        <label for="tags" class="col-sm-2 control-label">Tags</label>
        <div class="col-sm-10">
          <textarea id="tags" class="form-control" name="tags" placeholder="Comma seperated list of tags" rows="3" required></textarea>
        </div>
      </div>
      <div class="form-group">
        <label for="fullname" class="col-sm-2 control-label">Full Name</label>
        <div class="col-sm-10">
          <input type="text" class="form-control" id="fullname" name="fullname" placeholder="Enter your Full Name like Shekhar Gulati" required>
        </div>
      </div>
      <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
          <button type="submit" class="btn btn-success" {{action 'save'}}>Submit Story</button>
        </div>
      </div>
  </form>
  </script>

To see the form just go to '#/story/new'.

Next we will add a link in the navbar to make it easy for us to navigate to story submission form. Replace the nav element with the one mentioned below.

<nav class="navbar navbar-default navbar-fixed-top navbar-inverse" role="navigation">
      <div class="container">
        <div class="navbar-header">
          <a class="navbar-brand" href="#">GetBookMarks</a>
 
        </div>
        <ul class="nav navbar-nav pull-right">
            <li>{{#link-to 'newstory'}}<span class="glyphicon glyphicon-plus"></span> Submit Story{{/link-to}}</li>
        </ul>
 
      </div>
    </nav>

The important thing to notice in the above snippet is the use of {{#link-to}}helper. The {{#link-to}} helper is used to create a link to a route. Please refer to documentation for more information.

Now that we can view the form, let us add the functionality to store the story in HTML 5 Local Storage. To add local storage support, we will have to first download the Ember Data and Local Storage Adapter JavaScript files. Place these files in js/libs folder. Next, add these script tags to the index.html.

  <script src="js/libs/jquery-1.9.1.js"></script>
  <script src="js/libs/handlebars-1.0.0.js"></script>
  <script src="js/libs/ember-1.1.2.js"></script>
  <script src="js/libs/ember-data.js"></script>
  <script src="js/libs/localstorage_adapter.js"></script>
  <script src="js/app.js"></script>

As discussed before, Ember Data is the client side ORM implementation which make it easy to perform CRUD operations on the underlying store. Here, we are going to use the LSAdapter(Local Storage Adapter). Add the following line to app.js.

App.ApplicationAdapter = DS.LSAdapter.extend({
  namespace: 'stories'
});

Next let use define our model. A story would have a url, title, fullname of the user who submitted the story, excerpt of the story, and submittedOn i.e. date on which story was submitted. We can also specify the type of the property. In the model shown below, we have used string and date types. The default adapter supports attribute types of string, number, boolean, and date.

App.Story = DS.Model.extend({
    url : DS.attr('string'),
    tags : DS.attr('string'),
    fullname : DS.attr('string'),
    title : DS.attr('string'),
    excerpt : DS.attr('string'),
    submittedOn : DS.attr('date')
 
});

Next we will write the NewstoryController which will persist the user when save action is made.

App.NewstoryController = Ember.ObjectController.extend({
 
 actions :{
    save : function(){
        var url = $('#url').val();
        var tags = $('#tags').val();
        var fullname = $('#fullname').val();
        var title = $('#title').val();
        var excerpt = $('#excerpt').val();
        var submittedOn = new Date();
        var store = this.get('store');
        var story = store.createRecord('story',{
            url : url,
            tags : tags,
            fullname : fullname,
            title : title,
            excerpt : excerpt,
            submittedOn : submittedOn
        });
        story.save();
        this.transitionToRoute('index');
    }
 }
});

The code shown above gets all the form values and then create an in-memory record using the store API. To store that record in the localstorage we will have to call save method on the Story object. Finally, we will redirect the user to index route.

Now you can test the application, create new story and then go to Chrome Developer tools and in the resources section you can view the story.

Step 6 : Show All Stories

The next logical step is to show all the stories when user view the home page.

As I mentioned before, a route is responsible for querying the model. We will add the IndexRoute which will find all the stories persisted in the local storage.

App.IndexRoute = Ember.Route.extend({
    model : function(){
        var stories = this.get('store').findAll('story');
        return stories;
    }
});

Every route backs a template. In this IndexRoute backs the index template. So add index template in index.html

  <script type="text/x-handlebars" id="index">
    <div class="row">
      <div class="col-md-4">
        <table class='table'>
          <thead>
            <tr><th>Recent Stories</th></tr>
          </thead>
          {{#each controller}}
            <tr><td>
 
              {{title}}
 
            </td></tr>
 
          {{/each}}
        </table>
      </div>
      <div class="col-md-8">
        {{outlet}}
      </div>
    </div>
  </script>

If we visit your app at the URL '/' , we will see list of stories. The each loop iterates over the stories collection; here, controller equals IndexController.

Now you will see the stories on the '/' route.

There is one issue that stories are not sorted by date. To sort them by submittedOn date, we will create IndexController which will be responsible for sorting the model. We specified that we want to sort on submittedOn property and it should in descending order to make sure new stories are on top.

App.IndexController = Ember.ArrayController.extend({
    sortProperties : ['submittedOn'],
    sortAscending : false
});

After making this change, we will see stories sorted by submittedOn property.

Step 7 : Viewing the individual story

The last functionality that we have to implement in this application is that when a user click on a story then user should see all the details related to the story. To achieve that, we will add the following route.

App.Router.map(function() {
    this.resource('index',{path : '/'},function(){
        this.resource('story', { path:'/stories/:story_id' });
    });
 
    this.resource('newstory' , {path : 'story/new'});
 
});

The code shown above is an example of nested route.

The :story_id part is called a dynamic segment because the corresponding story id will be injected into the URL.

Next we will add StoryRoute which will find the story corresponding to the story id.

App.StoryRoute = Ember.Route.extend({
    model : function(params){
        var store = this.get('store');
        return store.find('story',params.story_id);
    }
});

Lastly, we will update the index.html to link to each story from the index template. We will use the {{#link-to}} block helper inside the each loop.

<script type="text/x-handlebars" id="index">
    <div class="row">
      <div class="col-md-4">
        <table class='table'>
          <thead>
            <tr><th>Recent Stories</th></tr>
          </thead>
          {{#each controller}}
            <tr><td>
            {{#link-to 'story' this}}
              {{title}}
            {{/link-to}}
            </td></tr>
 
          {{/each}}
        </table>
      </div>
      <div class="col-md-8">
        {{outlet}}
      </div>
    </div>
  </script>
 
  <script type="text/x-handlebars" id="story">
    <h1>{{title}}</h1>
    <h2> by {{fullname}} <small class="muted">{{submittedOn}}</small></h2>
    {{#each tagnames}}
 
        <span class="label label-primary">{{this}}</span>
 
      {{/each}}
    <hr>
    <p class="lead">
      {{excerpt}}
    </p>
 
  </script>

Once you have made the changes, you can view the changes in the app by visiting your browser.

Step 8 : Formatting the submittedOn date

Ember has concept of helpers. The helpers are functions that can be invoked from any Handlebars template.

We will format the date using moment.js library. Add the following to index.html.

<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.4.0/moment.min.js"></script>

Then, we’ll define our first helper which will format the date to human readable format.

Ember.Handlebars.helper('format-date', function(date){
    return moment(date).fromNow();
});

Finally we will add format-date helper in the story template as shown below.

<script type="text/x-handlebars" id="story">
    <h1>{{title}}</h1>
    <h2> by {{fullname}} <small class="muted">{{format-date submittedOn}}</small></h2>
    {{#each tagnames}}
 
        <span class="label label-primary">{{this}}</span>
 
      {{/each}}
    <hr>
    <p class="lead">
      {{excerpt}}
    </p>
 
  </script>

You can view the updated story

That's it for today. Keep giving feedback.

What's Next

Awesome, I used ember and component at http://pitchas-kelonye.rhcloud.com

Hi Shekhar, i'm learning ember at the moment and did find this tutorial which gave me quite a kickstart especially on authorization. apart from that your NewstoryController's save method is backbone style and completely fails the purpose of ember's controller layer. ember's ObjecController proxies properties back and forth from your model to the template. in order to leverage that functionality use ember's handlebars helpers {{input value=nameOfBoundProperty ...}} (omit the quotation marks) instead of usual html tags and then retrieve the input's value from within your save method via this.get('nameOfBoundProperty'). your save method rewritten:

save : function(){
        var self = this;
        var submittedOn = new Date();
        var store = this.get('store');
        var story = store.createRecord('story', this.getProperties('url', 'tags', 'fullname', 'title', ...));
        story.save().then(function(){
            self.transitionToRoute('index');
        });
    }