Thursday, June 26, 2014

Part 2: Ember.js + Sails.js + PostgreSQL - An end-to-end technology-chain that enables rapid web application development in a RESTful fashion


Welcome document

Goals

This blog-spot is a “how-to” on implementing a client-side Single-Page Application (SPA) that consumes RESTful API services. We build this SPA app based on the existing code base, which was produced in the previous blog-post. In short, we showcase a simple SPA app that carries-out CRUD operations using RESTful API calls like GET, POST, & DELETE.

SPA App: Feature-set to implement

  • F1: Display a ‘Colour-List’ that shows previously selected colours. That is, make a RESTful GET call and list the response colour-dataset. On the server-side, this RESTful GET call will be translated to a database READ operation. Implementation of this feature is covered in stages: 1, 2, 3, 4.
  • F2: Display a ‘Colour-Selection-List’, in-order to manually add colours to the above ‘Colour-List’. That is, make a RESTful POST call that persist a new colour entry. On the server-side, this RESTful POST call will be translated to a database CREATE operation. Implementation of this feature is covered in stage 5.

    Note: ‘Colour-Selection-List’ contains a set of predefined colours

  • F3: Possibility to delete colours from the ‘Colour-List’. That is, make a RESTful DELETE call that destroy a colour entry. On the server-side, this RESTful DELETE call will be translated to a database DELETE operation. Implementation of this feature is covered in stage 6.

Hence, our target is to achieve a SPA app with a Colours-View that looks as follows:

Table of contents

Stage1: Implement colours Route/View

Step1: Clean-up code base

i. Purge demo artifacts

Delete following files/folder under the directory ‘/assets/javascripts/templates/’:
  1. food.hbs
  2. tab.hbs
  3. table.hbs
  4. tables.hbs
  5. /tables/index.hbs

ii. Purge exiting code

Clear the content of these files:
  1. /assets/javascripts/app/app.coffee
  2. /assets/javascripts/app/models.coffee
  3. /assets/javascripts/templates/application.hbs

Step2: Template

i. Create colours template

Create a file ‘/assets/javascripts/templates/colours.hbs’ & copy-&-paste the below markup to this file:
<div class="jumbotron">
  <h1>Colours View</h1>
</div>

ii. Modify application template

Copy-&-paste below markup in the file ‘/assets/javascripts/templates/application.hbs’:
<div class="row">
    <div class="small-12 columns">
        <p>
            {{outlet}}
        </p>
    </div>
</div>

Step3: Add Router/Route

Copy-&-paste below code in the file ‘/assets/javascripts/app/app.coffee’:
window.App = App = Ember.Application.create(
  LOG_TRANSITIONS: true
)

# --- Router -----
App.Router.map ->
  @resource 'colours'

# --- Routes -----
App.IndexRoute = Ember.Route.extend
  redirect: ->
    @.transitionTo('colours')

console.log('reloaded')

Step4: Adhoc testing: Route/View

Run the Mimosa watch server with the below command:
$ mimosa watch -s

Note: The above command is run under the root folder of the SPA project

Now, test the running SPA app by visit the following URL in your favourite web-browser:

http://localhost:3000

You will automatically be re-directed to the following route:

http://localhost:3000/#/colours

Below screenshot shows the view after the above route has been loaded with Ember-Inspector running in Firefox web-browser:

Stage 2: Implement ‘LoadingRoute’ with asynchronous delayed routine

Step1: Template

i. Create loading template

Create a file ‘/assets/javascripts/templates/loading.hbs’ & copy-&-paste the below markup to this file:
<img src="../../img/activity_indicator.gif" height="100" width="100" />

Note: Download the above gif from below URL and save this image under the directory ‘/assets/img/’:
http://f.cl.ly/items/0W0l3d1O1l171Y1p192N/activity%20indicator.gif

ii. Modify application template

Copy-&-paste the below markup in the file ‘/assets/javascripts/templates/application.hbs’:
<div class="row">
    <div class="small-12 columns">
        <p>
          Current route path: "{{currentPath}}"
      </p>
        <p>
            {{outlet}}
        </p>
    </div>
</div>

Step2: Add ‘ColourRoute’ and simulated async delay routine

