Grails, AngularJS, CoffeeScript and more... Craming Cool Stuff Into a Grails Application – Grails Resources plugin – CoffeeScript



Grails, AngularJS, CoffeeScript and more... Craming Cool Stuff Into a Grails Application – Grails Resources plugin – CoffeeScript

0 0


slides_GrailsWithAngularAndCoffeeScript

Slides for a talk I did for the SF Groovy Users Group at Standford.

On Github claymccoy / slides_GrailsWithAngularAndCoffeeScript

Grails, AngularJS, CoffeeScript and more...

Craming Cool Stuff Into a Grails Application

Created by Clay McCoy

Who is Clay McCoy?

  • Engineering Tools @ Netflix
  • Asgard - AWS deployment
    • on Github
    • written in Grails
  • from Alabama with 3 kids

Setting Expectations

  • Not a Javascript MVC expert
  • Not an AngularJS expert
  • Not claiming the best approach
  • Biased, Grails FTW!

Why AngularJS

  • Written by testability guru
  • Reimagines JS development
  • Good documentation
  • Great tutorial

Motivation

  • Wanted JS to feel more Groovy.
  • AngularJS tutorial with Grails.
  • Then write a real application.

Why not use all the cool toys together?

  • AngularJS
  • Grails 2.1
  • resources plugin
  • CoffeeScript
  • Geb with Spock
  • Jasmine

... and never finish

Grails Resources plugin

Manage client side static resources like images, js, and css files.

Define dependencies between the resources.

/grails-app/conf/ApplicationResources.groovy
modules = {
    app {
        dependsOn 'jquery, angularResource, bootstrap'
        resource url: 'css/app.css'
        resource url: 'js/angular/app/js/angularApp.js'
    }
    bootstrap {
        dependsOn 'jquery'
        resource url: 'js/bootstrap/css/bootstrap.min.css'
        resource url: 'js/bootstrap/js/bootstrap.min.js'
    }
    angular {
        resource id: 'js', url: [dir: 'js/angular/app/lib/angular',
        file: "angular.js"], nominify: true
    }
    angularResource {
        dependsOn 'angular'
        resource id: 'js', url: [dir: 'js/angular/app/lib/angular',
        file: "angular-resource.js"], nominify: true
    }
}
					

Bring them into a page by specifying the top level module.

CoffeeScript

CoffeeScript is a little language that compiles into JavaScript. ...CoffeeScript is an attempt to expose the good parts of JavaScript in a simple way.

CoffeeScript is more like Groovy

  • No semicolons
  • Default arguments
    fill = (container, liquid = "coffee") ->
      "Filling the #{container} with #{liquid}..."
  • String interpolation
  • Multiline Strings
  • Existential Operator
    zip = lottery.drawWinner?().address?.zipcode

CoffeeScript is more like Groovy

  • Splats
    awardMedals = (first, second, others...) ->
      gold   = first
      silver = second
      rest   = others
  • Ranges
    numbers[0..2]

Except for significant white space.

Using CoffeeScript in Grails

...with old version of CoffeeScript Resources plugin to write tests in CoffeeScript.

/grails-app/conf/BuildConfig.groovy
        runtime ":coffeescript-resources:0.2"
//        runtime ":coffeescript-resources:0.3.2"

Using CoffeeScript in Grails

/grails-app/conf/Config.groovy
coffeescript.modules = {
    angularApp {
        String src = 'src/coffee/angular'
        files "${src}/services", "${src}/filters", "${src}/controllers", "${src}/app"
        output 'angular/app/js/angularApp.js'
    }
    angularTests {
        String src = 'src/coffee/angular'
        files "${src}/servicesSpec", "${src}/filtersSpec", "${src}/controllersSpec"
        output 'angular/test/unit/angularTests.js'
    }
}

Testing with Spock

class PublisherSpec extends Specification {
    Publisher publisher = new Publisher()
    Subscriber subscriber = Mock()

    def setup() {
        publisher << subscriber
    }

    def "should send messages to subscribers"() {
        when:
        publisher.send("hello")

        then:
        1 * subscriber.receive("hello")
    }
}
  • MVC
  • Data Binding
  • Templates (less DOM manipulation)
  • Dependency Injection
  • Testable Code

AngularJS Concepts

  • Templates - HTML with angular elements and attributes
  • Controllers - behavior for a page
  • Scope - data model
  • Services - reusable logic
  • Resources - interact with RESTful services
  • Directives - invent new HTML syntax

AngularJS using CoffeeScript

Pretty easy conversion with a few gotchas due to CoffeeScript's lexical scoping.

  • Prefix controllers with an @ to elevate their scope
  • Use of a 'setup' variable to give tests access to objects created in the 'beforeEach' section

AngularJS in Grails

Grails Controller

class PhoneController {
    def phoneService

    // pass through actions are like like AngularJS routes
    // they map a URL to a template and AngularJS controller
    def list() { [jsController: 'PhoneListCtrl'] }
    def show() { [jsController: 'PhoneDetailCtrl'] }

