Another dive into the wonderful world of web technologies.
The requirement was simple: I needed an editable, pageable, grid backed by a REST-based resource.
This is what we are aiming at:
Being supremely lazy (as all developers should be) I wanted the simplest possible solution.
I am looking at AngularJS + Ng-grid + Bootstrap directives + Coffeescript for the front-end of the application, coupled with Ratpack + Groovy on the back-end. Plus, there's a smattering of
Gradle for building and running the application, along with GVM and Lazybones.
Buzzwords galore!
Here's the zipped-up project for you. I know, I know…for maximum cool points, I really should use Github.
There's a lot to get through.
"Begin at the beginning," the King said, very gravely, "and go on till you come to the end: then stop."
- Lewis Carroll, Alice in Wonderland
Sage words!
HTML
The beginning for any web application is surely the HTML page. Here it is (lightly edited):
<!doctype html>
<html lang="en" ng-app="rest">
<head>
...
</head>
<body>
<div class="container">
<h1>${model.title}</h1>
<p>Brings all these components together to make a CRUD-dy grid…</p>
<div ng-controller="ServantListCtrl">
<div class="outer">
<div class="gridStyle" ng-grid="gridOptions"></div>
<div class="inner">
<div class="left">Total Items: {{totalItems}}</div>
<div ng-if="errorMessage" class="right animate-if error">{{errorMessage}}</div>
</div>
</div>
<pagination items-per-page="itemsPerPage" total-items="totalItems" ng-model="currentPage"
ng-change="getPagedDataAsync()" class="pagination-sm paginationOveride"
boundary-links="true"></pagination>
</div>
</div>
<script type="text/ng-template" id="modalFormFields.tmpl">
<div class="form-group">
<label for="name" class="control-label col-xs-2">Name</label>
<div class="col-xs-10">
<input type="text" name="name" id="name" placeholder="Feline name" ng-model="row.name" class="form-control"/>
</div>
</div>
<div class="form-group">
<label for="name" class="control-label col-xs-2">Age</label>
<div class="col-xs-10">
<input type="text" name="age" id="age" placeholder="Age" ng-model="row.age" class="form-control"/>
</div>
</div>
<div class="form-group">
<label for="name" class="control-label col-xs-2">Dead</label>
<div class="col-xs-10">
<input type="text" name="dead" id="dead" placeholder="Dead" ng-model="row.deceased" class="form-control"/>
</div>
</div>
<div class="form-group">
<label for="name" class="control-label col-xs-2">Description</label>
<div class="col-xs-10">
<input type="text" name="description" id="description" placeholder="Description" ng-model="row.description"
class="form-control"/>
</div>
</div>
</script>
<script type="text/ng-template" id="modalEditCreateForm.tmpl">
<div class="modal-header">
<h3 class="modal-title">Servant</h3>
</div>
<div class="modal-body">
<form class="form-horizontal" role="form">
<legend ng-switch on="modalMode">
<span ng-switch-when="create">Create</span>
<span ng-switch-default>Edit</span>
</legend>
<div class="form-group" ng-if="modalMode != 'create'">
<label for="name" class="control-label col-xs-2">ID</label>
<p class="form-control-static col-xs-10">{{row.id}}</p>
</div>
<!--
Use a second template for the form fields. Why? Because!
Note single quotes: http://lostechies.com/gabrielschenker/2013/12/28/angularjspart-6-templates/
-->
<div ng-include="'modalFormFields.tmpl'"/>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok()">OK</button>
<button class="btn btn-warning" ng-click="cancel()">Cancel</button>
</div>
</script>
<script type="text/ng-template" id="modalRmForm.tmpl">
<div class="modal-header">
<h3 class="modal-title">Remove</h3>
</div>
<div class="modal-body">
Remove record for item {{row.id}}? Cannot be undone.
</div>
<div class="modal-footer">
<button class="btn btn-primary" ng-click="ok()">OK</button>
<button class="btn btn-warning" ng-click="cancel()">Cancel</button>
</div>
</script>
<script src="..."></script>
</body>
</html>
There's quite a lot going on here and I'm not going to go through all this line by line but here are some highlights for you to watch out for:
- the main container DIV is pretty standard bootstrap-ese, even though bootstrap is being brought to you courtesy of the angular-ui bootstrap module.
- lots of Angular bits
- ng-app directive
- ng-controller directive
- data binding to/from the controller's scoped data with {{}}
- ng-click
- ng-model ng-if/ng-switch, ng-include
- even though ng-grid has a facility for pagination, I have chosen to do pagination courtesy of the bootstrap module. Why am I making life harder than I must? 'cos the ng-grid stuff looks horrid and is hard to restyle…that's
why.
- the use of the angular-ui ng-grid module for providing a data-bound grid. Ng-grid appears reliable and flexible but seems a bit 'unsophisticated' when compared with likes of Kendo's grid.
- Use of ng-template to extract repeated boilerplate
The end result of all the shenanigans listed above is notably clean HTML. This is surely A Good Thing.
Now let's draw the HTML curtain away, to reveal…
Angular
For a long time, my mantra was: "Thou shalt not Javascript!" These days, I am likely to append "…too much" to the exhortation. Still, old habits die hard and for this application I have implemented all the requisite
functionality in Coffeescript: (IMHO) a much nicer language for doing anything other than "hello, world!."
First off is the definition of the Angular application 'rest.' This is the entry point that provisions the whole application and dependencies, and is initiated by the value of the ng-app attribute in the outer html tag:
app = angular.module('rest', ['ui.bootstrap', 'ngGrid', 'rest.controllers'])
.config ($locationProvider) ->
.html5Mode(true)
Aside from expressing the module depdendency list, pretty much all this does is configure Angular to use HTML5-style 'pretty' URLs if it needs to generate or parse URLs. See Pretty URLs in AngularJS for more.
The real 'meat' of the application is to be found in the controller created within the 'rest.controllers' module. Viz:
controllers = angular.module 'rest.controllers', ['ngResource']
controllers.controller 'ServantListCtrl', ($scope, $modal, $resource, $http, $log) ->
resource = $resource('api/felines/:id', {}, {'update': { method:'PUT' }})
resource.fetchCount = () ->
$http({method: 'GET', url: 'api/felines/count'})
.success((data) -> $scope.totalItems = data.count)
.error((_, status) -> setErrMsg("Get/Count ERROR: #{status}"))
$scope.errorMessage = undefined
clearErrMsg = -> $scope.errorMessage = ""
setErrMsg = (m) -> $scope.errorMessage = m
$scope.selectedRow = []
$scope.felines = []
$scope.totalItems = 0
$scope.currentPage = 1
$scope.itemsPerPage = 5
$scope.getPagedDataAsync = ->
setTimeout(
-> resource.query(
{offset: ($scope.itemsPerPage * ($scope.currentPage - 1)),
max: $scope.itemsPerPage, sort: "id", order: "asc"}
(value) ->
$scope.felines = value
resource.fetchCount()
(httpResponse) -> setErrMsg("Query ERROR: #{httpResponse.statusText}")
)
10
)
cellTmpl = '''
<div class="ngCellText ng-scope col0 colt0" ng-class="col.colIndex()">
<span ng-cell-text="" class="ng-binding">
<button type="button" class="btn btn-default btn-xs" ng-click="rm(row.entity)">
<span class="glyphicon glyphicon-minus"></span>
</button>
<button type="button" class="btn btn-default btn-xs" ng-click="edit(row.entity)">
<span class="glyphicon glyphicon-pencil"></span>
</button>
</span>
</div>
'''
col0HeaderCellTemplate = '''
<div class="ngHeaderSortColumn ngCellText {{col.headerClass}}"">
<span ng-cell-text="" class="ng-binding">
<div>
<button type="button" class="btn btn-default btn-xs" ng-click="create()">
<span class="glyphicon glyphicon-plus"></span>
</button>
</div>
</span>
</div>
'''
$scope.gridOptions = {
totalServerItems: 'totalServerItems'
data: 'felines'
columnDefs: [
{field:'', displayName: '', width: '64px', sortable: false, enableCellEdit: false,
resizable: false, cellTemplate: cellTmpl, headerCellTemplate: col0HeaderCellTemplate},
{field: 'id', displayName: 'ID', width: "**", resizable: false},
{field: 'name', displayName: 'Name', width: "***", resizable: false}
{field: 'age', displayName: 'Age', width: "*", resizable: false}
{field: 'deceased', displayName: 'Dead', width: "*", resizable: false}
{field: 'description', displayName: 'Description', width: "**********"}
]
selectedItems: $scope.selectedRow
enableSorting: false
multiSelect: false
showFooter: false
}
$scope.getPagedDataAsync()
$scope.$on('ngGridEventData', -> $scope.gridOptions.selectRow(0, true))
$scope.create = ->
clearErrMsg()
modalInstance = $modal.open({
templateUrl: 'modalEditCreateForm.tmpl',
controller: ModalCreateEditCtrl,
resolve: {
modalMode: -> 'create'
row: ->
{
name: ""
age: 0
deceased: false
description: ""
}
}
})
modalInstance.result.then((e) ->
resource.save e,
(-> $scope.getPagedDataAsync()),
((httpResponse) -> setErrMsg("Save ERROR: #{httpResponse.statusText}"))
)
$scope.rm = (e) ->
clearErrMsg()
modalInstance = $modal.open({
templateUrl: 'modalRmForm.tmpl',
controller: ModalRmCtrl,
resolve: { row: -> e }
})
modalInstance.result.then((e) ->
e.$remove {id: e.id},
(-> $scope.getPagedDataAsync()),
((httpResponse) -> setErrMsg("Remove ERROR: #{httpResponse.statusText}"))
)
$scope.edit = (e) ->
clearErrMsg()
modalInstance = $modal.open({
templateUrl: 'modalEditCreateForm.tmpl',
controller: ModalCreateEditCtrl,
resolve: {
modalMode: -> 'edit'
row: -> angular.copy(e) # allows for 'cancel'
}
})
modalInstance.result.then((e) ->
oldId = e.id
delete e[x] for x in ['class', 'felines', 'servant', 'id']
e.$update {id: oldId},
(-> $scope.getPagedDataAsync()),
((httpResponse) -> setErrMsg("Edit ERROR: #{httpResponse.statusText}"))
)
ModalRmCtrl = ($scope, $modalInstance, row) ->
$scope.row = row
$scope.ok = -> $modalInstance.close(row)
$scope.cancel = -> $modalInstance.dismiss('cancel')
ModalCreateEditCtrl = ($scope, $modalInstance, row, modalMode) ->
$scope.row = row
$scope.modalMode = modalMode
$scope.ok = -> $modalInstance.close(row)
$scope.cancel = -> $modalInstance.dismiss('cancel')
The main points of interest in the above include:
- the $scope.gridOptions object configures the ng-grid module. It's worth contrasting the way that ng-grid handles the need for cell/header templates to the way that the bootstrap module approaches templating. I really hope that
ng-grid adopts this same approach in the future.
- the use of $scope to tie data into the controller, not the global scope
- the use use of Angular's $resource to support a 'pure' restful interacation, and $http for plain
HTTP GET
- the use of the angular bootstrap module's modal dialog, with associated controllers and HTML templates
Asynchronous processing style is used as much as possible, so that I can be as trendy as I can be to keep the UI as 'live' as possible .
This means that success/fail callbacks are often seen in resource handling. Consider using a $resource for example:
e.$update {id: oldId},
(-> $scope.getPagedDataAsync()),
((httpResponse) -> setErrMsg("Edit ERROR: #{httpResponse.statusText}"))
It's also worth looking at how $http's promise-based API is dealt with in fetchCount().
A promise-style asynchronous approach is also seen with respect to modal dialog handling:
modalInstance = $modal.open({...})
modalInstance.result.then((e) -> ...)
$resource has a strange quirk. Although designed explicitly for REST-ful intereactions, it does not support the use of PUT for resource updates "out of the box." Heaven knows why! The fix is easy and given in the official documentation;
you can see it applied on the very first line of the controller.
As far as I can see (and I am the first to admit that I am not omniscient), Angular has an architectural "blind spot." Consider the following:
$scope.edit = (e) ->
clearErrMsg()
modalInstance = $modal.open({
...
})
modalInstance.result.then((e) ->
...
e.$update ...
(-> ...),
((httpResponse) -> setErrMsg("Edit ERROR: #{httpResponse.statusText}"))
)
That clearErrMsg/setErrMsg pairing is seen all through this application…not very DRY.
I'd love to be able to centralise this processing, and I could IF $resource (or $http) allowed a PRE-invocation callback to be specified but here's the blind spot: there are various post-facto success/fail callbacks, but no
"let's get going" one.
The web is replete with solutions (LMGTFY) for the superficially similar task of hiding/showing a "please wait" spinner but that's not sufficient for what is needed here.
It's a much simpler task, for one thing. A spinner has no need to access a controller's $scope or the request parameters at invocation time, or the call's success/failure status…my desire to provide success/failure UI feedback requires all
these things.
At first blush, $httpProvider.interceptors looks promising, but once again, interceptors don't get access to a given controller's $scope and all the other Good Stuff.
And now I know why 99.99999% of all examples out there on the Interwebs don't show any error handling: 'cos it's too darned difficult to do correctly, and project deadlines have to be met, and… (and what could
possibly go wrong, anyway :-))
Onwards and upwards, what? To the server-side we go!
Ratpack
Keep It Simple, Stupid! In my desire to follow this mantra, I chose to build my server-side RESTful API using Ratpack and Groovy. Ratpack is:
"a simple, capable, toolkit for creating high performance web applications."
How simple? Take a look:
import rest.*
import static ratpack.groovy.Groovy.groovyTemplate
import ratpack.jackson.JacksonModule
import static ratpack.jackson.Jackson.json
import static ratpack.groovy.Groovy.ratpack
import com.google.inject.AbstractModule
import static com.google.inject.Scopes.SINGLETON
ratpack {
bindings {
add new JacksonModule()
add new AbstractModule() {
@Override
protected void configure() {
bind(FelineStore).in(SINGLETON)
}
}
// a few fixtures
init { FelineStore felineStore ->
felineStore.add(new Feline(id: 0, name: "Scotty", age: 5,
description: "Active young(ish) male", deceased: Boolean.FALSE))
felineStore.add(new Feline(id: 1, name: "Furball", age: 5,
description: "Fluffy!", deceased: Boolean.TRUE))
felineStore.add(new Feline(id: 2, name: "Blackie", age: 6,
description: "Black and very affectionate!", deceased: Boolean.FALSE))
felineStore.add(new Feline(id: 3, name: "Midnight", age: 4,
description: "Shy male!", deceased: Boolean.FALSE))
felineStore.add(new Feline(id: 4, name: "Julius", age: 6,
description: "Can clearly say 'Hello!", deceased: Boolean.FALSE))
felineStore.add(new Feline(id: 5, name: "Meow Meow", age: 10,
description: "Getting on a bit", deceased: Boolean.FALSE))
String.metaClass.safeParseAsLong = {
try {
delegate as Long
}
catch (e) {
null
}
}
}
}
handlers { FelineStore datastore ->
get("api/felines/count") {
blocking {
datastore.size()
}
.then {
render json(count: it)
}
}
handler("api/felines/:id?") {
def id = pathTokens.id?.safeParseAsLong()
byMethod {
get {
blocking {
id ? datastore.get(id) : datastore.list(request.queryParams)
}
.then {
if (it != null)
render json(it)
else {
clientError(404)
}
}
}
post {
blocking {
def f = parse Feline
datastore.add(f)
}
.then {
render json(it)
}
}
delete {
blocking {
id ? datastore.delete(id) : null
}
.then {
clientError(it ? 204 : 404)
}
}
put {
blocking {
def f = parse Feline
f.id = id
f.id ? datastore.update(f) : null
}
.then {
clientError(it ? 204 : 404)
}
}
}
}
get {
render groovyTemplate("grid.html", title: "AngularJS + Ng-grid + Bootstrap + Ratpack REST")
}
assets "public"
}
}
It should be pretty clear what's going on here. There are a few nice points of interest:
- JSON processing is much simplified, thanks to Ratpack's Jackson module
- the REST API is clearly and cleanly enunciated in the code
- handling of other request paths is also pretty clear
- asynchronous processing is quite unceremonious
- the use of safeParseAsLong
- dependency injection makes life easy; for proof, look at how the FelineStore is instantiated, configured and injected
A point to note regarding the various handlers…in the worlds of Luke Daly, Ratpack's creator:
Order is crucially important, and this is very much intentional.
It is interesting to note that Ratpack is not purely a Groovy technology. It is being built from the ground up to also
take advantage of all the goodness available in Java 7 and 8. This should make it attractive to a very wide audience. Of course,
Ratpack's Groovy DSL is still nicer to use than the pure Java API…
Be aware that I am deliberately ignoring the FelineStore here…it's in the project but it's only a silly little facade over a list.
Tying all this together is…
Gradle
Gradle is doing the neccessary dependency management, building and launching. As with most simple use-cases, the build.gradle is very clear:
import org.gradle.plugins.javascript.coffeescript.CoffeeScriptCompile
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.ratpack:ratpack-gradle:0.9.5"
}
}
apply plugin: 'coffeescript-base'
apply plugin: "ratpack-groovy"
apply plugin: "idea"
apply plugin: "eclipse"
repositories {
jcenter()
maven {
url "http://repo.springsource.org/repo" // for springloaded
}
maven {
url 'http://repo.gradle.org/gradle/javascript-public'// for coffeescript
}
}
dependencies {
// SpringLoaded enables runtime hot reloading.
// It is not part of the app runtime and is not shipped in the distribution.
springloaded "org.springsource.loaded:springloaded:1.1.5.RELEASE"
compile ratpack.dependency("jackson")
testCompile "org.spockframework:spock-core:0.7-groovy-2.0"
}
task compileCoffee(type: CoffeeScriptCompile) {
source fileTree('src/main/coffee')
destinationDir file('src/ratpack/public/app/js')
}
processResources {
from compileCoffee
}
It's worth looking at how coffeescript precompilation is handled, as well as how springloaded is used. Apart from this, all is boilerplate.
The Rest
A few other noteworthy bits and pieces: I used gvm to handle installs and lazybones to create the intial ratpack project.
But Wait! There's More!
Ratpack is cool and fairly clean, but one can arguably do better: Grails to the rescue!
Here's the Grails restful controller in its entirety:
package rest
import catsrest.Feline
import grails.rest.RestfulController
class FelinesRestController extends RestfulController<Feline> {
static responseFormats = ['json', 'xml']
FelinesRestController() {
super(Feline)
}
def count() {
respond([count: Feline.count()])
}
}
This corresponds pretty much completely to the Ratpack application shown earlier. It will happily service the same Angular application.
To work effectively, the Grails version requires a few URL Mappings to be created, thusly:
"/api/felines/count"(controller: "felinesRest", action: 'count', method: 'GET')
"/api/felines"(resources: "felinesRest")
And that's really about all there is to it. Excellent stuff!
Of course, there is (much) more to Ratpack than is shown in this posting, so don't feel that I am being dismissive of it…that's not my intention at all!
And now I'll let you into a little secret: I originally developed the Angular stuff using the Grails backend shown here, then decided to "keep on playing" with a Ratpack-based alternative implementation…there's always something
more to learn, lurking just around the next corner!
Tags: AngularJS, Coffeescript, Gradle, Grails, Groovy, Javascript, Programming, Ratpack