Copy-&-paste the below code in the file ‘/assets/javascripts/app/app.coffee’:
window.App = App = Ember.Application.create(
  LOG_TRANSITIONS: true
)

# --- Router -----
App.Router.map ->
  @resource 'colours'

# --- Promises -----
delayRoutine_Async = ->
  new Ember.RSVP.Promise((resolve) ->
    Ember.run.later ->
      resolve()
    , 5000 # Simulating 5 seconds delay
  )

# --- Routes -----
App.IndexRoute = Ember.Route.extend
  redirect: ->
    @.transitionTo('colours')

App.ColoursRoute = Ember.Route.extend
  model: delayRoutine_Async

console.log('reloaded')

Step3: Adhoc testing: Route/View

Now, test the running SPA app by visiting the following URL in your favourite web-browser:

http://localhost:3000

Note: It’s presumed, that you are already running the Mimosa watch server in the background.

Below screenshot shows the view when the above route is getting loaded. Also, note that the Ember-Inspector is running in Firefox web-browser:
You will automatically be re-directed after 5seconds delay to the following route:

http://localhost:3000/#/colours

Below screenshot shows the view after the above route has been loaded with Ember-Inspector running in Firefox web-browser:

Stage 3: Implement ‘Colour-List’ view

Step1: Template

i. Modify colours-view