    // end point for restful call
    def query(String id) {
        if (id) {
            render phoneService.getPhone(id)
        } else {
            render phoneService.allPhones()
        }
    }
}

AngularJS Controller

/src/coffee/angular/controllers.coffee
@PhoneListCtrl = ($scope, Grails) ->
  $scope.phones = Grails.getResource($scope).query({action: 'query'})
  $scope.orderProp = 'age'

@PhoneDetailCtrl = ($scope, Grails) ->
  $scope.phone = Grails.getResource($scope).get {action: 'query'}, (phone) ->
    $scope.setMainImage(phone.images[0])
  $scope.setMainImage = (imageUrl) ->
    $scope.mainImageUrl = imageUrl

Templates are GSPs

Grails params become AngularJS scope in main.gsp using ng-init

Grails and AngularJS both manipulate template.

An AngularJS service named Grails.

/src/coffee/angular/services.coffee
angular.module('phonecatServices', ['ngResource']).service 'Grails', ($resource) ->
  getResource: (scope) ->
    $resource "/#{appName}/:controller/:action/:id",
      {controller: scope.controller || '', action: scope.action || '', id: scope.id || ''}

Developer Testing

Grails and AngularJS both have great testing support.

  • Jasmine - Javascript behavior driven framework
  • js-test-driver - actually runs your tests

Angular Unit Testing

describe 'service', ->
  setup = {}

  beforeEach module 'phonecatServices'

  beforeEach inject (_$httpBackend_, $rootScope, Grails) ->
    setup.httpBackend = _$httpBackend_
    setup.scope = $rootScope.$new()
    setup.grails = Grails

  describe 'Grails', ->

    it 'should call url with controller, action, and id', ->
      setup.httpBackend.expectGET('/phonecat/grailsControllerName/grailsActionName/grailsId').respond()
      setup.scope.controller = 'grailsControllerName'
      setup.scope.action = 'grailsActionName'
      setup.scope.id = 'grailsId'
      setup.grails.getResource(setup.scope).get()

    it 'should call url with only controller and action', ->
      setup.httpBackend.expectGET('/phonecat/grailsControllerName/grailsActionName').respond()
      setup.scope.controller = 'grailsControllerName'
      setup.scope.action = 'grailsActionName'
      setup.grails.getResource(setup.scope).get()

    it 'should call url with only controller and specified action', ->
      setup.httpBackend.expectGET('/phonecat/grailsControllerName/alternateGrailsActionName').respond()
      setup.scope.controller = 'grailsControllerName'
      setup.scope.action = 'grailsActionName'
      setup.grails.getResource(setup.scope).get {action: 'alternateGrailsActionName'}

    it 'should call url without controller, action, or id', ->
      setup.httpBackend.expectGET('/phonecat').respond()
      setup.grails.getResource(setup.scope).get()

Angular End-To-End Testing

Grails Functional Testing with Geb

  • Groovy DSL on top of Selenium/WebDriver
  • Slower than AngularJS end-to-end tests
  • I found it more intuitive due to Groovy background
class PhoneDetailSpec extends GebSpecWithDefaultConfig {
    def setup() {
        to PhoneDetailPage, 'nexus-s'
    }

    def 'should display nexus-s page'() {
        expect: phoneName == 'Nexus S'
    }

    def 'should display the first phone image as the main phone image'() {
        expect: mainPhoneImage.endsWith('/phonecat/images/phones/nexus-s.0.jpg')
    }

    def 'should swap main image if a thumbnail image is clicked on'() {
        when: phoneThumb(3).click()
        then: mainPhoneImage.endsWith('/phonecat/images/phones/nexus-s.2.jpg')
        when: phoneThumb(1).click()
        then: mainPhoneImage.endsWith('/phonecat/images/phones/nexus-s.0.jpg')
    }
}

Page Objects

  • Avoid repetition of selectors
  • More robust tests
class PhoneDetailPage extends PhonePage {
    static action = "show"

    static url = "$controller/$action" // This seems to only be needed on pages that are called with an id.

    static content = {
        phoneName { $('h1').text() }
        mainPhoneImage { $('img.phone').@src }
        phoneThumb { int index -> $(".phone-thumbs li:nth-child(${index}) img") }
    }
}

Running Geb Tests

Not as easy as writing them.

  • Using Geb plugin allows you to run tests from the command line.
  • It is possible to run them from your IDE with some hacks.
  • FirefoxDriver worked out of the box, but ChromeDriver is much faster with a minimal amount of setup on your machine.

And now for something not completely contrived.

  • Show project structure
  • Run application.
  • Run unit tests.
  • Run functional tests.

Github and Blog

https://github.com/claymccoy/GrailsWithAngularAndCoffeeScript

http://claymccoy.blogspot.com/2012/09/grails-with-angularjs-and-coffeescript.html
  • Document lessons learned
  • Get feedback
  • Use as a seed for other projects