Grails And Angular

Bridging The Gap

GITHUB Links

http://bit.ly/1DobCC3

What I do

Why
How
What is Next

http://codemash.jackiergleason.com/

Example Application

Codemash.jackiergleason.com

bit.ly/jgleason

What used to matter

What matters Now

Main Issues With our Project

  • Lots of data

  • Real time updating

  • Testable and Agile

What is Grails?

  • JVM based solution for web applications
  • Built on proven technologies like Spring and Groovy
  • Mature and Established in many businesses

So WhY Grails?

  • JAVA!!!!!

  • Large pool of developers
  • Simple and mature conventions
  • Spring Security and other cool tools

Resonable Rates

Source Business Insider

Large Pool of Developers

Source Linked In

Flourishing Community

Source Redmonk |  X: # Github Y: # Stackoverflow

Typical Grails Layout

What is ANGular?

  • Javascript
  • MVC platform for the client
  • Allows for efficient 2-way Data Binding
  • Supported by Google*

Why Angular

  • Smaller Requests == Less Bandwidth == $$
  • Realtime Data-Binding
  • Single Page Paradigm

Angular Application Structure

Grails/Angular Visualized

Large Data Sets

Solutions

  • ReactJS
  • Polymer
  • Wait for Angular 2.0

Example With ReactJS

angular.module('fasterAngular', [])
  .directive('fastRepeat', function(){
      return{
          restrict: 'E',
          scope:{
              data: '='
          },
          link:function(scope, el, attrs){
              scope.$watch('data', function(newValue, oldValue){
                  React.renderComponent(
                      MYLIST({data:newValue}),
                      el[0]
                  );
              })
          }
      }
  })

How Do We Use Grails and AngulAr

   |-grails-app
   |---assets
   |-----images
   |-----javascripts
   |-----stylesheets
   |---conf
   |-----hibernate
   |-----spring
   |---controllers
   |-----org
   |-------gleason
   |---------test
   |---domain
   |---i18n
   |---migrations
   |---services
   |-----org
   |-------gleason
   |---------test
   |---taglib
   |---utils
   |---views
   |-----layouts
   |-----socket
   |-----thefirm
   |-scripts
   |-src
   |---groovy
   |-----org
   |-------gleason
   |---------test
   |-----------command
   |---java
   |-test

Grails Structure

@TestFor(QuestionController)
@Mock([QuestionService])
class NoteControllerSpec extends Specification {
    QuestionService questionService	
    def setup() {
        questionService = Mock()
	controller.questionService = questionService
    }
    def "Test a simple GET request"(){
      	when:
            controller.get()
	then:
            1 * questionService.get(_) >> []
	}
}

Spock test

class QuestionController {
	def questionService
	def index(){
	}
	def get() {
		render questionService.get(params.id) as JSON
	}
	def update() {
		def obj = JSON.parse(request.reader.text);
		Question question = new Question(key:obj.key, text: obj.text, desc: obj.desc, voteCount: obj.voteCount);
		render questionService.update(question) as JSON
	}
	def create(Question q){
		render questionService.create(q) as JSON
	}
	def delete(){
		def text = request.reader.text;
		def slurper = new JsonSlurper();
		def result = slurper.parseText(text)
		render questionService.delete(result.key)
	}
}

Grails Controller

class UrlMappings {
	static mappings = {
		"/Note/$id?"(controller: "question", parseRequest: true) {
			action = [GET: "get", POST: "create", PUT: "update", DELETE: "delete"]
		}
		"/socket"(controller:"socket"){
			action=[GET:"index"]
		}
		"/"(view:"/app")
		"500"(view:'/error')
	}
}

URL Mapping

import grails.validation.Validateable

@Validateable
class Question {
	Integer key
	String text
	String desc
	Integer voteCount
	List<String> errorMessages
	static constraints = {
		key unique: true
		text blank: false
		desc blank: false
		errorMessages nullable: true
	}
}

Command Object And Validation