Copy-&-paste the below markup in the file ‘/assets/javascripts/templates/colours.hbs’:
<div class="jumbotron">
  <h1>Colours View</h1>

  <div class="panel-body">
    <ul class="list-group">

      {{#each model}}

        {{#if-equal name "Red"}}
        <li class="list-group-item list-group-item-danger">
          {{name}}
        </li>
        {{/if-equal}}

        {{#if-equal name "Yellow"}}
        <li class="list-group-item list-group-item-warning">
          {{name}}
        </li>
        {{/if-equal}}

        {{#if-equal name "Green"}}
        <li class="list-group-item list-group-item-success">
          {{name}}
        </li>
        {{/if-equal}}

        {{#if-equal name "Blue"}}
        <li class="list-group-item list-group-item-info">
          {{name}}
        </li>
        {{/if-equal}}

      {{/each}}

    </ul>
  </div>
</div>

ii. Handlebars-helper: Add conditional logic

Copy-&-paste the below code in the file ‘/assets/javascripts/app/handlebars-helpers.coffee’:
Ember.Handlebars.registerHelper('if-equal', (a, b, options) ->
  Ember.Handlebars.bind.call(options.contexts[0], a, options, true, (result) ->
    result == b)
)

Step2: Hardcode a colours JSON dataset

Copy-&-paste the below code in the file ‘/assets/javascripts/app/app.coffee’:
window.App = App = Ember.Application.create(
  LOG_TRANSITIONS: true
)

# --- Router -----
App.Router.map ->
  @resource 'colours'

# --- Promises -----
delayRoutine_Async = ->
  new Ember.RSVP.Promise((resolve) ->
    Ember.run.later ->
      colours_dataset = [
                          {
                            "id": 1,
                            "name": "Red",
                            "symbol": "STOP"
                          },
                          {
                            "id": 2,
                            "name": "Yellow",
                            "symbol": "GET SET"
                          },
                          {
                            "id": 3,
                            "name": "Green",
                            "symbol": "GO"
                          },
                          {
                            "id": 4,
                            "name": "Blue",
                            "symbol": "INFO"
                          }
                        ]

      resolve(colours_dataset)
    , 5000 # Simulating 5 seconds delay
  )

# --- Routes -----
App.IndexRoute = Ember.Route.extend
  redirect: ->
    @.transitionTo('colours')

App.ColoursRoute = Ember.Route.extend
  model: delayRoutine_Async

console.log('reloaded')

Step3: Adhoc testing: Colours-View

Now, test the running SPA app by visiting the following URL in your favourite web-browser:

http://localhost:3000

Note: It’s presumed, that you are already running the Mimosa watch server in the background.

You will automatically be re-directed after 5seconds delay to the following route:

http://localhost:3000/#/colours

Below screenshot shows the view after the above route has been loaded with Ember-Inspector running in Firefox web-browser:

Stage 4: GET colour-dataset from RESTful API

In this section, we will setup the Colour model in association with its REST data-source (i.e. Sails.js RESTful adapter).

Step1: Colour model: Setup REST adapter and model

Copy-&-paste the below code in the file ‘/assets/javascripts/app/models.coffee’:
App = window.App

#-------------------------------------------------------
# Helper: SailsRESTAdapter/Ember-Data specific 
#-------------------------------------------------------
App.ApplicationSerializer = DS.JSONSerializer.extend
  # Fix broken extractArray in Ember-Data
  extractArray: (store, type, arrayPayload) ->
    serializer = @
    Ember.ArrayPolyfills.map.call(arrayPayload, (singlePayload) ->
      serializer.extractSingle(store, type, singlePayload)
    )

  # Fix JSONSerializer to work with Ember-Data's RESTAdapter 
  serializeIntoHash: (hash, type, record, options) ->
    Ember.merge(hash, this.serialize(record, options))

# ------------------------------
# Colour Model
# ------------------------------
# Define Sails REST adapter
App.ColourAdapter = DS.SailsRESTAdapter.extend
  host: 'http://localhost:1337'
  namespace: ''

# Define colour model
App.Colour = DS.Model.extend
  name: DS.attr()
  symbol: DS.attr()

Step2: Write an async integration point

Now, that the colour model & its RESTful adapter have been setup. It’s time to write an integration point i.e. a function that asynchronously obtains data from a RESTful service for a given model, i.e. a RESTful **GET** call. So, copy-&-paste the below code in the file ‘/assets/javascripts/app/app.coffee’:

Note: delayRoutine_Async promise is now renamed to colourGetAll_Async

window.App = App = Ember.Application.create(
  LOG_TRANSITIONS: true
)

# --- Router -----
App.Router.map ->
  @resource 'colours'

# --- Promises -----
colourGetAll_Async = ->
  context = @
  new Ember.RSVP.Promise((resolve) ->
    resolve(context.store.find('colour'))
  )

# --- Routes -----
App.IndexRoute = Ember.Route.extend
  redirect: ->
    @.transitionTo('colours')

App.ColoursRoute = Ember.Route.extend
  model: colourGetAll_Async

console.log('reloaded')

Step3: Adhoc testing: Colours-View - Obtain all colours from REST API & render view

Now, test the Sails.js RESTful API by visiting the following URL in your favourite web-browser:

http://localhost:1337

Note: It’s presumed, that you are already running the Sails.js server in the background

Now, add three colours: *Red*, *Yellow*, & *Red* by entering following URLs:

http://localhost:1337/colours/create?name=Red&symbol=STOP
http://localhost:1337/colours/create?name=Yellow&symbol=GET%20SET
http://localhost:1337/colours/create?name=Red&symbol=STOP

Note: It’s presumed that previously you have not add any colours.

Now, test the running SPA app by visiting the following URL in your favourite web-browser:

http://localhost:3000

Note: It’s presumed, that you are already running the Mimosa watch server in the background.

You will automatically be re-directed after obtaining all colours-dataset from the RESTful API to the following route:

http://localhost:3000/#/colours

Below screenshot shows the view after the above route has been loaded with Ember-Inspector running in Firefox web-browser:
Now, let’s test if we delete a colour on the server-side, will actually be reflected in our Colours-View. So, delete the redundant ‘Red’ colour by entering following URL:

http://localhost:1337/colours/destroy?id=3

Now, reload the following URL in your web-browser:

http://localhost:3000/#/colours

Now, the Colours-View should look as follows:

Stage 5: Add colour on selection and fire CREATE operation on RESTful API

Step1: Add Controller/Properties/Observer to manage predefined colour selections

Copy-&-paste the below code in the file ‘/assets/javascripts/app/app.coffee’:
window.App = App = Ember.Application.create(
  LOG_TRANSITIONS: true
)

# --- Router -----
App.Router.map ->
  @resource 'colours'

# --- Promises -----
colourGetAll_Async = ->
  context = @
  new Ember.RSVP.Promise((resolve) ->
    resolve(context.store.find('colour'))
  )

colourAdd_Async = (store, newColour) ->
  new Ember.RSVP.Promise((resolve) ->
    Ember.run.later ->
      newColour = store.createRecord('colour', newColour)
      resolve(newColour.save('colour').then((params) ->
          console.log(params.get('name') + ' colour added.')
        )
      )
  )

# --- Routes -----
App.IndexRoute = Ember.Route.extend
  redirect: ->
    @.transitionTo('colours')

App.ColoursRoute = Ember.Route.extend
  model: colourGetAll_Async

# --- Controllers -----
App.ColoursController = Ember.Controller.extend
  # -- Controller States ---
  isLoadingItem: false

  # -- Controller's local dataset ---
  palette:  [ 
              { name:'Red', symbol:'STOP' },
              { name:'Yellow', symbol:'GET SET' },
              { name:'Green', symbol:'GO' },
              { name:'Blue', symbol:'INFO' }
            ]

  # -- Observable ---
  selectedColour: null
  selectedColourObserver: ( ->
    if @selectedColour != null
      n = @selectedColour.name
      s = @selectedColour.symbol

      @set('isLoadingItem', true)
      newColourItem = { name: n, symbol: s }
      promise = colourAdd_Async(@store, newColourItem)
      context = @
      promise.then((params) ->
        context.set('isLoadingItem', false)
      )
  ).observes('selectedColour')

console.log('reloaded')

Step2: Template: Add colour selection component

Copy-&-paste the below markup in the file ‘/assets/javascripts/template/colours.coffee’:
<div class="jumbotron">
  <h1>Colours View</h1>

  <div class="panel-heading">
      {{view Ember.Select
        contentBinding="palette"
        optionValuePath="content"
        valueBinding="selectedColour"
        optionLabelPath="content.name"
        prompt="Please select a colour to added to the list"
        action="selectedAction"
        disabled=isLoadingItem
      }}

    <p>
      {{#if isLoadingItem}}
        {{render 'loading'}}
      {{/if}}
    </p>
  </div>    

  <div class="panel-body">
    <ul class="list-group">

      {{#each model}}

        {{#if-equal name "Red"}}
        <li class="list-group-item list-group-item-danger">
          {{name}}
        </li>
        {{/if-equal}}

        {{#if-equal name "Yellow"}}
        <li class="list-group-item list-group-item-warning">
          {{name}}
        </li>
        {{/if-equal}}

        {{#if-equal name "Green"}}
        <li class="list-group-item list-group-item-success">
          {{name}}
        </li>
        {{/if-equal}}

        {{#if-equal name "Blue"}}
        <li class="list-group-item list-group-item-info">
          {{name}}
        </li>
        {{/if-equal}}

      {{/each}}

    </ul>
  </div>
</div>

Step3: Adhoc testing: Colours-View - Add colour from a predefined colour selections

Now, test the running SPA app by visiting the following URL in your favourite web-browser:

http://localhost:3000

Note: It’s presumed, that you are already running the Mimosa watch server in the background.

You will automatically be re-directed after obtaining the all colours-dataset from the RESTful API to the following route:

http://localhost:3000/#/colours

Below screenshot shows the view after the above route has been loaded with Ember-Inspector running in Firefox web-browser:
Now, let’s test if we are able to add a colour. So, select ‘Green’ colour within the drop-down selection item. As a result of this user action, the Colours-View should look as follows:

Stage 6: Delete colour and fire DELETE operation on RESTful API

Step1: Add colour deletion action and an async promise to fire DELETE operation on RESTful API

Copy-&-paste the below code in the file ‘/assets/javascripts/app/app.coffee’:
window.App = App = Ember.Application.create(
  LOG_TRANSITIONS: true
)

# --- Router -----
App.Router.map ->
  @resource 'colours'

# --- Promises -----
colourGetAll_Async = ->
  context = @
  new Ember.RSVP.Promise((resolve) ->
    resolve(context.store.find('colour'))
  )

colourAdd_Async = (store, newColour) ->
  new Ember.RSVP.Promise((resolve) ->
    Ember.run.later ->
      newColour = store.createRecord('colour', newColour)
      resolve(newColour.save('colour').then((params) ->
          console.log(params.get('name') + ' colour added.')
        )
      )
  )

colourDelete_Async = (store, colour) ->
  new Ember.RSVP.Promise( (resolve) ->
    Ember.run.later ->
      resolve(store.find('colour', colour).then( (colour2delete) ->
          colour2delete.destroyRecord().then( (deletedColour) ->
            console.log(deletedColour.get('name')+" colour has been deleted!")
          )
        )
      )
  )

# --- Routes -----
App.IndexRoute = Ember.Route.extend
  redirect: ->
    @.transitionTo('colours')

App.ColoursRoute = Ember.Route.extend
  model: colourGetAll_Async

# --- Controllers -----
App.ColoursController = Ember.Controller.extend
  # -- Controller States ---
  isLoadingItem: false

  # -- Controller's local dataset ---
  palette:  [ 
              { name:'Red', symbol:'STOP' },
              { name:'Yellow', symbol:'GET SET' },
              { name:'Green', symbol:'GO' },
              { name:'Blue', symbol:'INFO' }
            ]

  # -- Observable ---
  selectedColour: null
  selectedColourObserver: ( ->
    if @selectedColour != null
      n = @selectedColour.name
      s = @selectedColour.symbol

      @set('isLoadingItem', true)
      newColourItem = { name: n, symbol: s }
      promise = colourAdd_Async(@store, newColourItem)
      context = @
      promise.then((params) ->
        context.set('isLoadingItem', false)
      )
  ).observes('selectedColour')

  # -- User Actions ---
  actions:
    deleteColour: (colour) ->
      console.log "Delete Button Clicked!"
      @set('isLoadingItem', true)
      promise = colourDelete_Async(@store, colour)
      context = @
      promise.then( ->
        context.set('isLoadingItem', false)
      )

console.log('reloaded')

Step2: Template: Add delete button to every colour item

Copy-&-paste the below markup in the file ‘/assets/javascripts/template/colours.coffee’:
<div class="jumbotron">
  <h1>Colours View</h1>

  <div class="panel-heading">
      {{view Ember.Select
        contentBinding="palette"
        optionValuePath="content"
        valueBinding="selectedColour"
        optionLabelPath="content.name"
        prompt="Please select a colour to added to the list"
        action="selectedAction"
        disabled=isLoadingItem
      }}

    <p>
      {{#if isLoadingItem}}
        {{render 'loading'}}
      {{/if}}
    </p>
  </div>    

  <div class="panel-body">
    <ul class="list-group">

      {{#each model}}

        {{#if-equal name "Red"}}
        <li class="list-group-item list-group-item-danger">
          {{name}}
          <form class="navbar-right">
            <button type="button" class="btn btn-danger btn-xs"
                    {{action 'deleteColour' id}} {{bind-attr disabled='controller.isLoadingItem'}}>
              Delete
            </button>
          </form>
        </li>
        {{/if-equal}}

        {{#if-equal name "Yellow"}}
        <li class="list-group-item list-group-item-warning">
          {{name}}
          <form class="navbar-right">
            <button type="button" class="btn btn-danger btn-xs"
                    {{action 'deleteColour' id}} {{bind-attr disabled='controller.isLoadingItem'}}>
              Delete
            </button>
          </form>
        </li>
        {{/if-equal}}

        {{#if-equal name "Green"}}
        <li class="list-group-item list-group-item-success">
          {{name}}
          <form class="navbar-right">
            <button type="button" class="btn btn-danger btn-xs"
                    {{action 'deleteColour' id}} {{bind-attr disabled='controller.isLoadingItem'}}>
              Delete
            </button>
          </form>
        </li>
        {{/if-equal}}

        {{#if-equal name "Blue"}}
        <li class="list-group-item list-group-item-info">
          {{name}}
          <form class="navbar-right">
            <button type="button" class="btn btn-danger btn-xs"
                    {{action 'deleteColour' id}} {{bind-attr disabled='controller.isLoadingItem'}}>
              Delete
            </button>
          </form>
        </li>
        {{/if-equal}}

      {{/each}}

    </ul>
  </div>
</div>

Step3: Adhoc testing: Colours-View - Colour deletion

Now, test the running SPA app by visiting the following URL in your favourite web-browser:

http://localhost:3000

Note: It’s presumed, that you are already running the Mimosa watch server in the background.

You will automatically be re-directed after obtaining the all colours-dataset from the RESTful API to the following route:

http://localhost:3000/#/colours

Below screenshot shows the view after the above route has been loaded with Ember-Inspector running in Firefox web-browser:
Now, let’s test if we are able to delete a colour. So, click the ‘Green’ colour’s delete button. As a result of this user action, the Colours-View should look as follows:

Additional cosmetic features

i. Font-colour formating: An use-case for ‘Ember View’

Here, we will set colour item’s text-font foreground-colour to black. The below implementation uses the Ember hook *didInsertElement* within Ember View to manipulate jQuery. As a result of this view implementation following is manifest:
  • On initial colours-list loading, the text font will be black
  • On addition of a new colour, the font colour will be also set to black

Copy-&-paste the below code in the file ‘/assets/javascripts/app/app.coffee’:

window.App = App = Ember.Application.create(
  LOG_TRANSITIONS: true
)

# --- Router -----
App.Router.map ->
  @resource 'colours'


# --- Helpers -----
$.fn.decorate = ->
  $('ul.list-group').children("li").css('color', 'black')


# --- Promises -----
colourGetAll_Async = ->
  context = @
  new Ember.RSVP.Promise((resolve) ->
    resolve(context.store.find('colour'))
  )

colourAdd_Async = (store, newColour) ->
  new Ember.RSVP.Promise((resolve) ->
    Ember.run.later ->
      newColour = store.createRecord('colour', newColour)
      resolve(newColour.save('colour').then((params) ->
          console.log(params.get('name') + ' colour added.')
        )
      )
  )

colourDelete_Async = (store, colour) ->
  new Ember.RSVP.Promise( (resolve) ->
    Ember.run.later ->
      resolve(store.find('colour', colour).then( (colour2delete) ->
          colour2delete.destroyRecord().then( (deletedColour) ->
            console.log(deletedColour.get('name')+" colour has been deleted!")
          )
        )
      )
  )


# --- Routes -----
App.IndexRoute = Ember.Route.extend
  redirect: ->
    @.transitionTo('colours')

App.ColoursRoute = Ember.Route.extend
  model: colourGetAll_Async


# --- Views -----
App.ColoursView = Ember.View.extend
  didInsertElement: ->
    @$().decorate()


# --- Controllers -----
App.ColoursController = Ember.Controller.extend
  # -- Controller States ---
  isLoadingItem: false

  # -- Controller's local dataset ---
  palette:  [ 
              { name:'Red', symbol:'STOP' },
              { name:'Yellow', symbol:'GET SET' },
              { name:'Green', symbol:'GO' },
              { name:'Blue', symbol:'INFO' }
            ]

  # -- Observable ---
  main_Context = @
  selectedColour: null
  selectedColourObserver: ( ->
    if @selectedColour != null
      n = @selectedColour.name
      s = @selectedColour.symbol

      @set('isLoadingItem', true)
      newColourItem = { name: n, symbol: s }
      promise = colourAdd_Async(@store, newColourItem)
      local_Context = @
      promise.then((params) ->
        local_Context.set('isLoadingItem', false)
        main_Context.$().decorate()
      )
  ).observes('selectedColour')

  # -- User Actions ---
  actions:
    deleteColour: (colour) ->
      console.log "Delete Button Clicked!"
      @set('isLoadingItem', true)
      promise = colourDelete_Async(@store, colour)
      context = @
      promise.then( ->
        context.set('isLoadingItem', false)
      )

console.log('reloaded')

ii. Showing confirmation modal-dialog on deletion: An use-case for ‘Ember Component’

Here we implement a modal-dialog box, which will be shown on attempting to delete a colour item i.e. an implemenation of a confirmation dialog.

This modal-dailog is an Ember Component composed of Handelbars-templates and Colours-Controller that handles the business-logic required by confirmation actions. Also, note that the confirmation button within the modal-dialog box i.e. the Delete Button, now shows a loading image on attempting to delete the record asynchronously over RESTful API.

As a result, the Colours-View looks as below on attempting to delete a blue colour:

Templates

Copy-&-paste the below markup to file ‘/assets/javascripts/templates/modal.hbs’:

{{#modal-dialog action="closeDialog"}}
  <h2 class="flush--top">
    Colour to delete:
  </h2>

  <h3>
    {{presentColour}} 
    <small>
      id: {{presentId}}
    </small>
  </h3>

  <button type="button" class="btn btn-success btn-xs"
        {{action "deleteColour"}}>

  {{#if isDeletingItem}}
      Delete
      <img src="../../img/spiffygif_18x18.gif">
    {{else}}
        Delete
    {{/if}}

  </button>

  <button type="button" class="btn btn-danger btn-xs"
        {{action "closeDialog"}}>
    Cancel
  </button>

{{/modal-dialog}}

Copy-&-paste the below markup to file ‘/assets/javascripts/templates/components/modal-dialog.hbs’:

<div class="overlay" {{action "closeDialog"}}> 
</div>
<div class="delete_modal">
  {{yield}}
</div>

Copy-&-paste the below markup to file ‘/assets/javascripts/templates/colours.hbs’:

<div class="jumbotron">
  <h1>Colours View</h1>

  {{outlet modal}}

  <div class="panel-heading">
      {{view Ember.Select
        contentBinding="palette"
        optionValuePath="content"
        valueBinding="selectedColour"
        optionLabelPath="content.name"
        prompt="Please select a colour to added to the list"
        action="selectedAction"
        disabled=isLoadingItem
      }}

    <p>
      {{#if isLoadingItem}}
        {{render 'loading'}}
      {{/if}}
    </p>
  </div>

  <div class="panel-body">
    <ul class="list-group">

      {{#each model}}

        {{#if-equal name "Red"}}
        <li class="list-group-item list-group-item-danger">
          {{name}}
          <form class="navbar-right">
            <button type="button" class="btn btn-danger btn-xs"
                    {{action 'openDialog' 'modal' id name}} {{bind-attr disabled='controller.isLoadingItem'}}>
              Delete
            </button>
          </form>
        </li>
        {{/if-equal}}

        {{#if-equal name "Yellow"}}
        <li class="list-group-item list-group-item-warning">
          {{name}}
          <form class="navbar-right">
            <button type="button" class="btn btn-danger btn-xs"
                    {{action 'openDialog' 'modal' id name}} {{bind-attr disabled='controller.isLoadingItem'}}>
              Delete
            </button>
          </form>
        </li>
        {{/if-equal}}

        {{#if-equal name "Green"}}
        <li class="list-group-item list-group-item-success">
          {{name}}
          <form class="navbar-right">
            <button type="button" class="btn btn-danger btn-xs"
                    {{action 'openDialog' 'modal' id name}} {{bind-attr disabled='controller.isLoadingItem'}}>
              Delete
            </button>
          </form>
        </li>
        {{/if-equal}}

        {{#if-equal name "Blue"}}
        <li class="list-group-item list-group-item-info">
          {{name}}
          <form class="navbar-right">
            <button type="button" class="btn btn-danger btn-xs"
                    {{action 'openDialog' 'modal' id name}} {{bind-attr disabled='controller.isLoadingItem'}}>
              Delete
            </button>
          </form>
        </li>
        {{/if-equal}}

      {{/each}}

    </ul>
  </div>
</div>

CSS

Append the below markup to the end-of-the-file ‘/assets/stylesheets/style.styl’:

...
.delete_modal
  position relative
  margin 10px auto
  width 377px
  background-color #fff
  padding 1em

.overlay
  height 100%
  width 100%
  position fixed
  top 0
  left 0
  background-color rgba(0, 0, 0, 0.2)

.flush--top
  margin-top 0
...

Image file

Download an image file & save it under ‘/assets/img/spiffygif_18x18.gif’, this image is a loading animation used inside the Delete button:

http://i639.photobucket.com/albums/uu116/pksjce/spiffygif_18x18.gif

Application code

Copy-&-paste the below code to file ‘/assets/javascripts/app/app.coffee’:

window.App = App = Ember.Application.create(
  LOG_TRANSITIONS: true
)

# --- Router -----
App.Router.map ->
  @resource 'colours'


# --- Helpers -----
$.fn.decorate = ->
  $('ul.list-group').children("li").css('color', 'black')


# --- Promises -----
colourGetAll_Async = ->
  context = @
  new Ember.RSVP.Promise((resolve) ->
    resolve(context.store.find('colour'))
  )

colourAdd_Async = (store, newColour) ->
  new Ember.RSVP.Promise((resolve) ->
    Ember.run.later ->
      newColour = store.createRecord('colour', newColour)
      resolve(newColour.save('colour').then((params) ->
          console.log(params.get('name') + ' colour added.')
        )
      )
  )

colourDelete_Async = (store, colour_id) ->
  new Ember.RSVP.Promise((resolve) ->
    Ember.run.later ->
      resolve(store.find('colour', colour_id).then((colour2delete) ->
          colour2delete.destroyRecord().then((deletedColour) ->
            console.log(deletedColour.get('name')+" colour has been deleted!")
          )
        )
      )
  )


# --- Routes -----
App.ApplicationRoute = Ember.Route.extend
  actions:
    openModal: (modalName)->
      @render(modalName, {
        into: 'colours',
        outlet: 'modal',
        controller: 'colours'
      })

    closeModal: ->
      @disconnectOutlet({
        outlet: 'modal',
        parentView: 'colours'
      })

App.IndexRoute = Ember.Route.extend
  redirect: ->
    @.transitionTo('colours')

App.ColoursRoute = Ember.Route.extend
  model: colourGetAll_Async


# --- Views -----
App.ColoursView = Ember.View.extend
  didInsertElement: ->
    @$().decorate()


# --- Controllers -----
App.ColoursController = Ember.Controller.extend
  # -- Controller States ---
  isLoadingItem: false
  isDeletingItem: false
  presentId: null
  presentColour: null

  # -- Controller's local dataset ---
  palette:  [ 
              { name:'Red', symbol:'STOP' },
              { name:'Yellow', symbol:'GET SET' },
              { name:'Green', symbol:'GO' },
              { name:'Blue', symbol:'INFO' }
            ]

  # -- Observable ---
  main_Context = @
  selectedColour: null
  selectedColourObserver: ( ->
    if @selectedColour != null
      n = @selectedColour.name
      s = @selectedColour.symbol

      @set('isLoadingItem', true)
      newColourItem = { name: n, symbol: s }
      promise = colourAdd_Async(@store, newColourItem)
      local_Context = @
      promise.then((params) ->
        local_Context.set('isLoadingItem', false)
        main_Context.$().decorate()
      )
  ).observes('selectedColour')

  # -- User Actions ---
  actions:
    deleteColour: ->
      @set('isDeletingItem', true)
      promise = colourDelete_Async(@store, @presentId)
      context = @
      promise.then( ->
        context.set('isDeletingItem', false)
        context.send('closeModal')
      )

    openDialog: (modalName, id, colour) ->
      @presentId = id
      @presentColour = colour
      @send('openModal', modalName)

    closeDialog: ->
      @send('closeModal')


# --- Components -----
App.ModalDialogComponent = Ember.Component.extend
  actions:
    closeDialog: ->
      @sendAction()

console.log('reloaded')

Source Code

You can view both the projects source code in its entirety, here’s the Bitbucket repository:

Note: The commit history is in direct concert with how this blog-post builds the SPA app, here’s the direct link to commits. With help of this commit history you will be able to see the diff between every iteration.

Conclusion

Until now we have gone through the implementation details of a simple SPA app that carries-out CRUD operations using GET, POST, & DELETE calls on RESTful API. Which means, you have gotten a taste on how to stack below technologies and seen how easy it is to incrementally add feature-set in-order to produce a data-centric web-app:

  • Client-side SPA framework: Ember.js
  • Server-side RESTful API framework: Sails.js

References

1 comment:

  1. Wow, it is great to see more and more tutorials on Sails + Ember. If you are interested, I created a Starter Kit, to get started quickly with a production ready environment: https://github.com/artificialio/sane
    Our Vagrant Box currently does not have PostgreSQL support but we are planning on adding it at some point. Let me know what you think.

    ReplyDelete