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/’:
- food.hbs
- tab.hbs
- table.hbs
- tables.hbs
- /tables/index.hbs
ii. Purge exiting code
Clear the content of these files:
- /assets/javascripts/app/app.coffee
- /assets/javascripts/app/models.coffee
- /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
)
App.Router.map ->
@resource 'colours'
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
)
App.Router.map ->
@resource 'colours'
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
)
App.Router.map ->
@resource 'colours'
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
App.ApplicationSerializer = DS.JSONSerializer.extend
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))
App.ColourAdapter = DS.SailsRESTAdapter.extend
host: 'http://localhost:1337'
namespace: ''
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
)
App.Router.map ->
@resource 'colours'
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
)
App.Router.map ->
@resource 'colours'
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
App.ColoursController = Ember.Controller.extend
isLoadingItem: false
palette: [
{ name:'Red', symbol:'STOP' },
{ name:'Yellow', symbol:'GET SET' },
{ name:'Green', symbol:'GO' },
{ name:'Blue', symbol:'INFO' }
]
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
)
App.Router.map ->
@resource 'colours'
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
App.ColoursController = Ember.Controller.extend
isLoadingItem: false
palette: [
{ name:'Red', symbol:'STOP' },
{ name:'Yellow', symbol:'GET SET' },
{ name:'Green', symbol:'GO' },
{ name:'Blue', symbol:'INFO' }
]
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')
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
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
)
App.Router.map ->
@resource 'colours'
$.fn.decorate = ->
$('ul.list-group').children("li").css('color', 'black')
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
App.ColoursView = Ember.View.extend
didInsertElement: ->
@$().decorate()
App.ColoursController = Ember.Controller.extend
isLoadingItem: false
palette: [
{ name:'Red', symbol:'STOP' },
{ name:'Yellow', symbol:'GET SET' },
{ name:'Green', symbol:'GO' },
{ name:'Blue', symbol:'INFO' }
]
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
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
)
App.Router.map ->
@resource 'colours'
$.fn.decorate = ->
$('ul.list-group').children("li").css('color', 'black')
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
App.ColoursView = Ember.View.extend
didInsertElement: ->
@$().decorate()
App.ColoursController = Ember.Controller.extend
isLoadingItem: false
isDeletingItem: false
presentId: null
presentColour: null
palette: [
{ name:'Red', symbol:'STOP' },
{ name:'Yellow', symbol:'GET SET' },
{ name:'Green', symbol:'GO' },
{ name:'Blue', symbol:'INFO' }
]
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')
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:
References