app/ app.module.js app.config.js components/ calendar.directive.js user-profile.directive.js user-profile.directive.html layout/ shell.html shell.controller.js shell.controller.spec.js topnav.controller.js topnav.controller.spec.js sessions/ sessions.html sessions.controller.js sessions.routes.js session-detail.html session-detail.controller.js
people/ attendees.html attendees.controller.js people.routes.js speakers.html speakers.controller.js speaker-detail.html speaker-detail.controller.js people.e2e.spec.js services/ data.service.js localstorage.service.js logger.service.js spinner.service.js
dist/ index.html js/ app094hgt.min.js vendor065htui.min.js css/ app465rte.min.css vendor543rte.min.css
configure the rules for each project, add githooks if the team is large
Create a set of gulp tasks that creates in the folder dist/
Refactor the app https://github.com/aotaduy/angular-phonecat-14.git into a main module and a set of submodules by feature using the main patterns present on the style guide.
$stateProvider
.state('movie-page', {
title: 'Movie',
url: '/movie/:movieId',
templateUrl: 'app/pages/movie/movie-page.html',
controller: 'MoviePageController',
controllerAs: 'movieVm',
resolve: {
movie: ['$stateParams', 'moviesConnector',
function($stateParams, moviesConnector) {
var id = $stateParams.movieId;
return moviesConnector.movieInfo(id)
.then(function (response) {
return response.data;
});
}]
}
});
Add a page that shows the movies being played from /api/movies/playing using a resolve to get the data.
component moviesConnector
has the method that retrives this data.
event.preventDefault()
to stop transitionAdding a class corresponding to the state name you can scope css
Intercept events to protect pages from access.
$rootScope.$on('$stateChangeStart', function(e, to) {
if (!to.forceLogin) return;
if (!isLoggedIn($currentUser)) {
e.preventDefault();
// Optionally set option.notify to false if you don't want
// to retrigger another $stateChangeStart event
$state.go(result.to, result.params, {notify: false});
}
Add an ordering option (asc, desc) and ordering field (title, release_date, vote_average) to simple search view in angular-example. to control the order of the query. This parameter shold persist in the query string to allow deeplinking of search results.
Create a search page with an input for the search string, and controls for the order of results (asc, desc) of the results. Search string should be part of the url and ordering should be a queryString parameter. A user should be able to get the same resluts by copy and pasting the link in another browser.
Create a new page for the login route. Add the events to protect the movies details page with the login. Save the login state in a mock service that logs in any user with a number on his password.
url
to all child state urls.template
with its own ui-view(s)
that its child states will populate.
resolve
.data
.onEnter
or onExit
function that may modify the application in someway.Create a page that shows a movie header with the title and poster, and three tabs. one with the overview of the movie, release date, and basic data. Other with the similar movies, and another with a list of genres. Page should receive the id as a parameter and if the user clicks in a movie poster should navigate to that movie in the same page.
$digest will run at least once, and
At least once, tops 10.
.directive('creditCardSelector', function () {
return {
restrict: 'E',
scope: {
cards: '=', // Array of cards
selectedCard: '=?', // Current selected card
removeAction: '&?', // Callback action
showRemove: '=?' // Boolean to show delete card button
},
templateUrl: 'templates/components/creditCardSelector/creditCardSelector.html',
controller: 'CreditCardSelectorController',
controllerAs: 'creditCardSelectorVm',
bindToController: true
};
});
define a link: function or object.
var NG_HIDE_CLASS = 'ng-hide';
var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
ngShowDirective = ['$animate', function($animate) {
return {
restrict: 'A',
multiElement: true,
link: function(scope, element, attr) {
scope.$watch(attr.ngShow, function ngShowWatchAction(value) {
$animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, {
tempClasses: NG_HIDE_IN_PROGRESS_CLASS
});
});
}
};
}];
When need to add new directives to the DOM.
.directive("directiveName",function () {
return {
controller: function() {
// controller code here...
},
compile: {
// compile code here...
return {
pre: function() {
// pre-link code here...
},
post: function() {
// post-link code here...
}
};
}
}
})
Create an atribute directive deletable
that adds a delete button to the top left of any block element. And triggers an event $deleted when done.
hint: Use the compile function.
Test your directive in an ng-repeat loop.
Wrap arbitrary content
{{::}}
On Object with three states, pending, fullfilled, resolved. Representing a future value.
function(aPromise, $q) {
aPromise
.then(successCallback, errorCallback)
.catch(errorCallback)
.finally(finallyCallback);
rejected = $q.reject();
resolved = $q.when();
newPromise = $q.all([rejected, resolved, aPromise]);
newPromise = $q.race([rejected, resolved, aPromise]);
//Create promises
deferred = $q.defer();
deferred.resolve(aValue);
deferred.reject(aValue);
return deferred.promise;
}
function anAsyncCall() {
var promise = doSomethingAsync();
promise.then(function() {
somethingComplicated();
});
return promise;
}
function anAsyncCall() {
var promise = doSomethingAsync();
return promise.then(function() {
somethingComplicated()
});
}
}
somethingAsync.then(
function() {
return somethingElseAsync();
},
function(err) {
handleMyError(err);
});
somethingAsync
.then(function() {
return somethingElseAsync();
})
.catch(function(err) {
handleMyError(err);
});
var promise;
if (asyncCallNeeded)
promise = doSomethingAsync();
else
promise = $q.when(42);
promise.then(function() {
doSomethingCool();
});
$q.when(asyncCallNeeded ? doSomethingAsync() : 42)
.then(function(value){
doSomethingGood();
})
.catch(function(err) {
handleTheError();
});
}
var deferred = $q.defer();
doSomethingAsync().then(function(res) {
res = manipulateMeInSomeWay(res);
deferred.resolve(res);
}, function(err) {
deferred.reject(err);
});
return deferred.promise;
return doSomethingAsync().then(function(res) {
return manipulateMeInSomeWay(res);
});
var config = {
method: 'GET',
url: '/someUrl',
params: {query: 'star wars'},
data: {user: 'pepe'}
headers: {'Application-Id': 'fdkjshagfkjhasdgfqwybbsdf'},
timeout: 1000, // response status will be -1
cache: true,
transformRequest: transformFun,
transformResponse: transformFun2
};
$http(config).then(function successCallback(response) {
// this callback will be called asynchronously
// when the response is available
}, function errorCallback(response) {
// called asynchronously if an error occurs
// or server returns response with an error status.
});
set default values for $http
$httpProvider.defaults.headers.common = {
'X-App': 'AngularJs'
};
$httpProvider.defaults.headers.put = {...};
$httpProvider.defaults.cache = true;
$httpProvider.useApplyAsync(true);
$http.defaults.cache = true;
$http.get('/api/random/slow', {cache: true});
$httpProvider.defaults.headers
$http.defaults.headers
$http({headers: {} })
Modify randomService so when it receives a 401 on the getRandom request (endpoint: /api/random/fast) so it Authorizes via HTTP Basic Authentication user: test password: 123456
ExampleAuthorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l
QWxhZGRpbjpPcGVuU2VzYW1l is base64 encoding of user:passwd
Pipeline to transform requests and responses data and headers
pre processing of requests and responses, injectable services and factories. Free to modify the response or request config. could return a promise
module.factory('interceptor', function($log) {
$log.debug('$log is here to show you that this is a regular factory with injection');
var myInterceptor = {
response: function () {},
responseError: function () {},
....
....
};
return myInterceptor;
});
module.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('myInterceptor');
}]);
Angular is a framework conceived with testeability in mind.
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function() {
assert.equal(-1, [1,2,3].indexOf(4));
});
});
});
//BDD Should
foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.length(3);
tea.should.have.property('flavors')
.with.length(3);
//BDD Expect expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.length(3);
expect(tea).to.have.property('flavors')
.with.length(3);
//Assert
assert.typeOf(foo, 'string');
assert.equal(foo, 'bar');
assert.lengthOf(foo, 3)
assert.property(tea, 'flavors');
assert.lengthOf(tea.flavors, 3);
Only for readibility:
to be
been is that which and has have with at of same
//BDD Expect expect(foo).to.be.a('string');
expect(foo).to.be.equal.to('bar');
expect(foo).to.have.same.length(3);
expect(tea).to.have.a.property('flavors').with.length(3);
expect(tea).to.have.been.equal('flavors').and.have.a.length.of(3);
ngMock, A module that facilitates mocking in unit tests.
function($provide){}
{value: value}
describe('MyApp', function() {
// You need to load modules that you want to test,
// it loads only the "ng" module by default.
beforeEach(module('myApplicationModule'));
beforeEach(module(function($provide) {
$provide.value('version', 'overridden'); // override version here
});
}));
injects components into the function parameter
var myService;
// Wrap the parameter in underscores
beforeEach( inject( function(_myService_){
myService = _myService_;
}));
// Use myService in a series of tests.
it('makes use of myService', function() {
myService.doStuff();
});
Easiest case since filters should not have side effects
describe('Filter: words', function() {
'use strict';
var filter;
// Refresh the $filter every time.
beforeEach(module('common-filters')); // load the module
beforeEach(inject(function(_$filter_) { // inject filter
filter = _$filter_;
})
);
it('should return array of words', function() {
expect(filter('words')('one two three')).to.deep.equal(['one', 'two', 'three']);
});
it('should return empty array', function() {
expect(filter('words')('')).to.equal('');
});
});
describe('Controller: todo-list', function() {
'use strict';
var controller,
scope;
// Refresh the $filter every time.
beforeEach(module('todoListControllerDemo'));
beforeEach(inject(function(_$rootScope_, $controller) {
scope = _$rootScope_.$new();
controller = $controller('TodoListController',
{$scope: scope});
})
);
it('should start empty', function() {
expect(controller.list.length).to.equal(0);
});
it('should add items', function() {
controller.itemText = 'First item';
controller.add(controller.itemText);
expect(controller.list.length).to.equal(1);
controller.itemText = 'Second item';
controller.add(controller.itemText);
expect(controller.list.length).to.equal(2);
controller.remove(1);
expect(controller.list.length).to.equal(1);
});
});
Test runner
Available for Chrome and Chrome Canary, Firefox, Safari, PhantomJS, Opera, IE, etc
npm install -g karma-cli
npm install karma-mocha karma-chrome-launcher karma-phantomjs-launcher --save-dev
Create karma.conf.js in the root of your project
// karma.conf.js
module.exports = function(config) {
config.set({
frameworks: ['mocha'],
files: [
'*.js'
],
client: {
mocha: {
// change Karma's debug.html to the mocha web reporter
reporter: 'html',
// require specific files after Mocha is initialized
require: [require.resolve('bdd-lazy-var/bdd_lazy_var_global')],
// custom ui, defined in required file above
ui: 'bdd-lazy-var/global',
}
}
});
};
npm install karma karma-coverage --save-dev
// karma.conf.js
module.exports = function(config) {
config.set({
files: [
'src/**/*.js',
'test/**/*.js'
],
// coverage reporter generates the coverage
reporters: ['progress', 'coverage'],
preprocessors: {
// source files, that you wanna generate coverage for
// do not include tests or libraries
// (these files will be instrumented by Istanbul)
'src/**/*.js': ['coverage']
},
// optionally, configure the reporter
coverageReporter: {
type : 'html',
dir : 'coverage/'
}
});
};
Both in angular-example repo.
whenGET().respond()
expectGET().respond()
flush()
to remove async responses an ease test writingverifyNoOutstandingRequest
, verifyNoOutstandingExpectation
describe('Service: moviesConnector', function() {
'use strict';
var
service,
$httpBackend;
// Refresh the $filter every time.
beforeEach(module('movies.connector'));
beforeEach(inject(function (_$httpBackend_, _moviesConnector_) {
$httpBackend = _$httpBackend_;
$httpBackend.whenGET('/api/movies/search/star')
.respond({
results:[
{title: 'Star Wars'},
{title: 'Star Trek'}
]
});
$httpBackend.whenGET('/api/movies/playing/')
.respond({
results:[
{title: 'Deadpool'},
{title: 'Superman'}
]
});
$httpBackend.whenGET('/api/movies/')
.respond({
results:[
{title: 'Deadpool'},
{title: 'Superman'}
]
});
service = _moviesConnector_;
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingRequest();
});
it('should get nowPlaying movies', function() {
service.nowPlaying().then(function (result) {
expect(result.data.results[0].title).to.equal('Deadpool');
});
$httpBackend.flush();
});
it('should get nowPlaying movies', function() {
$httpBackend.expectGET('/api/movies/playing/')
.respond({
results:[
{title: 'Deadpool'}
]
});
service.nowPlaying();
$httpBackend.flush();
});
it('should get nowPlaying movies and top rated', function() {
$httpBackend.expectGET('/api/movies/playing/')
.respond({
results:[
{title: 'Deadpool'}
]
});
service.nowPlaying();
service.topRatedMovies();
$httpBackend.flush();
});
it('should search', function() {
$httpBackend.expect('GET', /api\/movies\/search\/.*/ )
.respond({
results:[
{title: 'Deadpool'},
{title: 'Star wars'},
{title: 'Star trek'}
]
});
service.search('peperino');
$httpBackend.flush();
});
});
it('should getRandomNumberDeferred', function(done) {
service.getRandomDeferred().then(function (response) {
expect(response).to.equal((0.7).toFixed(3));
done();
});
service.getRandomDeferred().then(function (response) {
done();
});
$timeout.flush();
$rootScope.$digest();
});
describe('ngClick', function() {
var element;
afterEach(function() {
dealoc(element);
});
it('should get called on a click', inject(function($rootScope, $compile) {
element = $compile('')($rootScope);
$rootScope.$digest();
expect($rootScope.clicked).toBeFalsy();
browserTrigger(element, 'click');
expect($rootScope.clicked).toEqual(true);
}));
it('should pass event object', inject(function($rootScope, $compile) {
element = $compile('')($rootScope);
$rootScope.$digest();
browserTrigger(element, 'click');
expect($rootScope.event).toBeDefined();
}));
});
A spy is a function that records the calls and return values.
"test should call subscribers on publish": function () {
var callback = sinon.spy();
PubSub.subscribe("message", callback);
PubSub.publishSync("message");
assertTrue(callback.called);
}
a stub is a function with pre trained behaviour
it('should getRandom', function() {
sinon.stub(service, 'getRandom');
service.getRandom.onCall(0).returns($q.when(0.2));
service.getRandom.onCall(1).returns($q.when(0.3));
controller.updateNextRandom();
$rootScope.$digest();
expect(controller.messageHistory).to.equal('0.2\n');
controller.updateNextRandom();
$rootScope.$digest();
expect(controller.messageHistory).to.equal('0.2\n0.3\n');
service.getRandom.restore();
});
Mocks are stubs or spies that you can verify on
it('should getRandomLocal mock', function() {
var mock = sinon.mock(Math);
mock.expects('random').once();
controller.updateNextRandomLocal();
$rootScope.$digest();
mock.verify();
mock.restore();
});
Create a unit test for the search directive (client/app/components/search-box) in angular-example
import { Component } from '@angular/core';
import {TodoItem} from './todo-item'
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.css']
})
export class AppComponent {
title = 'app works!';
list: Array<TodoItem>;
item: TodoItem;
constructor() {
this.list = [];
this.item = new TodoItem;
}
add( anItem: TodoItem) {
this.list.push(anItem);
this.item = new TodoItem;
}
remove( index: number) {
this.list.splice(index,1);
}
}
<h1 [ngClass]="{'big': list.length > 5}" >Todo {{list.length}}</h1>
<input type="text" [(ngModel)]="item.text">
<input type="button" value="Add" (click)="add(item)">
<ul>
<li
[ngClass]="{'done': todoItem.done}" \
*ngFor="let todoItem of list; let i = index">
<input type="checkbox" [(ngModel)]="todoItem.done">
{{todoItem.text}}
<input
*ngIf="todoItem.done"
(click)="remove(i)"
type="button"
name="name"
value="X">
</li>
</ul>
<table border=1>
<!-- expression calculates colspan=2 -->
<tr><td [attr.colspan]="1 + 1">One-Two</td></tr>
<!-- ERROR: There is no `colspan` property to set!
<tr><td colspan="{{1 + 1}}">Three-Four</td></tr>
-->
<tr><td>Five</td><td>Six</td></tr>
</table>
<!-- toggle the "special" class on/off with a property -->
<div [class.special]="isSpecial">The class binding is special</div>
<!-- binding to `class.special` trumps the class attribute -->
<div class="special"
[class.special]="!isSpecial">This one is not so special</div>
<button [style.color] = "isSpecial ? 'red': 'green'">Red</button>
<button [style.background-color]="canSave ? 'cyan': 'grey'" >Save</button>
<button [style.font-size.em]="isSpecial ? 3 : 1" >Big</button>
<button [style.font-size.%]="!isSpecial ? 150 : 50" >Small</button>
<!-- phone refers to the input element; pass its `value` to an event handler -->
<input #phone placeholder="phone number">
<button (click)="callPhone(phone.value)">Call</button>
<!-- fax refers to the input element; pass its `value` to an event handler -->
<input ref-fax placeholder="fax number">
<button (click)="callFax(fax.value)">Fax</button>
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();
NgModule(options : {
constructor(options?: NgModuleMetadataType)
providers : Provider[]
declarations : Array<Type<any>|any[]>
imports : Array<Type<any>|ModuleWithProviders|any[]>
exports : Array<Type<any>|any[]>
entryComponents : Array<Type<any>|any[]>
bootstrap : Array<Type<any>|any[]>
schemas : Array<SchemaMetadata|any[]>
})
import { Component, Input, Output, EventEmitter } from '@angular/core';
import {TodoItem} from './todo-item';
@Component({
selector: 'todo-line',
template: `
<input type="checkbox" [(ngModel)]="item.done">
{{item.text}}
<input type="button" name="name" value="X" (click)="remove(item)">
`
})
export class TodoLine {
@Input() item: TodoItem;
@Output() removeRequested: EventEmitter = new EventEmitter();
remove( index: number) {
this.removeRequested.emit(this.item)
}
}
Create a CRUD app for users, with login, email and password. Create a component for the form and other for the list with the form.
WIP!
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
platformBrowserDynamic().bootstrapModule(AppModule);