Day 24: Yeoman Ember--The Missing Tutorial

So far in this series we have looked at Bower, AngularJS, GruntJS, PhoneGap, Meteor, Ember, and TimelineJS JavaScript technologies. Today for my 30 day challenge, I decided to learn a productivity tool for front-end development called Yeoman. In this blog post, we will first cover the Yeoman basics and then we will develop an Ember application using Yeoman. This blog post will not cover EmberJS basics so please refer to my day 19 blog for more information.

Yeoman logo

What is Yeoman?

Yeoman is an open source productivity tool for client side development. It is a collection of tools and frameworks intended to help developers quickly build high quality web applications which follows best practices. It is inspired by Ruby on Rails generator concept. Yeoman consists of three tools :

  1. Yo : It is a scaffolding tool which uses generators to scaffold everything you need to start a new project. Yo helps avoid boilerplate code. It helps you start a new project and configure grunt tasks.

  2. Grunt : It is a JavaScript based command line build tool that can help developers automate repetitive tasks. You can think of it as a JavaScript alternative to Make or Ant. It can perform tasks like minification, compilation, unit testing, linting, etc. Read by day 5 blog on GruntJS for more information.

  3. Bower : It is a package manager for client side technologies. It can be used to search , install, uninstall web assets like JavaScript, HTML, and CSS. It is not an opinionated tool and leaves lot of choice to the developers who are using the technology. Read my day 1 blog on Bower for more information.

Why should I care?

If you want to convince yourself why you should learn Yeoman then read the whyyeoman section on Yeoman website.

Prerequisite

In order to install yeoman you need to have the following installed on your machine.

  1. Node : Yeoman requires NPM. NPM is the node package manager. It comes bundled with Nodejs installation. So, please download the latest version of node.js from http://nodejs.org.

  2. Git : You need to have git installed on your machine as some packages fetch code from git repositories. So, install git for your operating system.

Install Yeoman Ember Generator

Yeoman depends on generators to scaffold the web applications. There are generators for all modern JavaScript MV* frameworks. In this blog, we will use Ember generator. NPM is used to install the generators. This command will also install Yeoman.

$ npm install -g generator-ember

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. This is the same application which we developed on day 19 so please refer to the blog to better understand the application usecase.

Github Repository

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

Create Ember Application

Now that we have covered all the basics let us start with the application development.

Create a new directory at any convenient location on the file system for the application and change directory to it.

$ mkdir getbookmarks
$ cd getbookmarks

Next run the yo ember command and it will ask you wether you want to use Twitter Bootstrap or not. I normally use bootstrap in all my application so I entered Yes.

$ yo ember
 
     _-----_
    |       |
    |--(o)--|   .--------------------------.
   ---------  |    Welcome to Yeoman,    |
    ( __ )   |   ladies and gentlemen!  |
    /___A___\   '__________________________'
     |  ~  |
   __'.___.'__
 
 
[?] Would you like to include Twitter Bootstrap for Sass? Yes

After pressing Yes, Yeoman will scaffold an Ember application and automatically install its required dependencies by running bower install and npm install.

Let us now look at the Ember application generated by Yeoman. The application has three top-level directories: app, node_modules, and test. Then there are configuration files -- .bowerrc, .gitignore, .jshintrc, Gruntfile.js, and package.json. The app structure is shown below.

Yeoman Ember App Structure

All the application specific code is in the app directory. This application structure follows Ember best practices.

Yeoman Ember App Structure

  1. The bower_components directory houses all the client side dependencies like Ember , Twitter Bootstrap, etc. Bower installs all the dependencies in this folder. The location of this directory can be changed in .bowerrc file.

  2. The images directory is for any application specific image.Yeoman optimizes all the images in image directory.

  3. The index.html file contains all the ember.js dependencies in the correct order, all the bootstrap dependencies, and the 'build' comments used by Gruntfile.js to replace (or remove) references to non-optimized scripts or stylesheets within HTML files.

  4. The scripts directory contains all the Ember application controller, views, models , and routes.

  5. The styles directory has application specific css file. The css file imports bootstrap styles.

  6. The templates directory contains application handlebar templates.

Now, we can start up the built-in preview server by running. The grunt server uses livereload server which I discussed in day 7 blog.

$ grunt server

This will open up the application in the default system web browser.

Yeoman Ember App

Generate Story Model

The GetBookmarks application we developed in day 19 blog has one Ember Model called Story. A Yeoman subgenerator can be used to generate smaller pieces of that project like model. To generate Story model, execute the following model.

$ yo ember:model Story

