Building the Taskify Ember app

Let's get back to Ember development to build our SPA. Follow these steps. We will occasionally refer to previous sections of this chapter, and detail the specifics here.

Setting up Taskify as an Ember CLI project

Let's generate the project and set up all the artifacts. Follow these steps:

  1. Create a new Ember project using Ember CLI from the command line:
    ember new taskify
    
  2. Install broccoli-merge-trees and broccoli-static-compiler for a richer Broccoli configuration. Issue the following commands from the command line:
    npm install --save-dev broccoli-merge-trees
    npm install --save-dev broccoli-static-compiler
    
  3. Install Bootstrap with Bower from the command line:
    bower install bootstrap
    
  4. Configure Broccoli to include bootstrap.js, CSS, and fonts in the ember-cli-build.js file:
      var mergeTrees = require('broccoli-merge-trees');
      var pickFiles = require('broccoli-static-compiler');
      var extraAssets = pickFiles('bower_components/bootstrap/dist/fonts',{ srcDir: '/', files: ['**/*'], destDir: '/fonts' });
    
      app.import('bower_components/bootstrap/dist/css/bootstrap.css');
      app.import('bower_components/bootstrap/dist/js/bootstrap.js');
    
      return mergeTrees([app.toTree(), extraAssets]);
  5. In the application, we will be using a third-party Ember add-on called ember-bootstrap-datetimepicker. Let's install it into the project:
    ember install ember-bootstrap-datetimepicker
    
  6. Build npm and bower dependencies:
    npm install
    bower install
    
  7. Start the Ember server using the ember serve command, and make sure your application is accessible at http://localhost:4200/.
  8. Set the POD directory inside /config/environment.js:
      var ENV = {
        modulePrefix: 'ember-webapp-forspring',
        podModulePrefix: 'ember-webapp-forspring/modules',
        ...
      }

Now we can start generating the required Ember artifacts in this POD directory.

Setting up Ember Data

We need two models: User and Task. Let's generate them first with the following code. For models, we do not use POD:

ember generate model user
ember generate model task

Find the generated models under the /app/models/ folder. Open them and set the attributes and relationships:

User.js

import DS from 'ember-data';

export default DS.Model.extend({
  name: DS.attr('string'),
  userName: DS.attr('string'),
  password: DS.attr('string'),
  dateOfBirth: DS.attr('date')
});

Task.js

import DS from 'ember-data';

export default DS.Model.extend({
  name: DS.attr('string'),
  priority: DS.attr('number'),
  status: DS.attr('string'),
  createdBy: DS.belongsTo('user'),
  createdDate: DS.attr('date'),
  assignee: DS.belongsTo('user'),
  completedDate: DS.attr('date'),
  comments: DS.attr('string'),
});

Let's generate an (Ember Data) application adapter that has some global properties common to all adapters:

ember generate adapter application

Open the generated /app/adapters/application.js file, and add two attributes, host and namespace, with the right values as shown in the following code. After this, adapters for all models will take these attributes unless overridden individually:

import Ember from 'ember';
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  host: 'http://<apiserver-context-root>',
  namespace: 'api/v1'
});

We need to override the default serializers, as Ember Data expects the ID of the dependent objects for sideloading, where the API server sends out nested objects embedded within. So, generate both serializers from the command line and then update the content appropriately:

ember generate serializer user
ember generate serializer task

Update the generated /app/serializers/user.js file with the following content:

import DS from 'ember-data';

export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
    attrs: {
        profileImage: {embedded: 'always'},
    },
});

Update the generated /app/serializers/task.js file with the following content:

import DS from 'ember-data';

export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
    attrs: {
        createdBy: {embedded: 'always'},
        assignee: {embedded: 'always'},
    },
});

Configuring application routes

Routes represent application states. They need to be registered with the router of the application in order to enable navigation. Our application has three primary routes: index, user, and task. Let's generate them in the pod directory. Do it from the command line:

ember generate route index --pod
ember generate route user --pod
ember generate route task --pod

Take a look at router.js now; you will see these new routes registered there. Also, the route.js and template.hbs files generated for each of these under the POD directories will be present.

Building the home screen

