http://bit.ly/1DobCC3
http://codemash.jackiergleason.com/
Source Business Insider
Source Linked In
Source Redmonk | X: # Github Y: # Stackoverflow
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]
);
})
}
}
})
|-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
@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(_) >> []
}
}
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)
}
}
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')
}
}
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
}
}
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}';
|-src
|---dependencies
|-----angular
|---main
|-----angular
|-------question
|-------webComponent
|-----partials/templates
|-------error
|-------list
|-------question
|---test
|-----features
|-------step_definitions
|-------support
|-----jasmine
(by default)
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;
}
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('');
})
});
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
/*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;
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);
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);
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);
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);
ngRoute events
$routeChangeStart
$routeUpdate
$routeChangeSuccess
ngView events
$viewContentLoaded
.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>");
}]);
@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
}
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());
}
}
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);
}
http://bit.ly/1tLYgXM
http://bit.ly/1tLYgXM
One page per action
Started using ajax with special GSP tags