Design Strategies with AngularJS 1
Design Strategies with AngularJS 1 Somik Raha
Design Strategies with AngularJS Somik Raha Kai Wu 1
Design Strategies with AngularJS Somik Raha Kai Wu 1 #smartorgdev
Rip Van Winkle Slept through the American Revolution Slept off in 2004 and woke up in 2014 2
3
2004… 3
2004… 3
2004… C++ 3
2004… C++ Java 3
2004… C++ Java Microsoft 3
2004… C++ Java Microsoft Sun 3
2004… C++ Java Microsoft Sun #smartorgdev 3
2014. C++ Java Microsoft Sun #smartorgdev 4
2014. Javascript #smartorgdev 4
Backend #smartorgdev 5
Backend Middleware #smartorgdev 5
Backend Middleware Front-end #smartorgdev 5
Backend Middleware Front-end #smartorgdev 5
Backend Middleware Front-end #smartorgdev 6
Super-heroic Javascript framework #smartorgdev 7
Super-heroic Javascript framework #smartorgdev 7
First reaction I don’t get it. Backbone is great for us. #smartorgdev 8
Second reaction Whoa!! #smartorgdev 9
Backbone learning curve AngularJS learning curve Flattens quickly Minimal concepts 10 Service Directives Controllers and views Staircase … !
Backbone learning curve AngularJS learning curve Flattens quickly Minimal concepts 10 Ben Nadel’s blog
Stats comparing Backbone with AngularJS AppStructure Wizard #smartorgdev 11
Stats comparing Backbone with AngularJS AppStructure Wizard #smartorgdev 11
Stats comparing Backbone with AngularJS AppStructure Wizard #smartorgdev 11
Three Kinds of Scenarios Single Page Application Multi-Page Application Legacy Application Classic Sophisticated Convoluted #smartorgdev 12
Multi-Page Application Browser loads entire page e.g. Login e.g. Show Projects Page 1 Page 2 Page 3 View 1 View 2 View 3 Controller 1 Controller 2 Controller 3 Route 1 Route 2 Route 3 /login /showProjects /… Map routes to controllers and views #smartorgdev 13
angular.module('myApp', [ 'ngRoute', … ]).config(function ($routeProvider) { $routeProvider.when('/route1', { controller: 'Controller1 as c1', templateUrl: 'views/view1.html', }).when('/route2', { controller: 'Controller2 as c2', templateUrl: 'views/view2.html', }).otherwise({ redirectTo: '/route1' }); }); Route1 Controller1 View1 Route2 Controller2 View2 Map routes to controllers and views app.js 14
angular.module('myApp', [ 'ngRoute', … ]).config(function ($routeProvider) { $routeProvider.when('/showProjects', { controller: 'ShowProjectsController as show', templateUrl: ‘views/showProjects.html’, }).when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }).otherwise({ redirectTo: '/login' }); }); Routes! http://../login http://../showProjects app.js 15
angular.module('myApp', [ 'ngRoute', … ]).config(function ($routeProvider) { $routeProvider.when('/showProjects', { controller: 'ShowProjectsController as show', templateUrl: ‘views/showProjects.html’, }).when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }).otherwise({ redirectTo: '/login' }); }); Routes! http://../login http://../showProjects app.js 15
when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) Routes! http://../login http://../showProjects app.js 16
! class LoginController { $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
! class LoginController { $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js Pattern ! Controller in Typescript Classes! ! Packages the controller logic and makes it much easier to read and test. when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
! class LoginController { $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
! class LoginController { $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
! class LoginController { $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
! class LoginController { $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
! class LoginController { $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js Optional Pattern ! Named Controller Objects! ! Maintains modularity and avoids having to put data into scope objects, which hold so much other stuff, making it easier to debug. when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
! class LoginController { $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! function LoginController($scope, $location) { $scope.userName = ""; $scope.password = ""; $scope.doLogin = function() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode($scope.userName, $scope.password) .then($scope.onLoginSuccess(response)) } $scope.onLoginSuccess = function(response){ if (response.status) { $location.path("/showProjects"); } else { $location.path("/login"); console.log("Login failed. You cannot proceed.") } } } !! Typescript provides more readable structure More funky All initialization is in constructor Code completion; No $scope Initialization is not clearly demarcated, declarations interspersed with execution ! No code completion 18 Controller in Typescript Classes
19
19
Single-Page Application Rich interactions within a single page, loaded only once Navigation Workspace Actions Menu #smartorgdev 20
#smartorgdev 21
#smartorgdev 21
If we have only one controller and one view, how do we prevent our code from getting bloated? 22
Directives to the rescue index.html <body ng-app="myApp" ng-controller="myController"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body> 23 template.html directive.js
Directives to the rescue index.html <body ng-app="myApp" ng-controller="myController"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body> 23 template.html directive.js Pattern: ! Decompose view with directives
Directives to the rescue index.html <body ng-app="myApp" ng-controller="myController"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body> 23 template.html directive.js
But wait, how do we pass data between directives? Navigation Workspace Actions Menu e.g. How does the Navigation directive find out about a selection in the Menu directive? #smartorgdev 24
But wait, how do we pass data between directives? Navigation Workspace Actions Menu e.g. list of trees e.g. How does the Navigation directive find out which tree has been selected in the Menu directive? #smartorgdev 25
myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); But wait, how does scope.onSelectTree work correctly when called in the menu directive? 26
index.html menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 27 <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body>
index.html menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 27 <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body>
index.html menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 27 <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body>
<div top-menu-view menu-data="menuData" on-select-tree="onSelectTree(treeID)"></div> menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 28 index.html <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body>
<div top-menu-view menu-data="menuData" on-select-tree="onSelectTree(treeID)"></div> menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 28 index.html <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body>
index.html <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> <div top-menu-view menu-data="menuData" on-select-tree="onSelectTree(treeID)"></div> menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 28 </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> Pattern: ! Pass Controller Functions as Directive Attributes </div> </div> </div> … </body>
Legacy Application Non-JS MVC model and cannot make direct web calls from JS to your application Generate HTML stubs that invoke “widgets” using directives ! #smartorgdev 29
30
30
In your legacy web application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! 31
In your legacy web application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! 31
In your legacy web application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! 31
In your legacy web application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller widget container and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! 31
In your legacy web application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller widget container and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! 31
In your legacy web application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller widget container and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! 31
In your legacy web application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller widget container and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! 31
In your legacy web application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller widget container and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! Pattern: ! Dynamically invoke angular application 31
angular.module('myApp')! .controller('MyWidgetCtrl', function($scope) {! $scope.setupData = function(myWidgetConfig:MyWidgetConfig) {! if (!$scope.inputData && !myWidgetConfig) {! addTablesConfig = window.standalone.myWidgetConfig;! } else if ($scope.inputData && !myWidgetConfig) {! return;! }! $scope.inputData = myWidgetConfig.inputData;! };! })! .directive('mywidgetDirective', function() {! var templateUrl = '/path/to/template.html';! if (!window.production) {! templateUrl = 'localpath/to/template.html'! }! return {! restrict: 'A',! templateUrl: templateUrl,! scope: {! inputData: '=',! setupData: '&'! },! link: function (scope, elem, attrs, ctrl) {! }! }! });! ! Data made available to directive scope standalone.js 32
angular.module('myApp')! .controller('MyWidgetCtrl', function($scope) {! $scope.setupData = function(myWidgetConfig:MyWidgetConfig) {! if (!$scope.inputData && !myWidgetConfig) {! addTablesConfig = window.standalone.myWidgetConfig;! } else if ($scope.inputData && !myWidgetConfig) {! return;! }! $scope.inputData = myWidgetConfig.inputData;! };! })! .directive('mywidgetDirective', function() {! var templateUrl = '/path/to/template.html';! if (!window.production) {! templateUrl = 'localpath/to/template.html'! }! return {! restrict: 'A',! templateUrl: templateUrl,! scope: {! inputData: '=',! setupData: '&'! },! link: function (scope, elem, attrs, ctrl) {! }! }! });! ! Data made available to directive scope standalone.js 32
angular.module('myApp')! .controller('MyWidgetCtrl', function($scope) {! $scope.setupData = function(myWidgetConfig:MyWidgetConfig) {! if (!$scope.inputData && !myWidgetConfig) {! addTablesConfig = window.standalone.myWidgetConfig;! } else if ($scope.inputData && !myWidgetConfig) {! return;! }! $scope.inputData = myWidgetConfig.inputData;! };! })! .directive('mywidgetDirective', function() {! var templateUrl = '/path/to/template.html';! if (!window.production) {! templateUrl = 'localpath/to/template.html'! }! return {! restrict: 'A',! templateUrl: templateUrl,! scope: {! inputData: '=',! setupData: '&'! },! link: function (scope, elem, attrs, ctrl) {! }! }! });! ! Data made available to directive scope standalone.js 32
angular.module('myApp')! .controller('MyWidgetCtrl', function($scope) {! $scope.setupData = function(myWidgetConfig:MyWidgetConfig) {! if (!$scope.inputData && !myWidgetConfig) {! addTablesConfig = window.standalone.myWidgetConfig;! } else if ($scope.inputData && !myWidgetConfig) {! return;! }! $scope.inputData = myWidgetConfig.inputData;! };! })! .directive('mywidgetDirective', function() {! var templateUrl = '/path/to/template.html';! if (!window.production) {! Allows for standalone templateUrl = 'localpath/to/template.html'! }! return {! restrict: 'A',! templateUrl: templateUrl,! scope: {! inputData: '=',! setupData: '&'! },! link: function (scope, elem, attrs, ctrl) {! }! }! });! ! Data made available to directive scope standalone.js 32 widget testing window.standalone = {! myWidgetConfig: {! inputData: …! }! }! ! standalone index.html
angular.module('myApp')! .controller('MyWidgetCtrl', function($scope) {! $scope.setupData = function(myWidgetConfig:MyWidgetConfig) {! if (!$scope.inputData && !myWidgetConfig) {! addTablesConfig = window.standalone.myWidgetConfig;! } else if ($scope.inputData && !myWidgetConfig) {! return;! }! $scope.inputData = myWidgetConfig.inputData;! };! })! .directive('mywidgetDirective', function() {! var templateUrl = '/path/to/template.html';! if (!window.production) {! Allows for standalone templateUrl = 'localpath/to/template.html'! }! return {! restrict: 'A',! templateUrl: templateUrl,! scope: {! inputData: '=',! setupData: '&'! },! link: function (scope, elem, attrs, ctrl) {! }! }! });! ! Data made available to directive scope standalone.js Pattern: ! Standalone Widget Mode 32 widget testing window.standalone = {! myWidgetConfig: {! inputData: …! }! }! ! standalone index.html
Testing notes Avoid putting business logic in controllers ! Put them in Typescript classes that can be independently tested ! Use HttpMocks to ensure controllers work fine #smartorgdev 33
Old ideas like modularity, once-and-only-once, etc. still apply ! They just look different #smartorgdev 34
Get expert help 35
Join us tomorrow for a Test-Driven Development session ! 10:45 AM Check us out at http://rangal.com and http://smartorg.com #smartorgdev 36
Open House! ! Pattern Summary! ! Controller in Typescript classes (17) Named Controller objects (17) ! Decompose View with Directives (22) ! Pass Controller Functions as Directive Attributes (28) ! Dynamically Invoke Angular Application (31) ! Standalone Widget Mode (32) ! 37 ! Questions for reflection ! What’s your AngularJS adoption experience? ! What have you learned? Write to us! Somik Raha: sraha@smartorg.com Kai Wu: kwu@smartorg.com #smartorgdev

Design strategies for AngularJS

  • 1.
  • 2.
    Design Strategies with AngularJS 1 Somik Raha
  • 3.
    Design Strategies with AngularJS Somik Raha Kai Wu 1
  • 4.
    Design Strategies with AngularJS Somik Raha Kai Wu 1 #smartorgdev
  • 5.
    Rip Van Winkle Slept through the American Revolution Slept off in 2004 and woke up in 2014 2
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
    2004… C++ Java Microsoft 3
  • 12.
    2004… C++ Java Microsoft Sun 3
  • 13.
    2004… C++ Java Microsoft Sun #smartorgdev 3
  • 14.
    2014. C++ Java Microsoft Sun #smartorgdev 4
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
    First reaction Idon’t get it. Backbone is great for us. #smartorgdev 8
  • 24.
    Second reaction Whoa!! #smartorgdev 9
  • 25.
    Backbone learning curveAngularJS learning curve Flattens quickly Minimal concepts 10 Service Directives Controllers and views Staircase … !
  • 26.
    Backbone learning curveAngularJS learning curve Flattens quickly Minimal concepts 10 Ben Nadel’s blog
  • 27.
    Stats comparing Backbone with AngularJS AppStructure Wizard #smartorgdev 11
  • 28.
    Stats comparing Backbone with AngularJS AppStructure Wizard #smartorgdev 11
  • 29.
    Stats comparing Backbone with AngularJS AppStructure Wizard #smartorgdev 11
  • 30.
    Three Kinds ofScenarios Single Page Application Multi-Page Application Legacy Application Classic Sophisticated Convoluted #smartorgdev 12
  • 31.
    Multi-Page Application Browserloads entire page e.g. Login e.g. Show Projects Page 1 Page 2 Page 3 View 1 View 2 View 3 Controller 1 Controller 2 Controller 3 Route 1 Route 2 Route 3 /login /showProjects /… Map routes to controllers and views #smartorgdev 13
  • 32.
    angular.module('myApp', [ 'ngRoute', … ]).config(function ($routeProvider) { $routeProvider.when('/route1', { controller: 'Controller1 as c1', templateUrl: 'views/view1.html', }).when('/route2', { controller: 'Controller2 as c2', templateUrl: 'views/view2.html', }).otherwise({ redirectTo: '/route1' }); }); Route1 Controller1 View1 Route2 Controller2 View2 Map routes to controllers and views app.js 14
  • 33.
    angular.module('myApp', [ 'ngRoute', … ]).config(function ($routeProvider) { $routeProvider.when('/showProjects', { controller: 'ShowProjectsController as show', templateUrl: ‘views/showProjects.html’, }).when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }).otherwise({ redirectTo: '/login' }); }); Routes! http://../login http://../showProjects app.js 15
  • 34.
    angular.module('myApp', [ 'ngRoute', … ]).config(function ($routeProvider) { $routeProvider.when('/showProjects', { controller: 'ShowProjectsController as show', templateUrl: ‘views/showProjects.html’, }).when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }).otherwise({ redirectTo: '/login' }); }); Routes! http://../login http://../showProjects app.js 15
  • 35.
    when('/login', { controller:'LoginController as login', templateUrl: 'views/login.html', }) Routes! http://../login http://../showProjects app.js 16
  • 36.
    ! class LoginController{ $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
  • 37.
    ! class LoginController{ $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js Pattern ! Controller in Typescript Classes! ! Packages the controller logic and makes it much easier to read and test. when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
  • 38.
    ! class LoginController{ $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
  • 39.
    ! class LoginController{ $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
  • 40.
    ! class LoginController{ $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
  • 41.
    ! class LoginController{ $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
  • 42.
    ! class LoginController{ $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! <form name="loginForm" class="navbar-form navbar-left form-signin"> <input ng-model="login.userName" class="form-control" placeholder="User Name" required autofocus> <input ng-model="login.password" type="password" class="form-control" placeholder="Password" required> <button ng-click="login.doLogin()" class="btn btn-primary" type="submit">Sign in</button> </form> login.html TypeScript Simple OO Code AngularJS Creates an object in app.js app.js Optional Pattern ! Named Controller Objects! ! Maintains modularity and avoids having to put data into scope objects, which hold so much other stuff, making it easier to debug. when('/login', { controller: 'LoginController as login', templateUrl: 'views/login.html', }) 17
  • 43.
    ! class LoginController{ $location: ng.ILocationService; password: string; userName: string; constructor($location:ng.ILocationService) { this.$location = $location; this.userName = ""; this.password = ""; } doLogin() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode(this.userName, this.password) .then((response) => this.onLoginSuccess(response)) } onLoginSuccess(response){ if (response.status) { this.$location.path("/showProjects"); } else { this.$location.path("/login"); console.log("Login failed. You cannot proceed.") } } } ! ! function LoginController($scope, $location) { $scope.userName = ""; $scope.password = ""; $scope.doLogin = function() { // Call your login code with a call back to loginSuccessFn callSomeAuthCode($scope.userName, $scope.password) .then($scope.onLoginSuccess(response)) } $scope.onLoginSuccess = function(response){ if (response.status) { $location.path("/showProjects"); } else { $location.path("/login"); console.log("Login failed. You cannot proceed.") } } } !! Typescript provides more readable structure More funky All initialization is in constructor Code completion; No $scope Initialization is not clearly demarcated, declarations interspersed with execution ! No code completion 18 Controller in Typescript Classes
  • 44.
  • 45.
  • 46.
    Single-Page Application Richinteractions within a single page, loaded only once Navigation Workspace Actions Menu #smartorgdev 20
  • 47.
  • 48.
  • 49.
    If we haveonly one controller and one view, how do we prevent our code from getting bloated? 22
  • 50.
    Directives to therescue index.html <body ng-app="myApp" ng-controller="myController"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body> 23 template.html directive.js
  • 51.
    Directives to therescue index.html <body ng-app="myApp" ng-controller="myController"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body> 23 template.html directive.js Pattern: ! Decompose view with directives
  • 52.
    Directives to therescue index.html <body ng-app="myApp" ng-controller="myController"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body> 23 template.html directive.js
  • 53.
    But wait, howdo we pass data between directives? Navigation Workspace Actions Menu e.g. How does the Navigation directive find out about a selection in the Menu directive? #smartorgdev 24
  • 54.
    But wait, howdo we pass data between directives? Navigation Workspace Actions Menu e.g. list of trees e.g. How does the Navigation directive find out which tree has been selected in the Menu directive? #smartorgdev 25
  • 55.
    myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
  • 56.
    myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
  • 57.
    myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
  • 58.
    myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
  • 59.
    myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
  • 60.
    myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 26
  • 61.
    myController angular.module('myApp') .controller('myController', function ($scope, $route, $routeParams, $location) { $scope.action = 'login'; $scope.$on("$routeChangeSuccess", function( $currentRoute, $previousRoute ) { $scope.action = $route.current.action; }); $scope.login = function() { // do login, and on success: $location.path('/home'); } $scope.onSelectTree = function(treeID) { // go fetch the tree using the treeID, … // myTree holds the tree $scope.$broadcast("treeLoaded", { "myTree": myTree }); } }); navigation directive angular.module('myApp') .directive("navigation", function() { return { restrict: 'A', scope: { myTree: '=' } }, templateUrl: '...', link: function(scope) { scope.$on('treeLoaded', function() { makeTreeWith(scope.myTree); //.. } } }); menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); But wait, how does scope.onSelectTree work correctly when called in the menu directive? 26
  • 62.
    index.html menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 27 <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body>
  • 63.
    index.html menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 27 <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body>
  • 64.
    index.html menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 27 <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> <div top-menu-view …></div> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body>
  • 65.
    <div top-menu-view menu-data="menuData"on-select-tree="onSelectTree(treeID)"></div> menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 28 index.html <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body>
  • 66.
    <div top-menu-view menu-data="menuData"on-select-tree="onSelectTree(treeID)"></div> menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 28 index.html <body ng-app="myApp" ng-controller="myController"> <div id="homescope"> <div class="row"> </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> </div> </div> </div> … </body>
  • 67.
    index.html <body ng-app="myApp"ng-controller="myController"> <div id="homescope"> <div class="row"> <div top-menu-view menu-data="menuData" on-select-tree="onSelectTree(treeID)"></div> menu directive angular.module('myApp') .directive("topMenuView", function() { return { restrict: 'A', scope: { menuData: '=', onSelectTree: '&' } }, templateUrl: '...', link: function(scope) { scope.chooseTree = function(tree){ scope.onSelectTree({treeID: tree.id}); }; } }); 28 </div> <div class="row"> <div id="leftPanel"> <div nav-widget …></div> </div> <div id="middlePanel"> <div workspace-contents …></div> </div> <div id="rightPanel" > <div action-menu …></div> Pattern: ! Pass Controller Functions as Directive Attributes </div> </div> </div> … </body>
  • 68.
    Legacy Application Non-JSMVC model and cannot make direct web calls from JS to your application Generate HTML stubs that invoke “widgets” using directives ! #smartorgdev 29
  • 69.
  • 70.
  • 71.
    In your legacyweb application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! 31
  • 72.
    In your legacyweb application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! 31
  • 73.
    In your legacyweb application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! 31
  • 74.
    In your legacyweb application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller widget container and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! 31
  • 75.
    In your legacyweb application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller widget container and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! 31
  • 76.
    In your legacyweb application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller widget container and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! 31
  • 77.
    In your legacyweb application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller widget container and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! 31
  • 78.
    In your legacyweb application, render this html: <div id="mywidgetOuterDiv">! <div id="mywidget" class="" ng-controller="MyWidgetCtrl">! <div mywidget-directive! input-data="inputData" ! setup-data="setupData()">! </div> ! </div>! </div>! ! widget directive In your JS code, bootstrap your widget: angular.bootstrap($('#mywidgetOuterDiv'), ['myApp']);! var scope = $('#mywidget').scope();! scope.setupData(APP.myWidgetConfig);! ! widget controller widget container and this JS: jsCode = "APP.myWidgetConfig = { inputData: {…} }"! Pattern: ! Dynamically invoke angular application 31
  • 79.
    angular.module('myApp')! .controller('MyWidgetCtrl', function($scope){! $scope.setupData = function(myWidgetConfig:MyWidgetConfig) {! if (!$scope.inputData && !myWidgetConfig) {! addTablesConfig = window.standalone.myWidgetConfig;! } else if ($scope.inputData && !myWidgetConfig) {! return;! }! $scope.inputData = myWidgetConfig.inputData;! };! })! .directive('mywidgetDirective', function() {! var templateUrl = '/path/to/template.html';! if (!window.production) {! templateUrl = 'localpath/to/template.html'! }! return {! restrict: 'A',! templateUrl: templateUrl,! scope: {! inputData: '=',! setupData: '&'! },! link: function (scope, elem, attrs, ctrl) {! }! }! });! ! Data made available to directive scope standalone.js 32
  • 80.
    angular.module('myApp')! .controller('MyWidgetCtrl', function($scope){! $scope.setupData = function(myWidgetConfig:MyWidgetConfig) {! if (!$scope.inputData && !myWidgetConfig) {! addTablesConfig = window.standalone.myWidgetConfig;! } else if ($scope.inputData && !myWidgetConfig) {! return;! }! $scope.inputData = myWidgetConfig.inputData;! };! })! .directive('mywidgetDirective', function() {! var templateUrl = '/path/to/template.html';! if (!window.production) {! templateUrl = 'localpath/to/template.html'! }! return {! restrict: 'A',! templateUrl: templateUrl,! scope: {! inputData: '=',! setupData: '&'! },! link: function (scope, elem, attrs, ctrl) {! }! }! });! ! Data made available to directive scope standalone.js 32
  • 81.
    angular.module('myApp')! .controller('MyWidgetCtrl', function($scope){! $scope.setupData = function(myWidgetConfig:MyWidgetConfig) {! if (!$scope.inputData && !myWidgetConfig) {! addTablesConfig = window.standalone.myWidgetConfig;! } else if ($scope.inputData && !myWidgetConfig) {! return;! }! $scope.inputData = myWidgetConfig.inputData;! };! })! .directive('mywidgetDirective', function() {! var templateUrl = '/path/to/template.html';! if (!window.production) {! templateUrl = 'localpath/to/template.html'! }! return {! restrict: 'A',! templateUrl: templateUrl,! scope: {! inputData: '=',! setupData: '&'! },! link: function (scope, elem, attrs, ctrl) {! }! }! });! ! Data made available to directive scope standalone.js 32
  • 82.
    angular.module('myApp')! .controller('MyWidgetCtrl', function($scope){! $scope.setupData = function(myWidgetConfig:MyWidgetConfig) {! if (!$scope.inputData && !myWidgetConfig) {! addTablesConfig = window.standalone.myWidgetConfig;! } else if ($scope.inputData && !myWidgetConfig) {! return;! }! $scope.inputData = myWidgetConfig.inputData;! };! })! .directive('mywidgetDirective', function() {! var templateUrl = '/path/to/template.html';! if (!window.production) {! Allows for standalone templateUrl = 'localpath/to/template.html'! }! return {! restrict: 'A',! templateUrl: templateUrl,! scope: {! inputData: '=',! setupData: '&'! },! link: function (scope, elem, attrs, ctrl) {! }! }! });! ! Data made available to directive scope standalone.js 32 widget testing window.standalone = {! myWidgetConfig: {! inputData: …! }! }! ! standalone index.html
  • 83.
    angular.module('myApp')! .controller('MyWidgetCtrl', function($scope){! $scope.setupData = function(myWidgetConfig:MyWidgetConfig) {! if (!$scope.inputData && !myWidgetConfig) {! addTablesConfig = window.standalone.myWidgetConfig;! } else if ($scope.inputData && !myWidgetConfig) {! return;! }! $scope.inputData = myWidgetConfig.inputData;! };! })! .directive('mywidgetDirective', function() {! var templateUrl = '/path/to/template.html';! if (!window.production) {! Allows for standalone templateUrl = 'localpath/to/template.html'! }! return {! restrict: 'A',! templateUrl: templateUrl,! scope: {! inputData: '=',! setupData: '&'! },! link: function (scope, elem, attrs, ctrl) {! }! }! });! ! Data made available to directive scope standalone.js Pattern: ! Standalone Widget Mode 32 widget testing window.standalone = {! myWidgetConfig: {! inputData: …! }! }! ! standalone index.html
  • 84.
    Testing notes Avoidputting business logic in controllers ! Put them in Typescript classes that can be independently tested ! Use HttpMocks to ensure controllers work fine #smartorgdev 33
  • 85.
    Old ideas likemodularity, once-and-only-once, etc. still apply ! They just look different #smartorgdev 34
  • 86.
  • 87.
    Join us tomorrowfor a Test-Driven Development session ! 10:45 AM Check us out at http://rangal.com and http://smartorg.com #smartorgdev 36
  • 88.
    Open House! ! Pattern Summary! ! Controller in Typescript classes (17) Named Controller objects (17) ! Decompose View with Directives (22) ! Pass Controller Functions as Directive Attributes (28) ! Dynamically Invoke Angular Application (31) ! Standalone Widget Mode (32) ! 37 ! Questions for reflection ! What’s your AngularJS adoption experience? ! What have you learned? Write to us! Somik Raha: sraha@smartorg.com Kai Wu: kwu@smartorg.com #smartorgdev