The output of the command will look like as shown below.

   create app/scripts/models/story_model.js
   invoke   ember:controller:/usr/local/lib/node_modules/generator-ember/model/index.js
   create     app/scripts/controllers/stories_controller.js
   create     app/scripts/controllers/story_edit_controller.js
   create     app/scripts/routes/stories_route.js
   create     app/scripts/routes/story_route.js
   create     app/scripts/routes/story_edit_route.js
   invoke       ember:view:/usr/local/lib/node_modules/generator-ember/controller/index.js
   create         app/scripts/views/story_view.js
   create         app/scripts/views/story_edit_view.js
   create         app/scripts/views/stories_view.js
   create         app/templates/story.hbs
   create         app/templates/story_edit.hbs
   create         app/templates/stories.hbs
   create         app/scripts/views/bound_text_field_view.js
   invoke       ember:router:/usr/local/lib/node_modules/generator-ember/controller/index.js
 conflict         app/scripts/router.js
[?] Overwrite app/scripts/router.js? overwrite
    force         app/scripts/router.js

This will generate story_model.js in app/scripts/models directory. Along with model, it will also generate corresponding views, controllers, and routes. Please refer to day 19 blog if you are not comfortable with these terms.

Update story_model with the one shown below.

Emberapp.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')
});

Please restart the Grunt server for changes to take effect.

Install Ember LocalStorage Adapter

We will use HTML 5 LocalStorage to store the data. Install the adapter using bower.

$ bower install --save ember-localstorage-adapter

Then update the index.html with the dependency.

<script src="bower_components/ember-localstorage-adapter/localstorage_adapter.js"></script>

Also update the app/scripts/store.js with the code shown below. This will configure the application to use LSAdapter(Local Storage Adapter) instead of FixtureAdapter.

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

Update Routes

Replace the router.js code with the one shown below.

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

In the code shown above, we have defined three routes.

  1. The index route corresponds to the root url.

  2. To view the individual story, we are using story route.

  3. To create a new story we are using story_edit route. When a user views the '#/story/new' url, then a form should be displayed to the user.

Submit New Story

Now let us first add the form which will be displayed when user goes to '#/story/new'. Update the app/templates/story_edit.hbs with the code shown below.

    <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>

Now if you go to http://localhost:9000/#/story/new you will see the story submission form.

Update the StoryEditController with save function to persist the story in local storage.

Getbookmarks.StoryEditController = Ember.ObjectController.extend({
 
  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');
        console.log('Store .. '+store);
        var story = store.createRecord('story',{
            url : url,
            tags : tags,
            fullname : fullname,
            title : title,
            excerpt : excerpt,
            submittedOn : submittedOn
        });
    story.save();
    this.transitionToRoute('index');
  }
});

List All Stories

The next functionality that we have to implement is to show list of stories on the sidebar.

In the application_route.js, we will fetch all the stories from the local storage.

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

Next we will update the application.hbs to render the story title with a link. Update the application.hbs with the one shown below.

<div>
    <nav class="navbar navbar-default navbar-fixed-top" role="navigation">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">GetBookmarks</a>
        </div>
        <div class="collapse navbar-collapse navbar-ex1-collapse">
            <ul class="nav navbar-nav pull-right">
                <li>{{#link-to 'story_edit'}}<span class="glyphicon glyphicon-plus"></span> Submit Story{{/link-to}}</li>
            </ul>
        </div>
    </nav>
    <div class="container" id="main">
        <div class="row">
            <div>
                <div class="col-md-3">
                    <div class="well sidebar-nav">
 
                        <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>
                <div class="col-md-9">
                    {{outlet}}
                </div>
            </div>
        </div>
    </div>
</div>

The application user interface will reload with the changes.

View Individual Story

The last functionality is to view the individual story when user navigates to http://localhost:9000/#/story/:id. The :id corresponds to the story id. Update the story_route.js with the code shown below.

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

Update the app/templates/story.hbs with the code shown below.

<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>

Build For Production

Finally, we will run the grunt build command to produce a distributable application. The grunt build command takes the source code files under app directory and turns them into a distributable application under dist directory.

$ grunt build

That's it for today. Keep giving feedback.

What's Next

I am not sure, i guess you are not so much into python / django and maybe this is a bad idea for your series.

But i really would love to see a tutorial for installing django, and i dont mean one of these "git clone - done" tutorials.

I really would love to learn how to install a python 3 cartridge and the newest django version running on it the "wsgi-way". So step by step to learn the openshift possibilites to customize.

Thanks in advance.

Great Tutorial and a very good initiative from you. Keep them coming.

I think in the "Install Yeoman" section, last sentence says : "To install bower type the following command" i believe you meant Yeoman.

Very well done to you. Cheers.

FYI, yeoman has been deprecated....use "yo" instead...

https://github.com/yeoman/yeoman/wiki/Migrate-from-0.9.6-to-1.0

If I want to use node/express and yeoman as the server for this generator-ember app - what's the best route (no pun intended) to go about this?

I ask this as a bit of a yeoman nube and as an experienced ruby/rails developer looking to leverage node as a backend api server for an express app: - should we be running client and server in the same grunt command? - what, if any, are the best practices for this?

Thanks in advance for any clarification...