MEAN - Notes from the field
Chris Clarke
Hydrahack Birmingham
18th March 2014
Full-Stack Development with Javascript
• Mongo
• Express
• AngularJS
• NodeJS
http://github.com/linnovate/mean
What’s MEAN?
Who are Talis?
• MongoDB ~2.5yrs
• Using express/node ~2yrs
• Angular ~9 months
Angular AppAngular App
Typical MEAN Shape
DBDB
APIAPI
Server side
pages
Server side
pages
StaticsStatics
JSON
JSON
HTMLHTML
JSON
Client side
Server side
Typical structure
Typical structure
Typical structure
Typical structure
Video Timeline Editor
Textbook Player
Angular 101
• Single page web app framework, by Google
• Extends HTML vocabulary to provide dynamic
views
• Broadly MVC (more accurately MVVM)
• Bi-directional data binding to HTML
Angular 101
• Routing
• Templates
• Controllers
• Directives
Routing
$routeProvider.when('/modules/:module_id', {
templateUrl: 'partials/module.html',
controller: 'TeachCtrl',
loginRequired: true,
activeTab:"teach"
});
Routing
$routeProvider.when('/modules/:module_id', {
templateUrl: 'partials/module.html',
controller: 'TeachCtrl',
loginRequired: true,
activeTab:"teach"
});
Routing
$routeProvider.when('/modules/:module_id', {
templateUrl: 'partials/module.html',
controller: 'TeachCtrl',
loginRequired: true,
activeTab:"teach"
});
Routing
$routeProvider.when('/modules/:module_id', {
templateUrl: 'partials/module.html',
controller: 'TeachCtrl',
loginRequired: true,
activeTab:"teach"
});
Routing
$routeProvider.when('/modules/:module_id', {
templateUrl: 'partials/module.html',
controller: 'TeachCtrl',
loginRequired: true,
activeTab:"teach"
});
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<input ng-model="profile.first_name" type="text" required>
<input ng-model="profile.surname" type="text" required>
<input ng-model="profile.email" type="email" required>
<button ng-disabled="!profile.email"
ng-click="update()">Update</button>
<input ng-model="profile.first_name" type="text" required>
<input ng-model="profile.surname" type="text" required>
<input ng-model="profile.email" type="email" required>
<button ng-disabled="!profile.email"
ng-click="update()">Update</button>
<input ng-model="profile.first_name" type="text" required>
<input ng-model="profile.surname" type="text" required>
<input ng-model="profile.email" type="email" required>
<button ng-disabled="!profile.email"
ng-click="update()">Update</button>
<input ng-model="profile.first_name" type="text" required>
<input ng-model="profile.surname" type="text" required>
<input ng-model="profile.email" type="email" required>
<button ng-disabled="!profile.email"
ng-click="update()">Update</button>
QuickTime™ and a
'avc1' decompressor
are needed to see this picture.
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Directives
<textbook-player user="user" textbook="textbook">
...
</textbook-player>
Directives
<textbook-player user="user" textbook="textbook">
...
</textbook-player>
Directives
<div user="user" textbook="textbook" textbook-player>
...
</div>
Directives
<div user="user" textbook="textbook" textbook-player>
...
</div>
angular.module('talis.directives.player.textbook', [])
.directive("textbookPlayer", function() {
return {
restrict: "A",
scope: {
user: '=',
entity: '='
},
controller: function($scope,textbookSvc) {
// textbook logic in here
}
}
});
Directives
<div user="user" textbook="textbook" textbook-player>
...
</div>
angular.module('talis.directives.player.textbook', [])
.directive("textbookPlayer", function() {
return {
restrict: "A",
scope: {
user: '=',
entity: '='
},
controller: function($scope,textbookSvc) {
// textbook logic in here
}
}
});
Directives
<div user="user" textbook="textbook" textbook-player>
...
</div>
angular.module('talis.directives.player.textbook', [])
.directive("textbookPlayer", function() {
return {
restrict: "A",
scope: {
user: '=',
entity: '='
},
controller: function($scope,textbookSvc) {
// textbook logic in here
}
}
});
Directives
<div user="user" textbook="textbook" textbook-player>
...
</div>
angular.module('talis.directives.player.textbook', [])
.directive("textbookPlayer", function() {
return {
restrict: "A",
scope: {
user: '=',
entity: '='
},
controller: function($scope,textbookSvc) {
// textbook logic in here
}
}
});
–Jonny Clientside
“Waat?”
Notes From the Field
Act I: The Basics
Elem vs. Attr directives
<textbook-player user="user" textbook="textbook">
...
</textbook-player>
<div user="user" textbook="textbook" textbook-player>
...
</div>
Minification
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
..
});
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',['$scope','userSvc’,
function($scope, userSvc) {
...
}
]);
Minification
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
..
});
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',['$scope','userSvc’,
function($scope, userSvc) {
...
}
]);
a.m('talis.controllers.user', [])
.c('AccountCtrl',['$scope','userSvc’,
function(s, u) {
...
}
]);
Mongo _id
{
_id: ObjectId(1234),
name: “Jonny Clientside”,
age: 24,
interests: [‘JQuery’,‘HTML5’
}
<a ng-href="#/people/{{ p._id }}">{{p.name}}</a>
Notes From the Field
Act II: Advanced
Angular AppAngular App
Typical MEAN Shape
DBDB
APIAPI
Server side
pages
Server side
pages
StaticsStatics
JSON
JSON
HTMLHTML
JSON
Client side
Server side
Angular AppAngular App
JSON
9090
9090
9090
9090
Users
API
Users
API
APIAPI
Server side
pages
Server side
pages
StaticsStatics
JSON
JSON
JSON
Client side
Meta
API
Meta
API
Files
API
Files
API
Anno
API
Anno
API
JSON
DBDBDBDBDBDB DBDB
RedisRedisRedisRedis
HTMLHTML
JSON
Logging
• A lot of activity in the client side
• Some within Express/Node server side
• More behind your API proxy
var loggingModule = angular.module('talis.services.logging', []);
loggingModule.factory(
"traceService",
function(){
return({
print: printStackTrace
});
}
);
loggingModule.provider(
"$exceptionHandler",{
$get: function(exceptionLoggingService){
return(exceptionLoggingService);
}
}
);
var loggingModule = angular.module('talis.services.logging', []);
loggingModule.factory(
"traceService",
function(){
return({
print: printStackTrace
});
}
);
loggingModule.provider(
"$exceptionHandler",{
$get: function(exceptionLoggingService){
return(exceptionLoggingService);
}
}
);
loggingModule.factory(
"exceptionLoggingService",
["$log","$window", "traceService",
function($log, $window, traceService){
function error(exception, cause){
$log.error.apply($log, arguments);
try{
var errorMessage = exception.toString();
var stackTrace = traceService.print({e: exception});
$.ajax({
type: "POST",
url: "/logger",
contentType: "application/json",
data: angular.toJson({
url: $window.location.href,
message: errorMessage,
type: "exception",
stackTrace: stackTrace,
cause: ( cause || "")
})
});
} catch (loggingError){
$log.warn("Error server-side logging failed");
$log.log(loggingError);
}
}
return(error);
}]
);
Logging
Security
• APIs secured with OAuth 2.0 Bearer tokens
• Tokens obtained with a key/secret
• If your app is downloaded and run on the client,
where do you put the secret?
Security
• Have node return the OAuth token as JSON behind
a login barrier
• Angular requests this JSON when a route that
requires login is first requsted
• If status != 200, Angular app redirects browser to
login page
• User logs in, repeat
Security
• Dealing with tokens on every service call is a PITA
• Tokens expiring is normal
• Deal with it globally using a couple of advanced
$http features
.run(function($rootScope,$injector) {
$injector.get("$http").defaults.transformRequest =
function(data, headersGetter) {
headersGetter()['Authorization']="Bearer "+$rootScope.token
if (data) {
return angular.toJson(data);
}
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
Environments
• Pretty usual to deal with prod, dev, testing
environment config on the server side
• Inject this into your client side app using a dynamic
JS include
Environments
<script type="text/javascript" src="env/config.js"></script>
Environments
angular.module('talis.environment', [], function($provide)
constant('API_ENDPOINT', 'http://localhost:3000').
constant('ACTIVATE_FEATURE_FLIPS',true);
Environments
angular.module('talis.environment', [], function($provide)
constant('API_ENDPOINT', 'https://talis.com').
constant('ACTIVATE_FEATURE_FLIPS',false);
That’s it.
http://engineering.talis.com
We are hiring!
http://www.talis.com/jobs
@talis
facebook.com/talisgroup
+44 (0) 121 374 2740
talis.com
info@talis.com
48 Frederick Street
Birmingham
B1 3HN

MEAN - Notes from the field (Full-Stack Development with Javascript)

  • 1.
    MEAN - Notesfrom the field Chris Clarke Hydrahack Birmingham 18th March 2014 Full-Stack Development with Javascript
  • 4.
    • Mongo • Express •AngularJS • NodeJS http://github.com/linnovate/mean What’s MEAN?
  • 5.
  • 6.
    • MongoDB ~2.5yrs •Using express/node ~2yrs • Angular ~9 months
  • 7.
    Angular AppAngular App TypicalMEAN Shape DBDB APIAPI Server side pages Server side pages StaticsStatics JSON JSON HTMLHTML JSON Client side Server side
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
    Angular 101 • Singlepage web app framework, by Google • Extends HTML vocabulary to provide dynamic views • Broadly MVC (more accurately MVVM) • Bi-directional data binding to HTML
  • 15.
    Angular 101 • Routing •Templates • Controllers • Directives
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
    <ul ng-show="modules!=null"> <li ng-repeat="min modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 22.
    <ul ng-show="modules!=null"> <li ng-repeat="min modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 23.
    <ul ng-show="modules!=null"> <li ng-repeat="min modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 24.
    <ul ng-show="modules!=null"> <li ng-repeat="min modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 25.
    <ul ng-show="modules!=null"> <li ng-repeat="min modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 26.
    <ul ng-show="modules!=null"> <li ng-repeat="min modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 27.
    <ul ng-show="modules!=null"> <li ng-repeat="min modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 28.
    <input ng-model="profile.first_name" type="text"required> <input ng-model="profile.surname" type="text" required> <input ng-model="profile.email" type="email" required> <button ng-disabled="!profile.email" ng-click="update()">Update</button>
  • 29.
    <input ng-model="profile.first_name" type="text"required> <input ng-model="profile.surname" type="text" required> <input ng-model="profile.email" type="email" required> <button ng-disabled="!profile.email" ng-click="update()">Update</button>
  • 30.
    <input ng-model="profile.first_name" type="text"required> <input ng-model="profile.surname" type="text" required> <input ng-model="profile.email" type="email" required> <button ng-disabled="!profile.email" ng-click="update()">Update</button>
  • 31.
    <input ng-model="profile.first_name" type="text"required> <input ng-model="profile.surname" type="text" required> <input ng-model="profile.email" type="email" required> <button ng-disabled="!profile.email" ng-click="update()">Update</button>
  • 32.
    QuickTime™ and a 'avc1'decompressor are needed to see this picture.
  • 33.
    Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope,userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 34.
    Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope,userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 35.
    Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope,userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 36.
    Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope,userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 37.
    Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope,userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 38.
    Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope,userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 39.
  • 40.
  • 41.
  • 42.
    Directives <div user="user" textbook="textbook"textbook-player> ... </div> angular.module('talis.directives.player.textbook', []) .directive("textbookPlayer", function() { return { restrict: "A", scope: { user: '=', entity: '=' }, controller: function($scope,textbookSvc) { // textbook logic in here } } });
  • 43.
    Directives <div user="user" textbook="textbook"textbook-player> ... </div> angular.module('talis.directives.player.textbook', []) .directive("textbookPlayer", function() { return { restrict: "A", scope: { user: '=', entity: '=' }, controller: function($scope,textbookSvc) { // textbook logic in here } } });
  • 44.
    Directives <div user="user" textbook="textbook"textbook-player> ... </div> angular.module('talis.directives.player.textbook', []) .directive("textbookPlayer", function() { return { restrict: "A", scope: { user: '=', entity: '=' }, controller: function($scope,textbookSvc) { // textbook logic in here } } });
  • 45.
    Directives <div user="user" textbook="textbook"textbook-player> ... </div> angular.module('talis.directives.player.textbook', []) .directive("textbookPlayer", function() { return { restrict: "A", scope: { user: '=', entity: '=' }, controller: function($scope,textbookSvc) { // textbook logic in here } } });
  • 46.
  • 47.
    Notes From theField Act I: The Basics
  • 48.
    Elem vs. Attrdirectives <textbook-player user="user" textbook="textbook"> ... </textbook-player> <div user="user" textbook="textbook" textbook-player> ... </div>
  • 49.
    Minification angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc){ .. }); angular.module('talis.controllers.user', []) .controller('AccountCtrl',['$scope','userSvc’, function($scope, userSvc) { ... } ]);
  • 50.
    Minification angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc){ .. }); angular.module('talis.controllers.user', []) .controller('AccountCtrl',['$scope','userSvc’, function($scope, userSvc) { ... } ]); a.m('talis.controllers.user', []) .c('AccountCtrl',['$scope','userSvc’, function(s, u) { ... } ]);
  • 51.
    Mongo _id { _id: ObjectId(1234), name:“Jonny Clientside”, age: 24, interests: [‘JQuery’,‘HTML5’ } <a ng-href="#/people/{{ p._id }}">{{p.name}}</a>
  • 52.
    Notes From theField Act II: Advanced
  • 53.
    Angular AppAngular App TypicalMEAN Shape DBDB APIAPI Server side pages Server side pages StaticsStatics JSON JSON HTMLHTML JSON Client side Server side
  • 54.
    Angular AppAngular App JSON 9090 9090 9090 9090 Users API Users API APIAPI Serverside pages Server side pages StaticsStatics JSON JSON JSON Client side Meta API Meta API Files API Files API Anno API Anno API JSON DBDBDBDBDBDB DBDB RedisRedisRedisRedis HTMLHTML JSON
  • 55.
    Logging • A lotof activity in the client side • Some within Express/Node server side • More behind your API proxy
  • 56.
    var loggingModule =angular.module('talis.services.logging', []); loggingModule.factory( "traceService", function(){ return({ print: printStackTrace }); } ); loggingModule.provider( "$exceptionHandler",{ $get: function(exceptionLoggingService){ return(exceptionLoggingService); } } );
  • 57.
    var loggingModule =angular.module('talis.services.logging', []); loggingModule.factory( "traceService", function(){ return({ print: printStackTrace }); } ); loggingModule.provider( "$exceptionHandler",{ $get: function(exceptionLoggingService){ return(exceptionLoggingService); } } );
  • 58.
    loggingModule.factory( "exceptionLoggingService", ["$log","$window", "traceService", function($log, $window,traceService){ function error(exception, cause){ $log.error.apply($log, arguments); try{ var errorMessage = exception.toString(); var stackTrace = traceService.print({e: exception}); $.ajax({ type: "POST", url: "/logger", contentType: "application/json", data: angular.toJson({ url: $window.location.href, message: errorMessage, type: "exception", stackTrace: stackTrace, cause: ( cause || "") }) }); } catch (loggingError){ $log.warn("Error server-side logging failed"); $log.log(loggingError); } } return(error); }] );
  • 60.
  • 61.
    Security • APIs securedwith OAuth 2.0 Bearer tokens • Tokens obtained with a key/secret • If your app is downloaded and run on the client, where do you put the secret?
  • 62.
    Security • Have nodereturn the OAuth token as JSON behind a login barrier • Angular requests this JSON when a route that requires login is first requsted • If status != 200, Angular app redirects browser to login page • User logs in, repeat
  • 63.
    Security • Dealing withtokens on every service call is a PITA • Tokens expiring is normal • Deal with it globally using a couple of advanced $http features
  • 64.
    .run(function($rootScope,$injector) { $injector.get("$http").defaults.transformRequest = function(data,headersGetter) { headersGetter()['Authorization']="Bearer "+$rootScope.token if (data) { return angular.toJson(data); } }; });
  • 65.
    $httpProvider.responseInterceptors.push( function ($rootScope, $q,$injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 66.
    $httpProvider.responseInterceptors.push( function ($rootScope, $q,$injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 67.
    $httpProvider.responseInterceptors.push( function ($rootScope, $q,$injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 68.
    $httpProvider.responseInterceptors.push( function ($rootScope, $q,$injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 69.
    $httpProvider.responseInterceptors.push( function ($rootScope, $q,$injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 70.
    $httpProvider.responseInterceptors.push( function ($rootScope, $q,$injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 71.
    $httpProvider.responseInterceptors.push( function ($rootScope, $q,$injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 72.
    Environments • Pretty usualto deal with prod, dev, testing environment config on the server side • Inject this into your client side app using a dynamic JS include
  • 73.
  • 74.
    Environments angular.module('talis.environment', [], function($provide) constant('API_ENDPOINT','http://localhost:3000'). constant('ACTIVATE_FEATURE_FLIPS',true);
  • 75.
    Environments angular.module('talis.environment', [], function($provide) constant('API_ENDPOINT','https://talis.com'). constant('ACTIVATE_FEATURE_FLIPS',false);
  • 76.
  • 77.
  • 78.
  • 79.
    @talis facebook.com/talisgroup +44 (0) 121374 2740 talis.com info@talis.com 48 Frederick Street Birmingham B1 3HN

Editor's Notes

  • #3 Anyone been playing this?
  • #4 WTF?
  • #5 Check out the score - weird! Back to work…
  • #6 Whole stack is JS, from the DB, the native data format (BSON), the server side and the front end. Even your DB queries are JSON and the DB client for debugging
  • #7 Birmingham EdTech learning platform 65 unis 1 million students 50% UK + other campuses on 3 continents ~15M hits per week page/api Legacy in PHP
  • #11 Node app here.
  • #12 Angular app here
  • #13 Note the node app serves index.html which includes app.js which bootstraps the angular app
  • #14 Some things we are building with MEAN Timeline editing UI
  • #15 Textbooks Copy and paste from OCRed images Rich annotations + notes In-book search
  • #16 Bi directional is AWESOME! Whistle stop tour
  • #17 Bi directional is AWESOME! Whistle stop tour
  • #18 Pretty familiar
  • #19 bind to params in URL
  • #20 our HTML template
  • #21 the controller
  • #22 can add extra properties to the route
  • #33 update() is a method in the CONTROLLER
  • #34 Does this via event loop
  • #38 dependancy injection $scope and userSvc are singletons
  • #40 SERVICES - generally logic for persisting models or other interactions with the server side
  • #41 wrapping behavior and logic to specific tags, independent from view-level controller independent scope, many on same view great for re-use
  • #42 user and textbook passed as parameters from view scope
  • #43 More common to use as attribute name
  • #44 namespace
  • #45 directive function
  • #46 view scope -&amp;gt; directive scope (user/entity)
  • #47 directive controller - $scope is injected by framework
  • #48 If you’re a client side developer then Angular comes as a shock Actually if you’re a server side guy you’ll be more at home Resist JQuery
  • #49 First some common gotchas
  • #50 For IE compat use attribute
  • #51 Dependancy injection Minification will shorten $scope, userSvc Array param will assume last param func, preceeding params strings mapping to vars
  • #52 End result
  • #53 Caught us out moving to 1.2 Angular assumes all _ properties are private PITA in templates Backed out in later versions
  • #54 You can find these on our blog
  • #55 Remember this?
  • #56 Talis App shape - SOA Proxy API CORS ripple of change protection - what version of angular app is it? api versioning
  • #57 Angular has built in $exceptionHandler that will log to console :-/
  • #60 Why are we using $.ajax?
  • #61 Collect more feedback from user Propigate unexpected errors up to $rootScope Form in your master template
  • #62 Logstash/ElasticSearch/Kibana Consider watermarking log statements through stack using an ID
  • #63 https!!
  • #66 transformRequest in your app’s run method adds the token (where available) to every single request made with $http
  • #67 Dealing with token expiry and re-request responseInterceptor
  • #68 Dealing with token expiry and re-request responseInterceptor
  • #69 Dealing with token expiry and re-request responseInterceptor
  • #70 Dealing with token expiry and re-request responseInterceptor
  • #71 Dealing with token expiry and re-request responseInterceptor
  • #72 Dealing with token expiry and re-request responseInterceptor
  • #73 Dealing with token expiry and re-request responseInterceptor
  • #75 In your main page
  • #76 Dynamically generated by a service in node constants available anywhere by dependancy injection
  • #77 in production
  • #78 You can find these on our blog