Now, let's set up the index template to show counts for the total number of tasks and the number of open tasks in the system. Open the /app/modules/index/template.js file and update it with this content:

<div class="container">
  <h1>Welcome to Taskify!</h1>
  <hr />
  <P>There are <strong>{{model.openTasks.length}}</strong> open
    {{#link-to "task"}}tasks{{/link-to}} out of total
    <strong>{{model.tasks.length}}</strong> in the system</P>
</div>

The preceding template binds the model attributes using Handlebars and expects the model to be loaded with proper data. Let's go build the model in the route.js file:

import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    var _model = Ember.Object.extend({
      tasks: null,
      openTasks: Ember.computed("tasks", function() {
        var _tasks = this.get("tasks");
        return Ember.isEmpty(_tasks) ? Ember.A([]): _tasks.filterBy("status", "Open");
      }),
    }).create();

    this.store.findAll('task').then(function(_tasks) {
    _model.set("tasks", _tasks);
    return _model;
  });
    return _model;
});

In the preceding code, the model hook first loads data from the server using DS.Store (Ember Data), constructs the model object with attributes, including computed properties, and then returns. The home screen will look like the following image (ignore the headers for now):

Building the home screen

Building the user screen

Now, let's build the user screen for listing all the users in the system. Let's build the model inside the route's model hook first. Add this method inside /app/modules/user/route.js:

model: function() {
  return this.store.findAll('user');
},

You can see how beautifully Ember and Ember Data work together to simplify such an otherwise complex task of fetching, transforming, and deserializing data into model instances and finally making it available for the consumption of the template and controller, asynchronously, without blocking the screen.

Now let's display this data on a screen. Update the /app/modules/user/template.hbs file with the following content:

