Day 24: Yeoman Ember–The Missing Tutorial

So far this series has looked at:

Today for my 30 day challenge, I decided to learn a productivity tool for front-end development called Yeoman. First we will first cover the Yeoman basics and then develop an Ember application using Yeoman. This blog post does not cover EmberJS basics so please refer to Day 19 where I explored it.

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 following best practices. It is inspired by Ruby on Rails generator concept.

Yeoman consists of three tools:

  1. Yo : a scaffolding tool using 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 : a JavaScript based command line build tool helping developers automate repetitive tasks. It’s a JavaScript alternative to Make or Ant. Grunt performs minification, compilation, unit testing, linting, and more. See Day 5 on GruntJS for more information.

  3. Bower : a package manager for client side technologies. It’s be used to search, install, uninstall web assets like JavaScript, HTML, and CSS. It is not a strict tool and gives developers many options. See Day 1 about Bower for more information.

Why should I care?

The Yeoman website gives compelling reasonse for using it. Check out the whyyeoman website.

Prerequisite

Yeoman requires the following dependencies on your machine.

  1. Node : Yeoman requires NPM. NPM is the node package manager. It comes bundled with Nodejs installation. 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. 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 we are ready for 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. It asks you if you want to use Twitter Bootstrap or not. I usually use bootstrap in all my application so enter 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 such as Ember, Twitter Bootstrap, etc. Bower installs all the dependencies in this folder. The location of this directory is set 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 start the built-in preview server. Grunt server uses the livereload server which I covered in Day 7.

$ 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 looks like this:

   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 generates story_model.js in app/scripts/models directory. Along with model, it generates corresponding views, controllers, and routes. Please refer to day 19 blog if you are not comfortable with these terms.

Update story_model with this:

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 configures 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 this:

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, a form is displayed to the user.

Submit New Story

Let’s add the form which is displayed when user goes to ‘#/story/new’. Update the app/templates/story_edit.hbs with this code:

    <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 the story submission form is at http://localhost:9000/#/story/new

Update the StoryEditController with save a function to save 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

Next we need to implement a function to show a list of stories in the sidebar.

In the application_route.js, we 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 update application.hbs to render the story title with a link. Update application.hbs to look like this:

<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 reloads and reflects the changes.

View Individual Story

The last funtion to implement is a method to view individual stories when users navigates to http://localhost:9000/#/story/:id. The :id corresponds to the story id.

Update story_route.js with this code:

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 this code:

<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

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.

Next Steps

Categories
News
Tags
,
Comments are closed.