question.desc.nullable=Description cannot be null
question.desc.blank=Description cannot be blank
obj.validate()
if(!obj.hasErrors()){
    notes.put(obj.key.toString(), obj)
}
else{
    obj.errorMessages = obj?.errors?.allErrors?.collect{messageSource.getMessage(it,null)
}
window.appContext = '${request.contextPath}';

Setting Up the Context Root

Angular Structure

   |-src
   |---dependencies
   |-----angular
   |---main
   |-----angular
   |-------question
   |-------webComponent
   |-----partials/templates
   |-------error
   |-------list
   |-------question
   |---test
   |-----features
   |-------step_definitions
   |-------support
   |-----jasmine

Grails Assets are Minified

(by default)

Ways To Integrate

  • Rhino based Grails plugin
  • Turn off minifiction
  • Grunt
module.exports = function(grunt) {
	grunt.loadNpmTasks('grunt-html2js');
	grunt.loadNpmTasks('grunt-contrib-concat');
	grunt.loadNpmTasks('grunt-contrib-watch');
	grunt.loadNpmTasks('grunt-contrib-clean');
	grunt.loadNpmTasks('grunt-ng-annotate');
	grunt.loadNpmTasks('grunt-contrib-uglify');
	grunt.loadNpmTasks('grunt-contrib-copy');
	grunt.loadNpmTasks('grunt-cucumber');
	grunt.initConfig({
		pkg : grunt.file.readJSON('package.json'),
		html2js : {
			build : {
				src : [ 'src/main/partials/**/*.html', 
				        'src/main/partials/**/*.jade' ],
				dest : 'build/ng-grails-templates.js'
			}
		},
		concat : {
			core : {
				src : [ 'src/main/angular/**/app.js',
						'src/main/angular/**/*.js'],
				dest : 'build/ng-grails-core.js',
			},
		    combine: {
		    	src:['build/ng-grails-templates.js',
		    	     'build/ng-grails-core.js'],
		    	dest : 'build/ng-grails.js',     
		    },
			jasmine: {
				src:['src/test/jasmine/**/*.js'],
				dest:'build/ng-grails-test.js'
			}
			
		},
		clean: {
			  build: ["build",'web-app/js/ng-grails.js','web-app/js/templates.js']
	    },
		watch : {
			html2js : {
				files : [ 'src/main/partials/**/*.html', 
				          'src/main/partials/**/*.jade' ],
				tasks : [ 'html2js' ]
			},
			concat : {
				files : ['src/main/angular/**/app.js',
							'src/main/angular/**/*.js',
							'build/ng-grails-templates.js'],
				tasks : [ 'concat' ]
			}
		},
		ngAnnotate: {
	        options: {
	        	remove: true
	        },
	        release: {
	        	files: {
	        	  'build/ng-grails-annotated.js': ['build/ng-grails.js']
	        	}
	        },
	    },
	    uglify: {
	        release: {
	          files: {
	            'build/ng-grails-min.js': ['build/ng-grails-annotated.js']
	          }
	        }
	    },
        copy: {
          release: {
              files: [
                  {src: ['build/ng-grails-min.js'], dest: 'web-app/js/ng-grails.js'},
              ],
          },
          build: {
        	  files: [
                  {src: ['build/ng-grails.js'], dest: 'web-app/js/ng-grails.js'},
              ],
          },
          jasmine: {
        	  files: [
        	          {src: ['build/ng-grails-test.js'], dest: 'web-app/js/ng-grails-test.js'}
        	          ]
          }
        },
        cucumberjs: {
            src: 'src/test/features',
            options: {
              steps: "src/test/features/step_definitions"
            }
        }
	});
	grunt.registerTask('default', [ 'build' ]);
	grunt.registerTask('build', [ 'clean', 'html2js', 'concat:core', 'concat:combine', 'copy:build']);
	grunt.registerTask('release', [ 'clean', 'html2js', 'concat:core', 'concat:combine', 'ngAnnotate', 'uglify', 'copy:release' ]);
	grunt.registerTask('test', ['build','concat:jasmine', 'copy:jasmine']);
	return grunt;
}

Grunt File

describe("Testing the question", function() {
	var $controller,
	    $location,
	    $rootScope,
	    $scope,
	    controller,
	    questionService;
	    var item = {
			put:function(){},
			remove:function(){}
	    }, question;
	beforeEach(module('jg.ngGrails'));
	var wire = function( _$rootScope_, _$location_, _$controller_, _questionService_, $q){
		$controller = _$controller_;
		$location = _$location_;
		spyOn($location, 'path');
		$rootScope = _$rootScope_;
		$scope = $rootScope.$new();
		questionService = _questionService_;
		spyOn(item, 'put');
		spyOn(item, 'remove');
		var defer = $q.defer();
		spyOn(questionService, 'add').and.returnValue(defer.promise);
		defer.resolve();
		questionService.questions = [item,item];
		controller = $controller('questionController', { 
		  $scope: $scope
		});
	}
	beforeEach(inject(wire));
	it("Make sure vote up works", function() {
		expect(item.put).not.toHaveBeenCalled();
		controller.voteUp(1);
		expect(item.put).toHaveBeenCalled();
	});
	it("Make sure vote down works", function() {
		expect(item.put).not.toHaveBeenCalled();
		controller.voteDown(1);
		expect(item.put).toHaveBeenCalled();
	});
	it("Make sure remove works", function() {
		expect(item.remove).not.toHaveBeenCalled();
		controller.delete(1);
		expect(item.remove).toHaveBeenCalled();
	});
	it("Make sure the edit is working as expected", function(){
		expect(item.put).not.toHaveBeenCalled();
		expect($location.path).not.toHaveBeenCalled();
		controller.edit(1);
		expect(item.put).toHaveBeenCalled();
		expect($location.path).toHaveBeenCalledWith('');
	})
});

Jasmine test

Feature: Simple Feature
  As a user 
  I want to add a question

  Scenario: Adding a question
    Given I am on the main page
    When I click the add button
    And I fill out question information
    Then I should see the new question

CucumberJS

/*jslint node: true */
"use strict";
var Browser = require('zombie'),
    assert = require("assert"),
    Chance = require('chance');

var WorldConstructor = function WorldConstructor(callback) {
	this.browser = new Browser();
	this.chance = new Chance();
	var addForm = "#add-form";
	var _this = this;
	function addQuestionFormLoaded(window) {
		return window.document.querySelector(addForm);
	}
	var world = {
		visit : function(url, callback) {
			this.browser.visit(url);
			this.browser.wait(function() {
				console.log("Waiting to callback");
				callback();
			});
		}.bind(this),
		clickLink : function(selector, callback) {
			this.browser.clickLink(selector, function() {
				var promise = _this.browser.wait(addQuestionFormLoaded, null)
				console.log("Test 123");
				promise.then(function() {
					console.log("Test");
					callback();
				})
			});
		}.bind(this), 
		fillQuestionForm: function(callback){
			var title = this.chance.string({length: 20});
			assert.ok(this.browser.query(addForm), "It should have the add form");
			this.browser.fill('textArea[id^="taHtmlElement"]', "Test Desc").fill("#questionText", this.title);
			this.browser.pressButton("Add Note", function() {
				callback();
			});
		}.bind(this),
		checkResults: function(callback){
			var x = this.browser.html(".question-text");
			var hasText = (x.indexOf(this.title) > -1)
			assert.equal(hasText, true, "The title should show up somewhere in the questions text.");
			callback();
		}.bind(this)
	}

	callback(world); // tell Cucumber we're finished and to use our world
						// object instead of 'this'
};
exports.World = WorldConstructor;
var noteStepDefinitionWrapper = function() {

	this.World = require("../support/world.js").World; // overwrite default
	// World constructor

	this.Given(/^I am on the main page$/, function(callback) {
		console.log("step 1");
		this.visit('http://localhost:8080/grails-angular', callback);
	});

	this.When(/^I click the add button$/, function(callback) {
		console.log("step 2");
		this.clickLink('#add-question', callback);
	});

	this.When(/^I fill out question information$/, function(callback) {
		console.log("step 3");
		this.fillQuestionForm(callback);
	});
	this.Then(/^I should see the new question$/, function(callback) {
		console.log("step 4");
		this.checkResults(callback)
	});
};
module.exports = noteStepDefinitionWrapper;

Grails/Angular Visualized

Example Module

var app = angular.module('jg.ngGrails', [ 
            'templates-build',
            'ngRoute', 
            'ngAnimate',
            'textAngular',
            'restangular'
          ]);

//We need to setup our router
var RouteProvider = function ($routeProvider) {
    $routeProvider.when('/', {
        templateUrl: 'main/partials/view.jade'
    })
    .when('/add', {
        templateUrl: 'main/partials/add.jade'
        	
    })
    .when('/edit', {
        templateUrl: 'main/partials/edit.jade'
    })
    .when('/wc', {
    	templateUrl: 'main/partials/webComponent.jade',
    	controller: WebComponentController
    })
    .otherwise({
        redirectTo: '/'
    });
}
app.config(RouteProvider);

Question Service

function QuestionService($interval, Restangular) {
	this.question = Restangular.all('Note');
	var _this = this, errorCount = 0, running = true;
	this.questions = [];
	this.selectedQuestion;
	function questionsReceived(questions) {
		errorCount = 0;
		if (_this.questions.length != questions.length) {
			// Updating the lists seemed to help with performance.
			while(_this.questions.length > 0) {
				_this.questions.pop();
			}
			angular.forEach(questions, function(value) {
				_this.questions.push(value)
			});
			
		} else {
			angular.forEach(questions, function(value, key) {
				_this.questions[key].voteCount = value.voteCount
			})
		}
	}
	function questionsFailed(question) {
		running = !(++errorCount > 5);
	}
	this.add = function(item){
		return _this.question.post(item);
	}
	this.getQuestions = function() {
		this.question.getList().then(questionsReceived, questionsFailed);
	}
	this.getQuestion = function(id) {
		return this.questions[id];
	}
	//TODO: Very inefficient some of these issues will be addressed in 2.0
	var clock = $interval(function() {
		if (running) {
			_this.getQuestions();
		}
	}, 100);
}
app.service('questionService', QuestionService);

List Directive

function ListDirective($templateCache) {
  return {
	  restrict: "E",
	  templateUrl: 'main/partials/question/listQuestion.jade',
	  controller:  'questionController',
	  //Example of how you could use controller as per https://github.com/angular/angular.js/issues/7635
	  //I left this out because it would add 3 lines and remove 1 ;-)
//	  controllerAs: 'questionCtrl'
  }
};
app.directive('jgNoteList', ListDirective);

Question Controller

function QuestionController($scope, $location, questionService) {
	//SEE ListController
    $scope.questionCtrl = this;
    var _this = this,
	question = questionService.question;
    questionService.getQuestions();    
    this.questions = questionService.questions;
    this.selectedId = questionService.selectedQuestion;
	this.selectedQuestion = questionService.questions[questionService.selectedQuestion];
	this.errorMessages = [];
	$scope.$watch('questionCtrl.questions' , function(val){
		if(val != null && questionService.selectedQuestion != null){
			this.selectedQuestion = questionService.questions[questionService.selectedQuestion];
		}
	})
	this.gotoEdit = function(key) {
		questionService.selectedQuestion = key
		$location.path("/edit");
    }
	
	this.add = function() {
		questionService.add(_this.newQuestion)
                 .then(function(data){
			if(!data.errorMessages || data.errorMessages.length == 0){
				$location.path('');
			}
			else{
				//TODO: Go back and look at Angular error handling
				//Clear the list
				while(_this.errorMessages.length > 0) {
					_this.errorMessages.pop();
				}
				//Add new items back on
				angular.forEach(data.errorMessages, function(value) {
					_this.errorMessages.push(value)
				})
			}
		})
	}
	this.voteUp = function(index) {
		_this.questions[index].voteCount++;
		_this.questions[index].put();
	}
	this.voteDown = function(index) {
		_this.questions[index].voteCount--;
		_this.questions[index].put();
	}
	this.delete = function(index) {
		_this.questions[index].remove();
	}
	this.edit = function(index){
		questionService.questions[index].put();
		//TODO Could probably be toggleable
		$location.path('');
	}
}
app.controller('questionController', QuestionController);

Scope Lifecycle

  • ngRoute events

    • $routeChangeStart

    • $routeUpdate

    • $routeChangeSuccess

  • ngView events

    • $viewContentLoaded

  • controller initialization

Templating (HTML2JS)

.add-note.question
  .row
     h3.col-xs-offset-1 Add A Question
  .row.well(ng-if="questionCtrl.errorMessages.length > 0")
     .validation-error(ng-repeat="errorMessage in questionCtrl.errorMessages")
       p {{errorMessage}}
  .row
      .input-group
        label(for="questionText") Question Text
        input#questionText(name="questionText", type="text", ng-model="questionCtrl.newQuestion.text")
      .input-group  
        label(for="questionDesc") Question Description
        text-angular(ng-model="questionCtrl.newQuestion.desc")
  .row.pad-top 
    .col-sm-offset-2
      button(class="btn btn-primary",ng-click="questionCtrl.add()") Add Note
      
angular.module("main/partials/question/addQuestion.jade", []).run(["$templateCache", function($templateCache) {
  $templateCache.put("main/partials/question/addQuestion.jade",
    "<div class=\"add-note question\">\n" +
    "  <div class=\"row\">\n" +
    "    <h3 class=\"col-xs-offset-1\">Add A Question</h3>\n" +
    "  </div>\n" +
    "  <div ng-if=\"questionCtrl.errorMessages.length > 0\" class=\"row well\">\n" +
    "    <div ng-repeat=\"errorMessage in questionCtrl.errorMessages\" class=\"validation-error\">\n" +
    "      <p>{{errorMessage}}</p>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "  <div class=\"row\">\n" +
    "    <div class=\"input-group\">\n" +
    "      <label for=\"questionText\">Question Text</label>\n" +
    "      <input id=\"questionText\" name=\"questionText\" type=\"text\" ng-model=\"questionCtrl.newQuestion.text\"/>\n" +
    "    </div>\n" +
    "    <div class=\"input-group\"> \n" +
    "      <label for=\"questionDesc\">Question Description</label>\n" +
    "      <text-angular ng-model=\"questionCtrl.newQuestion.desc\"></text-angular>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "  <div class=\"row pad-top\"> \n" +
    "    <div class=\"col-sm-offset-2\">\n" +
    "      <button ng-click=\"questionCtrl.add()\" class=\"btn btn-primary\">Add Note</button>\n" +
    "    </div>\n" +
    "  </div>\n" +
    "</div>");
}]);

Validation

@Validateable
class Question {
	Integer key
	String text
	String desc
	Integer voteCount
	List<String> errorMessages
	static constraints = {
		key unique: true
		text blank: false
		desc blank: false
		errorMessages nullable: true
	}
}
def create(obj){
	obj.key = key++
	obj.voteCount = 0
	//Validation needs to happen after adding the key
	obj.validate()
	if(!obj.hasErrors()){
		notes.put(obj.key.toString(), obj)
	}
	else{
		obj.errorMessages = obj?.errors?.allErrors?.collect{messageSource.getMessage(it,null)}
	}
	obj
}

What

Is

Next

Grails 2.4/3.0

  • Promises
  • Gradle over GANT
  • Spring Web Components
  • Java 8
  • Multiple Sever Environments

Promises

import static grails.async.Promises.task
import static grails.async.Promises.waitAll
 
def task1 = task {
    println "task1 - starting"
    Thread.sleep(5000)
    println "task1 - ending"
}
 
def task2 = task {
    println "task2 - starting"
    Thread.sleep(1000)
    println "task2 - ending"
}
 
waitAll(task1, task2)

var promise1 = function(){
  var defer1 = $q.defer();
  setTimeout(function(){
    console.log("Promise 1 done");
    defer1.resolve();
  }, 5000)
  return defer.promise;
}
var promise2 = function(){
  var defer2 = $q.defer();
  setTimeout(function(){
    console.log("Promise 2 done");
    defer2.resolve();
  }, 5000)
  return defer2.promise;
}
$q.all([promise1, promise2]).then(function(){
  console.log("Both Done");
});
class SocketController {
	def index(){
		
	}
	@MessageMapping("/hello")
	@SendTo("/topic/hello")
	protected String hello(String world) {
	   return "hello from controller, ${world}!"+new String(UUID.randomUUID().getMostSignificantBits());
	}
}

Grails STomp

    var socket = new SockJS(window.appContext+'/stomp');
    var client = Stomp.over(socket);
    $scope.messages = [];
    client.connect({}, function() {
       client.subscribe("/topic/hello", function(message) {
    	   console.log("Who is this");
    	   $scope.messages.push(message.body);
    	   $scope.$digest();
       });
    });
    $scope.send = function(){
    	client.send("/app/hello", {}, $scope.text);
    }

Angular 2.0 Syntax

Going

  • Controllers
  • Directives
  • Scope
  • Module
  • JQLite

Coming

  • More Generic Syntax
  • ShadowDOM!!!!!!!
  • DI Query
  • Benchpress
  • WTF Instrumentation (Web Tracing Framework)

http://bit.ly/1tLYgXM

http://bit.ly/1tLYgXM

Why
How
What is Next

Thanks CodeMasH

Grails V1 

  • One page per action

  • Offload complexity to services

Grails V2

  • Started using ajax with special GSP tags

  • Provide respond in addition to render

deck

By Jackie Gleason

deck

  • 1,175