<div class="container">
  <h1>List of users</h1><hr />
  <p class="text-right">
    <a {{action 'createNewUser'}} class="btn btn-primary" role="button">Create New User</a></p>

  <table class="table table-hover">
    <thead><tr>
      <th>ID</th>
      <th>User name</th>
      <th>Name</th>
      <th>Date Of Birth</th>
      <th>Edit</th>
      <th>Delete</th>
    </tr></thead>
  <tbody>
  {{#each model as |user|}}
  <tr>
    <td><a {{action 'showUser' user }}>{{user.id}}</a></td>
    <td>{{user.userName}}</td>
    <td>{{user.name}}</td>
    <td>{{format-date user.dateOfBirth format='MMM DD, YYYY'}}</td>
    <td><button type="button" class="btn btn-default" aria-label="Edit user" {{action 'editUser' user}}>
        <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span></button></td>
    <td><button type="button" class="btn btn-default" aria-label="Delete user" {{action 'deleteUser' user}}>
        <span class="glyphicon glyphicon-trash" aria-hidden="true"></span></button></td>
  </tr>
  {{/each}}
  </tbody>
  </table>
</div>

Now you can see the user route at http://localhost:4200/user, which looks like this:

Building the user screen

Building a custom helper

In the template.hbs file, you may notice a custom helper:

{{format-date user.dateOfBirth format='MMM DD, YYYY'}}

Let's go build it; you should have already got an error since this helper hasn't been defined yet. From the command line, generate it using the following command:

ember generate helper format-date

Update the generated /app/helpers/format-date.js file with the following script:

import Ember from 'ember';

export function formatDate(params, hash) {
  if(!Ember.isEmpty(hash.format)) {
    return moment(new Date(params)).format(hash.format);
  }
  return params;
}

export default Ember.Helper.helper(formatDate);

Now look at your browser; the user list should render properly.

Adding action handlers

Inside the /app/modules/user/template.hbs file, there are four action invocations: createNewUser, showUser, editUser, and deleteUser. All these methods accept a user variable as a parameter. Let's add these actions inside /app/modules/user/route.js first:

actions: {
  createNewUser: function() {
    this.controller.set("_editingUser", null);
    this.controller.set("editingUser", Ember.Object.create({
      name: null,
      userName: null,
      password: null,
      dateOfBirth: new Date()
    }));

  Ember.$("#userEditModal").modal("show");
  },
  showUser: function(_user) {
    this.controller.set("_editingUser", _user);
    this.controller.set("editingUser", Ember.Object.create(
    _user.getProperties("id", "name", "userName", "password", "dateOfBirth", "profileImage")));
    Ember.$("#userViewModal").modal("show");
  },
  editUser: function(_user) {
    this.actions.closeViewModal.call(this);
    this.controller.set("_editingUser", _user);
    this.controller.set("editingUser", Ember.Object.create(
    _user.getProperties("id", "name", "userName", "password", "dateOfBirth", "profileImage")));
    Ember.$("#userEditModal").modal("show");
  },
  deleteUser: function(_user) {
    if(confirm("Delete User, " + _user.get("name") + " ?")) {
      var _this = this.controller;
      _user.destroyRecord().then(function() {
        _this.set("editingUser", null);
        _this.set("_editingUser", null);
        _this.set("model", _this.store.findAll('user'));
      });
    }
  }
}

Building a custom component – modal window

In the preceding code listing, both the createNewUser and editUser methods use userViewModal using jQuery. This is a Bootstrap modal window built as a custom Ember component. In fact, there are four components working together in a nested fashion: {{modal-window}}, {{modal-header}}, {modal-body}}, and {{modal-footer}}.

Let's generate the artifacts from a commandline first:

ember generate component modal-window --pod
ember generate component modal-header --pod
ember generate component modal-body --pod
ember generate component modal-footer --pod

The component.js and template.hbs files should be generated under the /app/modules/components/<component-name>/ directory. Now let's update the .js and .hbs files to make it a true modal window:

modal-window/template.hbs

<div class="modal-dialog" role="document">
<div class="modal-content">{{yield}}</div>
</div>

modal-window/component.js

import Ember from 'ember';

export default Ember.Component.extend({
  classNames: ["modal", "fade"],
  attributeBindings: ['label:aria-label', 'tabindex', 'labelId:aria-labelledby'], ariaRole: "dialog", tabindex: -1, labelId: Ember.computed('id', function() {
    if(Ember.isEmpty(this.get("id"))) {
      this.set("id", this.get("parentView.elementId") + "_Modal");
    }
  return this.get('id') + "Label";
  })
});

modal-header/template.hbs

{{yield}}

modal-header/component.js

import Ember from 'ember';

export default Ember.Component.extend({
  classNames: ["modal-header"],
});

modal-body/template.hbs

{{yield}}

modal-body/component.js

import Ember from 'ember';

export default Ember.Component.extend({
  classNames: ["modal-body"],
});

modal-footer/template.hbs

{{yield}}

modal-footer/component.js

import Ember from 'ember';

export default Ember.Component.extend({
  classNames: ["modal-footer"],
});

Building userEditModal using {{modal-window}}

The four modal related components have been built; it's time to add userEditModal into the user/template.js file. Add the following code or userEditModal into the user/template.js file:

{{#modal-window id="userEditModal"}}

  {{#modal-header}}
  <button type="button" class="close" {{action "closeEditModal"}} aria-label="Close"><span aria-hidden="true">&times;</span></button>
  <h4 class="modal-title" id=labelId>{{modalTitle}}</h4>
  {{/modal-header}}

  {{#modal-body}}
  <form> <div class="form-group">
  <label for="txtName">Full Name:</label>
  {{input class="form-control" id="txtName" placeholder="Full Name" value=editingUser.name}} </div>
  <div class="form-group"> <label for="txtUserName">Username:</label>
  {{input class="form-control" id="txtUserName" placeholder="User Name" value=editingUser.userName}}</div>
  <div class="form-group"> <label for="txtPassword">Password:</label>
  {{input type="password" class="form-control" id="txtPassword" placeholder="Your secret password" value=editingUser.password}}</div>
  <div class="form-group"><label for="calDob">Date of Birth:</label>
  {{bs-datetimepicker id="calDob" date=editingUser.dateOfBirth
       updateDate=(action (mut editingUser.dateOfBirth))
       forceDateOutput=true}} </div> </form>
  {{/modal-body}}

  {{#modal-footer}}
  <a {{action "saveUser"}} class="btn btn-success">Save</a>
  <a {{action "closeEditModal"}} class="btn btn-primary">Cancel</a>
  <a {{action 'deleteUser' _editingUser}} class="btn btn-danger"> Delete </a>
  {{/modal-footer}}
{{/modal-window}}

The preceding code listing integrates the user edit form with {{modal-body}}, with the form title inside {{modal-header}}, action buttons inside {{modal-footer}}, and all of this inside {{modal-window}} with the ID userEditModal. Just click the Edit button of a user row; you will see this nice modal window pop up in front of you:

Building userEditModal using {{modal-window}}

The Save button of userEditModal invokes the saveUser action method, the Cancel button invokes the closeEditModal action, and the Delete button invokes deleteUser. Let's add them inside the actions hash of user/route.js, next to deleteUser:

...
closeEditModal: function() {
  Ember.$("#userEditModal").modal("hide");
  this.controller.set("editingUser", null);
  this.controller.set("_editingUser", null);
},
closeViewModal: function() {
  Ember.$("#userViewModal").modal("hide");
  this.controller.set("editingUser", null);
  this.controller.set("_editingUser", null);
},

saveUser: function() {
  if(this.controller.get("_editingUser") === null) {
  this.controller.set("_editingUser",this.store.createRecord("user",
    this.controller.get("editingUser").getProperties("id", "name", "userName", "password", "dateOfBirth")));
  } else {
    this.controller.get("_editingUser").setProperties(
         this.controller.get("editingUser").getProperties("name", "userName", "password", "dateOfBirth"));
  }
  this.controller.get("_editingUser").save();
  this.actions.closeEditModal.call(this);
}

Similarly, user/template.js has userViewModal, which just displays the user data in read-only format. Now, you can easily derive it from userEditModal; hence, we're not listing it here.

Building the task screen

The task screen follows the same pattern as the user screen. This section describes only the portions logically different from the user screen and assumes that you will start developing the task screen from the user screen and incorporate the changes described here. Also, you can see the complete code from the project files attached to this chapter of the book.

The task screen has some extra state-specific data besides the model data (the list of tasks). For maintaining that data while the task screen is active, we will create a controller:

ember generate controller task --pod

The relationship between Task and User is that a task is created by a user and assigned to another user. So, on the edit task (or create new task) screen, a list of users should be shown in a selection box so that one can be selected from the list. For that, we need to load the list of users from DS.store to a variable inside the controller. Here is the controller method that loads the user list:

loadUsers: function() {
  this.set("allUsers", this.store.findAll('user'));
}.on("init"),

This method will get fired on initialization of the controller, courtesy of the .on("init") construct. The template code extract that renders the user list in an HTML selection is here:

<div class="form-group">
  <label for="calDob">Created By:</label>
  <select onchange={{action "changeCreatedBy" value="target.value"}} class="form-control">
  {{#each allUsers as |user|}}
    <option value={{user.id}} selected={{eq editingTask.createdBy.id user.id}}>{{user.name}}</option>
  {{/each}}
  </select>
</div>

The action method, changeCreatedBy, is listed here:

changeCreatedBy: function(_userId) {
  this.get("editingTask").set("createdBy", this.get("allUsers").findBy("id", _userId));
},

Similarly, task priorities are also a list of integers from 1 to 10. The code to load them is here (this goes inside the controller):

taskPriorities: [],
  loadTaskPriorities: function() {
  for(var _idx=1; _idx<11; _idx++) {
    this.taskPriorities.pushObject(_idx);
  }
}.on("init"),

Code for the priority selection box is as follows:

<div class="form-group">
  <label for="selectPriority">Priority:</label>
  <select onchange={{action (mut editingTask.priority) value="target.value"}} class="form-control">
  {{#each taskPriorities as |priority|}}
   <option value={{priority}} selected={{eq editingTask.priority priority}}>{{priority}}</option>
  {{/each}}
  </select>
</div>

As a further step, you may add security to both ends of the application. You may personalize tasks for the logged-in user. Ember also supports WebSockets. Tasks can be pushed to the client as they are assigned to the logged-in user by another user somewhere else. For simplicity, those advanced features are not covered in this chapter. However, with the knowledge you have gained in this and the previous chapters, you are already at a comfortable stage to implement end-to-end security and real-time updates using WebSockets inside Taskify.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset