Source Redmonk | X: # Github Y: # Stackoverflow
Typically an MVC framework will refer to the controller to update the view.
This works great for simple or extremely secure applications.
But not everything needs to be done on the server!
Now we need a way to manage our view models.
One way to handle this is with JQuery. JQuery provides an easy interface for manipulating the DOM
JQuery tends to get really complicated.
Server
Browser
JQuery
DOM
Framework to help manage data bindings.
Most important for real-time frequently updating applications.
Compiles directly into the DOM. So no complex selectors.
Server
Browser
Angular
$rootScope
DOM
Instead of pages Angular uses routes.
Think of it as you render one page, then let Angular control navigation.
There are 3rd party routings like the Angular-UI project. However, we will talk more in the future of Angular section.
Routes will typically paint Directives. These are also known as "Web Components"
This allows for reusability and encapsulation.
Makes testing a whole lot easier.
More later
OMG there is an Angular 2!
But it is 5x faster
https://github.com/angular/angular
https://github.com/angular/angular.js
Separate folders for each type of code
Tests are separate from actual code
Tries to produce "components" but just seems to add complexity.
Works well for "legacy" projects
The idea is to break everything into small "units". To do this we include everything in one directory.
Fits with the idea of "Shadow DOM" and Web Components.
This will be fairly common in next-gen browsers.
// Create a new Module
var app1 = angular.module('plunker', []);
// Access an existing module
var app2 = angular.module('plunker');
// Create another module with a dependency
var app3 = angular.module('other', [
'restangular'
]);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
.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>");
}]);
// Controller
function TestController ($scope, $http){...}
// Without annotation (cannot be minified)
app.controller('testCtrl', TestController)
// Using $inject
TestController.$inject = ['$scope', '$http'];
// Without using $inject
app.controller('testCtrl', ['$scope', '$http', function($scope, $http) {...}]);
A Singleton within the application
Most Angular services begin with $
Basically "Custom Tags"
Allow one to easily bind Angular to HTML
Probably one of the hardest concepts to understand
Prepend to avoid collisions
Gone in 2.0
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);
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;
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('');
})
});
http://bit.ly/1tLYgXM
http://bit.ly/1tLYgXM
ngRoute events
$routeChangeStart
$routeUpdate
$routeChangeSuccess
ngView events
$viewContentLoaded
@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
}