ConnectJS Orchestrating Apps by Composing Angular Directives

ConnectJS Orchestrating Apps by Composing Angular Directives

94bd558238b69c45d3d3e15797ae94f7?s=128

Jeremy Fairbank

October 16, 2015
Tweet

Transcript

  1. Orchestrating Apps by Composing Angular Directives Jeremy Fairbank jeremyfairbank.com @elpapapollo

  2. Hi, I’m Jeremy jfairbank @elpapapollo pushagency.io blog.jeremyfairbank.com simplybuilt.com

  3. Building applications is hard

  4. Who is software for?

  5. Who is software for? • Computers • Users • Programmers

    • Me • You
  6. Competing Goals

  7. Competing Goals • Performance • Maintainability • Shipping code •

    Readability • Scalability
  8. Competing Goals • Performance • Maintainability • Shipping code •

    Readability • Scalability
  9. Patterns make it “easier”

  10. Why not use them with JavaScript frameworks?

  11. Composition

  12. What is composition?

  13. What is composition?

  14. What is composition?

  15. – Wikipedia “The various visual elements, known as elements of

    design, formal elements, or elements of art, are the vocabulary with which the visual artist composes. These elements in the overall design usually relate to each other and to the whole art work.” What is composition?
  16. – Wikipedia “The way in which something is put together

    or arranged. The combination of parts or elements that make up something.” What is composition?
  17. Composition Create with small independent pieces.

  18. Composition Embodiment of many design principles.

  19. Design Principles

  20. Design Principles • Modularity • Separation of concerns (SOC) •

    Loose coupling • High cohesion • Composition over inheritance • Ad nauseam…
  21. Directives Review

  22. Directives Review • Attach behavior to the DOM via attributes,

    elements, comments, or CSS classes • Componentize functionality with HTML • Create custom HTML elements
  23. Directives Review • Attach behavior to the DOM via attributes,

    elements, comments, or CSS classes • Componentize functionality with HTML • Create custom HTML elements <my-hello-world lang="en"></my-hello-world> <name-tag name="Jeremy"></name-tag>
  24. Directives Review angular.module('myApp', []) .directive('nameTag', function() { return { restrict:

    'E', scope: { name: '@' }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; });
  25. Directives Review angular.module('myApp', []) .directive('nameTag', function() { return { restrict:

    'E', scope: { name: '@' }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; });
  26. Directives Review angular.module('myApp', []) .directive('nameTag', function() { return { restrict:

    'E', scope: { name: '@' }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; });
  27. Directives Review angular.module('myApp', []) .directive('nameTag', function() { return { restrict:

    'E', scope: { name: '@' }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; });
  28. Directives Review <p> <name-tag name="Jeremy Fairbank"></name-tag> <!-- Hello, my name

    is Jeremy Fairbank --> </p> <p> <name-tag></name-tag> <!-- Hello, my name is Joe Schmoe --> </p>
  29. Directives Review <p> <name-tag name="Jeremy Fairbank"></name-tag> <!-- Hello, my name

    is Jeremy Fairbank --> </p> <p> <name-tag></name-tag> <!-- Hello, my name is Joe Schmoe --> </p>
  30. Directives Review <p> <name-tag name="Jeremy Fairbank"></name-tag> <!-- Hello, my name

    is Jeremy Fairbank --> </p> <p> <name-tag></name-tag> <!-- Hello, my name is Joe Schmoe --> </p>
  31. Directives Review return { restrict: 'E', scope: { name: '@'

    }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } };
  32. Directives Review return { restrict: 'E', scope: { name: '@'

    }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; Restrict to element declaration (i.e. `<name-tag></name-tag>`) E - element A - attribute C - class name
  33. Directives Review return { restrict: 'E', scope: { name: '@'

    }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; Isolate scope @ - attributes = - bindings & - behavior/callbacks
  34. Directives Review return { restrict: 'E', scope: { name: '@'

    }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; Templates template - literal template templateUrl - url or script id for template
  35. Directives Review return { restrict: 'E', scope: { name: '@'

    }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; link manipulate DOM or set some defaults on scope controller add to scope too, preferably for exposing an API Set up
  36. Directives Review return { restrict: 'E', scope: { name: '@'

    }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; <p> <name-tag name="Jeremy Fairbank"></name-tag> <!-- Hello, my name is Jeremy Fairbank --> </p> <p> <name-tag></name-tag> <!-- Hello, my name is Joe Schmoe --> </p>
  37. Directives Review return { restrict: 'E', scope: { name: '@'

    }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; <p> <name-tag name="Jeremy Fairbank"></name-tag> <!-- Hello, my name is Jeremy Fairbank --> </p> <p> <name-tag></name-tag> <!-- Hello, my name is Joe Schmoe --> </p> Supplying the attribute value
  38. return { restrict: 'E', scope: { name: '@' }, template:

    'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } }; <p> <name-tag name="Jeremy Fairbank"></name-tag> <!-- Hello, my name is Jeremy Fairbank --> </p> <p> <name-tag></name-tag> <!-- Hello, my name is Joe Schmoe --> </p> Directives Review Using the default value
  39. Composition and Angular

  40. Composition and Angular • Focus on smaller problems to solve

    a large problem • Apply design principles to directives • Build app in terms of directives • Decompose large directives into small reusable directives • Refactoring and adding new features becomes more manageable
  41. But how do we fit directives together?

  42. Data Flow

  43. Data Flow App Directive Directive Directive Directive Directive Directive Directive

    Directive Directive
  44. Data Flow • Connecting directives to build an “app” through

    the flow of data • Handling model data among directives • Many approaches with pros and cons App Directive Directive Directive Directive Directive Directive Directive Directive Directive
  45. Data Flow 1. Shared/global state with prototypal scoping 2. Exporting

    API’s with directive controllers 3. Isolate scope bindings and callbacks 4. Event broadcasting and emitting App Directive Directive Directive Directive Directive Directive Directive Directive Directive
  46. Prototypal Scoping

  47. Prototypal Scoping app.directive('imageGallery', function() { return { scope: true, templateUrl:

    'image-gallery.html' }; });
  48. Prototypal Scoping app.directive('imageGallery', function() { return { scope: true, templateUrl:

    'image-gallery.html' }; }); Create a child scope that “inherits” from parent scope
  49. Prototypal Scoping app.controller('ImageCtrl', function($scope) { function createImage(src, text) { return

    { src: src, text: text, favorited: false }; } $scope.mainImage = {}; $scope.imageFavorites = []; $scope.images = [ createImage('/images/cat-1.jpg', 'Cat 1'), createImage('/images/cat-2.jpg', 'Cat 2'), createImage('/images/cat-3.jpg', 'Cat 3'), createImage('/images/cat-4.jpg', 'Cat 4') ]; });
  50. Prototypal Scoping app.controller('ImageCtrl', function($scope) { function createImage(src, text) { return

    { src: src, text: text, favorited: false }; } $scope.mainImage = {}; $scope.imageFavorites = []; $scope.images = [ createImage('/images/cat-1.jpg', 'Cat 1'), createImage('/images/cat-2.jpg', 'Cat 2'), createImage('/images/cat-3.jpg', 'Cat 3'), createImage('/images/cat-4.jpg', 'Cat 4') ]; });
  51. Prototypal Scoping app.controller('ImageCtrl', function($scope) { function createImage(src, text) { return

    { src: src, text: text, favorited: false }; } $scope.mainImage = {}; $scope.imageFavorites = []; $scope.images = [ createImage('/images/cat-1.jpg', 'Cat 1'), createImage('/images/cat-2.jpg', 'Cat 2'), createImage('/images/cat-3.jpg', 'Cat 3'), createImage('/images/cat-4.jpg', 'Cat 4') ]; });
  52. Prototypal Scoping <script id="image-gallery.html" type="text/ng-template"> <main-image></main-image> <image-list></image-list> <image-favorites></image-favorites> </script> <body

    ng-controller="ImageCtrl"> <image-gallery></image-gallery> </body>
  53. Prototypal Scoping <script id="image-gallery.html" type="text/ng-template"> <main-image></main-image> <image-list></image-list> <image-favorites></image-favorites> </script> <body

    ng-controller="ImageCtrl"> <image-gallery></image-gallery> </body>
  54. Prototypal Scoping <script id="image-gallery.html" type="text/ng-template"> <main-image></main-image> <image-list></image-list> <image-favorites></image-favorites> </script> <body

    ng-controller="ImageCtrl"> <image-gallery></image-gallery> </body>
  55. Prototypal Scoping <script id="main-image.html" type="text/ng-template"> <h3>Main Image</h3> <img width="160" ng-src="{{mainImage.src}}">

    </script>
  56. Prototypal Scoping <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image in

    images"> <div ng-click="favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="setMainImage(image)"> Set Main Image </button> </div> </script>
  57. Prototypal Scoping <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image in

    images"> <div ng-click="favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="setMainImage(image)"> Set Main Image </button> </div> </script>
  58. Prototypal Scoping <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image in

    images"> <div ng-click="favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="setMainImage(image)"> Set Main Image </button> </div> </script>
  59. Prototypal Scoping <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image in

    images"> <div ng-click="favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="setMainImage(image)"> Set Main Image </button> </div> </script>
  60. Prototypal Scoping <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <h4>Favorited: {{imageFavorites.length}}</h4> <ul> <li

    ng-repeat="image in imageFavorites"> <p> <img ng-src="{{image.src}}" width="40"> {{image.text}} <button ng-click="unfavorite(image)"> Unfavorite </button> </p> </li> </ul> </script>
  61. Prototypal Scoping <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <h4>Favorited: {{imageFavorites.length}}</h4> <ul> <li

    ng-repeat="image in imageFavorites"> <p> <img ng-src="{{image.src}}" width="40"> {{image.text}} <button ng-click="unfavorite(image)"> Unfavorite </button> </p> </li> </ul> </script>
  62. Prototypal Scoping <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <h4>Favorited: {{imageFavorites.length}}</h4> <ul> <li

    ng-repeat="image in imageFavorites"> <p> <img ng-src="{{image.src}}" width="40"> {{image.text}} <button ng-click="unfavorite(image)"> Unfavorite </button> </p> </li> </ul> </script>
  63. Prototypal Scoping app.directive('mainImage', function() { return { scope: true, templateUrl:

    'image-gallery.html' }; });
  64. Prototypal Scoping app.directive('imageList', function() { return { scope: true, templateUrl:

    'image-list.html', link: function(scope) { scope.setMainImage = function(image) { scope.mainImage.src = image.src; }; scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.imageFavorites.push(image); } }; } }; });
  65. Prototypal Scoping app.directive('imageFavorites', function() { return { scope: true, templateUrl:

    'image-favorites.html', link: function(scope) { scope.unfavorite = function(image) { var i = scope.imageFavorites.indexOf(image); if (i > -1) { image.favorited = false; scope.imageFavorites.splice(i, 1); } }; } }; });
  66. Prototypal Scoping app.directive('imageFavorites', function() { return { scope: true, templateUrl:

    'image-favorites.html', link: function(scope) { scope.unfavorite = function(image) { var i = scope.imageFavorites.indexOf(image); if (i > -1) { image.favorited = false; scope.imageFavorites.splice(i, 1); } }; } }; });
  67. Prototypal Scoping Demo

  68. Prototypal Scoping What about more than one gallery? <body ng-controller="ImageCtrl">

    <div ng-controller="CatImageCtrl"> <image-gallery></image-gallery> </div> <div ng-controller="NatureImageCtrl"> <image-gallery></image-gallery> </div> </body>
  69. Prototypal Scoping What about more than one gallery? <body ng-controller="ImageCtrl">

    <div ng-controller="CatImageCtrl"> <image-gallery></image-gallery> </div> <div ng-controller="NatureImageCtrl"> <image-gallery></image-gallery> </div> </body>
  70. Prototypal Scoping app.controller('CatImageCtrl', function($scope) { $scope.images = [ createImage('/images/cat-1.jpg', 'Cat

    1'), createImage('/images/cat-2.jpg', 'Cat 2'), createImage('/images/cat-3.jpg', 'Cat 3'), createImage('/images/cat-4.jpg', 'Cat 4') ]; }).controller('NatureImageCtrl', function($scope) { $scope.images = [ createImage('/images/nature-1.jpg', 'Nature 1'), createImage('/images/nature-2.jpg', 'Nature 2'), createImage('/images/nature-3.jpg', 'Nature 3'), createImage('/images/nature-4.jpg', 'Nature 4') ]; });
  71. Prototypal Scoping Demo

  72. Prototypal Scoping What’s the problem?

  73. Prototypal Scoping Object properties on prototypes are problematic

  74. app.controller('ImageCtrl', function($scope) { // ... $scope.mainImage = {}; $scope.imageFavorites =

    []; }); app.directive('imageList', function() { return { // ... link: function(scope) { scope.setMainImage = function(image) { scope.mainImage.src = image.src; }; scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.imageFavorites.push(image); } }; } }; });
  75. app.controller('ImageCtrl', function($scope) { // ... $scope.mainImage = {}; $scope.imageFavorites =

    []; }); app.directive('imageList', function() { return { // ... link: function(scope) { scope.setMainImage = function(image) { scope.mainImage.src = image.src; }; scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.imageFavorites.push(image); } }; } }; });
  76. app.controller('ImageCtrl', function($scope) { // ... $scope.mainImage = {}; $scope.imageFavorites =

    []; }); app.directive('imageList', function() { return { // ... link: function(scope) { scope.setMainImage = function(image) { scope.mainImage.src = image.src; }; scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.imageFavorites.push(image); } }; } }; });
  77. app.controller('CatImageCtrl', function($scope) { $scope.mainImage = {}; $scope.imageFavorites = []; $scope.images

    = [ createImage('/images/cat-1.jpg', 'Cat 1'), createImage('/images/cat-2.jpg', 'Cat 2'), createImage('/images/cat-3.jpg', 'Cat 3'), createImage('/images/cat-4.jpg', 'Cat 4') ]; }).controller('NatureImageCtrl', function($scope) { $scope.mainImage = {}; $scope.imageFavorites = []; $scope.images = [ createImage('/images/nature-1.jpg', 'Nature 1'), createImage('/images/nature-2.jpg', 'Nature 2'), createImage('/images/nature-3.jpg', 'Nature 3'), createImage('/images/nature-4.jpg', 'Nature 4') ]; });
  78. app.controller('CatImageCtrl', function($scope) { $scope.mainImage = {}; $scope.imageFavorites = []; $scope.images

    = [ createImage('/images/cat-1.jpg', 'Cat 1'), createImage('/images/cat-2.jpg', 'Cat 2'), createImage('/images/cat-3.jpg', 'Cat 3'), createImage('/images/cat-4.jpg', 'Cat 4') ]; }).controller('NatureImageCtrl', function($scope) { $scope.mainImage = {}; $scope.imageFavorites = []; $scope.images = [ createImage('/images/nature-1.jpg', 'Nature 1'), createImage('/images/nature-2.jpg', 'Nature 2'), createImage('/images/nature-3.jpg', 'Nature 3'), createImage('/images/nature-4.jpg', 'Nature 4') ]; });
  79. Prototypal Scoping Fixed Demo

  80. Prototypal Scoping • Reusable functions • Perfect when sharing data

    is desired • Shared data is problematic when reusing directive • Fixing shared data issues duplicates code • Assumptions about parent scopes
  81. Requiring Directive Controllers

  82. Requiring Directive Controllers Export an API from a parent directive

  83. Requiring Directive Controllers app.directive('imageList', function() { return { require: '^imageGallery',

    scope: true, templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { ctrl.setMainImage(scope.images[0]); scope.ctrl = ctrl; } }; });
  84. Requiring Directive Controllers app.directive('imageList', function() { return { require: '^imageGallery',

    scope: true, templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { ctrl.setMainImage(scope.images[0]); scope.ctrl = ctrl; } }; });
  85. Requiring Directive Controllers app.directive('imageList', function() { return { require: '^imageGallery',

    scope: true, templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { ctrl.setMainImage(scope.images[0]); scope.ctrl = ctrl; } }; }); Directive name
  86. Requiring Directive Controllers app.directive('imageList', function() { return { require: '^imageGallery',

    scope: true, templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { ctrl.setMainImage(scope.images[0]); scope.ctrl = ctrl; } }; }); Parent
  87. Requiring Directive Controllers app.directive('imageList', function() { return { require: '^imageGallery',

    scope: true, templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { ctrl.setMainImage(scope.images[0]); scope.ctrl = ctrl; } }; });
  88. Requiring Directive Controllers app.directive('imageList', function() { return { require: '^imageGallery',

    scope: true, templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { ctrl.setMainImage(scope.images[0]); scope.ctrl = ctrl; } }; });
  89. Requiring Directive Controllers app.directive('imageGallery', function() { return { scope: true,

    templateUrl: 'templates/image-gallery.html', controller: function() { // ... } }; });
  90. Requiring Directive Controllers app.directive('imageGallery', function() { return { scope: true,

    templateUrl: 'templates/image-gallery.html', controller: function() { // ... } }; });
  91. controller: function() { this.mainImage = {}; this.imageFavorites = []; this.setMainImage

    = function(image) { this.mainImage.src = image.src; }; this.favorite = function(image) { if (!image.favorited) { image.favorited = true; this.imageFavorites.push(image); } }; this.unfavorite = function(image) { var i = this.imageFavorites.indexOf(image); if (i > -1) { image.favorited = false; this.imageFavorites.splice(i, 1); } }; }
  92. controller: function() { this.mainImage = {}; this.imageFavorites = []; this.setMainImage

    = function(image) { this.mainImage.src = image.src; }; this.favorite = function(image) { if (!image.favorited) { image.favorited = true; this.imageFavorites.push(image); } }; this.unfavorite = function(image) { var i = this.imageFavorites.indexOf(image); if (i > -1) { image.favorited = false; this.imageFavorites.splice(i, 1); } }; }
  93. controller: function() { this.mainImage = {}; this.imageFavorites = []; this.setMainImage

    = function(image) { this.mainImage.src = image.src; }; this.favorite = function(image) { if (!image.favorited) { image.favorited = true; this.imageFavorites.push(image); } }; this.unfavorite = function(image) { var i = this.imageFavorites.indexOf(image); if (i > -1) { image.favorited = false; this.imageFavorites.splice(i, 1); } }; }
  94. Requiring Directive Controllers app.directive('mainImage', function() { return { require: '^imageGallery',

    // ... link: function(scope, el, attrs, ctrl) { scope.ctrl = ctrl; } } }).directive('imageFavorites', function() { return { require: '^imageGallery', // ... link: function(scope, el, attrs, ctrl) { scope.ctrl = ctrl; } }; });
  95. Requiring Directive Controllers <script id="main-image.html" type="text/ng-template"> <h3>Main Image</h3> <img width="160"

    ng-src="{{ctrl.mainImage.src}}"> </script>
  96. Requiring Directive Controllers <script id="main-image.html" type="text/ng-template"> <h3>Main Image</h3> <img width="160"

    ng-src="{{ctrl.mainImage.src}}"> </script>
  97. Requiring Directive Controllers <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image

    in images"> <div ng-click="ctrl.favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="ctrl.setMainImage(image)"> Set Main Image </button> </div> </script>
  98. Requiring Directive Controllers <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image

    in images"> <div ng-click="ctrl.favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="ctrl.setMainImage(image)"> Set Main Image </button> </div> </script>
  99. Requiring Directive Controllers <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <h4>Favorited: {{ctrl.imageFavorites.length}}</h4> <ul>

    <li ng-repeat="image in ctrl.imageFavorites"> <p> <img ng-src="{{image.src}}" width="40"> {{image.text}} <button ng-click="ctrl.unfavorite(image)"> Unfavorite </button> </p> </li> </ul> </script>
  100. Requiring Directive Controllers <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <h4>Favorited: {{ctrl.imageFavorites.length}}</h4> <ul>

    <li ng-repeat="image in ctrl.imageFavorites"> <p> <img ng-src="{{image.src}}" width="40"> {{image.text}} <button ng-click="ctrl.unfavorite(image)"> Unfavorite </button> </p> </li> </ul> </script>
  101. Requiring Directive Controllers Demo

  102. Requiring Directive Controllers • Consolidate behavior into a controller API

    • Keep own copy of data instead of sharing • Higher coupling — child directives depend upon parent directive • Still need a parent controller to scope own images (cat versus nature)
  103. Isolate Scopes

  104. Isolate Scopes • Directive scope is isolated from other scopes

    – No parent scope to inherit from – Can’t access other scopes “easily” • Use object literal for scope definition scope: { age: '@age', name: '=', selectHobby: '&onSelectHobby' }
  105. Isolate Scopes <name-tag name="scopeName" age="28" on-select-hobby="printHobby(hobby)"></name-tag> scope: { name: '=',

    age: '@age', selectHobby: '&onSelectHobby' }
  106. scope: { name: '=', age: '@age', selectHobby: '&onSelectHobby' } <name-tag

    name="scopeName" age="28" on-select-hobby="printHobby(hobby)"></name-tag> Isolate Scopes Explicitly specify HTML attribute
  107. scope: { name: '=', age: '@age', selectHobby: '&onSelectHobby' } <name-tag

    name="scopeName" age="28" on-select-hobby="printHobby(hobby)"></name-tag> Isolate Scopes Implicit — use the scope key for HTML attribute
  108. Isolate Scopes <name-tag name="scopeName" age="28" on-select-hobby="printHobby(hobby)"></name-tag> scope: { name: '=',

    age: '@age', selectHobby: '&onSelectHobby' } Bidirectional data binding Use property scopeName from current scope
  109. Isolate Scopes <name-tag name="scopeName" age="28" on-select-hobby="printHobby(hobby)"></name-tag> scope: { name: '=',

    age: '@age', selectHobby: '&onSelectHobby' } Attribute/string binding; static. Use literal string "28"
  110. Isolate Scopes <name-tag name="scopeName" age="28" on-select-hobby="printHobby(hobby)"></name-tag> scope: { name: '=',

    age: '@age', selectHobby: '&onSelectHobby' } Execute code in a “parent” scope (i.e. a callback) Function call with any parameter names. Uses function printHobby on parent scope (not the directive scope).
  111. Isolate Scopes app.directive('nameTag', function() { return { scope: { age:

    '@age', name: '=', selectHobby: '&onSelectHobby' }, templateUrl: 'name-tag.html', link: function(scope, el, attrs) { scope.hobbies = ['program', 'read', 'play guitar']; scope.age = attrs.age; } }; });
  112. Isolate Scopes <script id="name-tag.html" type="text/ng-template"> <p>Hi, my name is {{name}}.

    I am {{age}} years old.</p> <p><input type="text" ng-model="name"></p> <ul> <li ng-repeat="hobby in hobbies"> <a href="javascript:void(0)" ng-click="selectHobby({ hobby: hobby })"> {{hobby}} </a> </li> </ul> </script>
  113. Isolate Scopes <script id="name-tag.html" type="text/ng-template"> <p>Hi, my name is {{name}}.

    I am {{age}} years old.</p> <p><input type="text" ng-model="name"></p> <ul> <li ng-repeat="hobby in hobbies"> <a href="javascript:void(0)" ng-click="selectHobby({ hobby: hobby })"> {{hobby}} </a> </li> </ul> </script>
  114. Isolate Scopes <script id="name-tag.html" type="text/ng-template"> <p>Hi, my name is {{name}}.

    I am {{age}} years old.</p> <p><input type="text" ng-model="name"></p> <ul> <li ng-repeat="hobby in hobbies"> <a href="javascript:void(0)" ng-click="selectHobby({ hobby: hobby })"> {{hobby}} </a> </li> </ul> </script>
  115. Isolate Scopes <script id="name-tag.html" type="text/ng-template"> <p>Hi, my name is {{name}}.

    I am {{age}} years old.</p> <p><input type="text" ng-model="name"></p> <ul> <li ng-repeat="hobby in hobbies"> <a href="javascript:void(0)" ng-click="selectHobby({ hobby: hobby })"> {{hobby}} </a> </li> </ul> </script>
  116. Isolate Scopes <name-tag name="scopeName" age="28" on-select-hobby="printHobby(hobby)"></name-tag> <script id="name-tag.html" type="text/ng-template"> <p>Hi,

    my name is {{name}}. I am {{age}} years old.</p> <p><input type="text" ng-model="name"></p> <ul> <li ng-repeat="hobby in hobbies"> <a href="javascript:void(0)" ng-click="selectHobby({ hobby: hobby })"> {{hobby}} </a> </li> </ul> </script>
  117. <script id="name-tag.html" type="text/ng-template"> <p>Hi, my name is {{name}}. I am

    {{age}} years old.</p> <p><input type="text" ng-model="name"></p> <ul> <li ng-repeat="hobby in hobbies"> <a href="javascript:void(0)" ng-click="selectHobby({ hobby: hobby })"> {{hobby}} </a> </li> </ul> </script> Isolate Scopes <name-tag name="scopeName" age="28" on-select-hobby="printHobby(hobby)"></name-tag> Invocations require “named arguments” via object literal.
  118. Isolate Scopes Apply to the image gallery now

  119. Isolate Scopes <body ng-controller="ImageCtrl"> <image-gallery images="catImages"></image-gallery> <image-gallery images="natureImages"></image-gallery> </body>

  120. Isolate Scopes <body ng-controller="ImageCtrl"> <image-gallery images="catImages"></image-gallery> <image-gallery images="natureImages"></image-gallery> </body>

  121. Isolate Scopes app.controller('ImageCtrl', function($scope) { $scope.catImages = [ createImage('/images/cat-1.jpg', 'Cat

    1'), createImage('/images/cat-2.jpg', 'Cat 2'), createImage('/images/cat-3.jpg', 'Cat 3'), createImage('/images/cat-4.jpg', 'Cat 4') ]; $scope.natureImages = [ createImage('/images/nature-1.jpg', 'Nature 1'), createImage('/images/nature-2.jpg', 'Nature 2'), createImage('/images/nature-3.jpg', 'Nature 3'), createImage('/images/nature-4.jpg', 'Nature 4') ]; });
  122. Isolate Scopes app.directive('imageGallery', function() { return { scope: { images:

    '=' }, templateUrl: 'templates/image-gallery.html', link: function(scope) { // ... } }; });
  123. Isolate Scopes app.directive('imageGallery', function() { return { scope: { images:

    '=' }, templateUrl: 'templates/image-gallery.html', link: function(scope) { // ... } }; });
  124. Isolate Scopes link: function(scope) { scope.mainImage = {}; scope.imageFavorites =

    []; scope.setMain = function(image) { scope.mainImage.src = image.src; }; scope.favorite = function(image) { scope.imageFavorites.push(image); }; scope.unfavorite = function(image) { var i = scope.imageFavorites.indexOf(image); if (i > -1) { scope.imageFavorites.splice(i, 1); } }; }
  125. Isolate Scopes link: function(scope) { scope.mainImage = {}; scope.imageFavorites =

    []; scope.setMain = function(image) { scope.mainImage.src = image.src; }; scope.favorite = function(image) { scope.imageFavorites.push(image); }; scope.unfavorite = function(image) { var i = scope.imageFavorites.indexOf(image); if (i > -1) { scope.imageFavorites.splice(i, 1); } }; }
  126. Isolate Scopes link: function(scope) { scope.mainImage = {}; scope.imageFavorites =

    []; scope.setMain = function(image) { scope.mainImage.src = image.src; }; scope.favorite = function(image) { scope.imageFavorites.push(image); }; scope.unfavorite = function(image) { var i = scope.imageFavorites.indexOf(image); if (i > -1) { scope.imageFavorites.splice(i, 1); } }; }
  127. Isolate Scopes <script id="image-gallery.html" type="text/ng-template"> <main-image image="mainImage"></main-image> <image-list images="images" on-set-main="setMain(image)"

    on-favorite="favorite(image)"></image-list> <image-favorites images="imageFavorites" on-unfavorite="unfavorite(image)"></image-favorites> </script>
  128. Isolate Scopes <script id="image-gallery.html" type="text/ng-template"> <main-image image="mainImage"></main-image> <image-list images="images" on-set-main="setMain(image)"

    on-favorite="favorite(image)"></image-list> <image-favorites images="imageFavorites" on-unfavorite="unfavorite(image)"></image-favorites> </script>
  129. Isolate Scopes <script id="image-gallery.html" type="text/ng-template"> <main-image image="mainImage"></main-image> <image-list images="images" on-set-main="setMain(image)"

    on-favorite="favorite(image)"></image-list> <image-favorites images="imageFavorites" on-unfavorite="unfavorite(image)"></image-favorites> </script>
  130. Isolate Scopes <script id="image-gallery.html" type="text/ng-template"> <main-image image="mainImage"></main-image> <image-list images="images" on-set-main="setMain(image)"

    on-favorite="favorite(image)"></image-list> <image-favorites images="imageFavorites" on-unfavorite="unfavorite(image)"></image-favorites> </script>
  131. Isolate Scopes <script id="image-gallery.html" type="text/ng-template"> <main-image image="mainImage"></main-image> <image-list images="images" on-set-main="setMain(image)"

    on-favorite="favorite(image)"></image-list> <image-favorites images="imageFavorites" on-unfavorite="unfavorite(image)"></image-favorites> </script>
  132. Isolate Scopes <script id="image-gallery.html" type="text/ng-template"> <main-image image="mainImage"></main-image> <image-list images="images" on-set-main="setMain(image)"

    on-favorite="favorite(image)"></image-list> <image-favorites images="imageFavorites" on-unfavorite="unfavorite(image)"></image-favorites> </script>
  133. Isolate Scopes <script id="image-gallery.html" type="text/ng-template"> <main-image image="mainImage"></main-image> <image-list images="images" on-set-main="setMain(image)"

    on-favorite="favorite(image)"></image-list> <image-favorites images="imageFavorites" on-unfavorite="unfavorite(image)"></image-favorites> </script>
  134. Isolate Scopes <script id="image-gallery.html" type="text/ng-template"> <main-image image="mainImage"></main-image> <image-list images="images" on-set-main="setMain(image)"

    on-favorite="favorite(image)"></image-list> <image-favorites images="imageFavorites" on-unfavorite="unfavorite(image)"></image-favorites> </script>
  135. Isolate Scopes <script id="image-gallery.html" type="text/ng-template"> <main-image image="mainImage"></main-image> <image-list images="images" on-set-main="setMain(image)"

    on-favorite="favorite(image)"></image-list> <image-favorites images="imageFavorites" on-unfavorite="unfavorite(image)"></image-favorites> </script>
  136. Isolate Scopes <script id="image-gallery.html" type="text/ng-template"> <main-image image="mainImage"></main-image> <image-list images="images" on-set-main="setMain(image)"

    on-favorite="favorite(image)"></image-list> <image-favorites images="imageFavorites" on-unfavorite="unfavorite(image)"></image-favorites> </script>
  137. Isolate Scopes app.directive('mainImage', function() { return { scope: { mainImage:

    '=image' }, templateUrl: 'templates/main-image.html' } });
  138. Isolate Scopes app.directive('mainImage', function() { return { scope: { mainImage:

    '=image' }, templateUrl: 'templates/main-image.html' } });
  139. Isolate Scopes <script id="main-image.html" type="text/ng-template"> <h3>Main Image</h3> <img width="160" ng-src="{{mainImage.src}}">

    </script>
  140. Isolate Scopes <script id="main-image.html" type="text/ng-template"> <h3>Main Image</h3> <img width="160" ng-src="{{mainImage.src}}">

    </script>
  141. Isolate Scopes app.directive('imageFavorites', function() { return { scope: { imageFavorites:

    '=images', triggerUnfavorite: '&onUnfavorite' }, templateUrl: 'templates/image-favorites.html', link: function(scope) { scope.unfavorite = function(image) { image.favorited = false; scope.triggerUnfavorite({ image: image }); }; } }; });
  142. Isolate Scopes app.directive('imageFavorites', function() { return { scope: { imageFavorites:

    '=images', triggerUnfavorite: '&onUnfavorite' }, templateUrl: 'templates/image-favorites.html', link: function(scope) { scope.unfavorite = function(image) { image.favorited = false; scope.triggerUnfavorite({ image: image }); }; } }; });
  143. Isolate Scopes app.directive('imageFavorites', function() { return { scope: { imageFavorites:

    '=images', triggerUnfavorite: '&onUnfavorite' }, templateUrl: 'templates/image-favorites.html', link: function(scope) { scope.unfavorite = function(image) { image.favorited = false; scope.triggerUnfavorite({ image: image }); }; } }; });
  144. Isolate Scopes <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <h4>Favorited: {{imageFavorites.length}}</h4> <ul> <li

    ng-repeat="image in imageFavorites"> <p> <img ng-src="{{image.src}}" width="40"> {{image.text}} <button ng-click="unfavorite(image)"> Unfavorite </button> </p> </li> </ul> </script>
  145. Isolate Scopes <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <h4>Favorited: {{imageFavorites.length}}</h4> <ul> <li

    ng-repeat="image in imageFavorites"> <p> <img ng-src="{{image.src}}" width="40"> {{image.text}} <button ng-click="unfavorite(image)"> Unfavorite </button> </p> </li> </ul> </script>
  146. Isolate Scopes <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <h4>Favorited: {{imageFavorites.length}}</h4> <ul> <li

    ng-repeat="image in imageFavorites"> <p> <img ng-src="{{image.src}}" width="40"> {{image.text}} <button ng-click="unfavorite(image)"> Unfavorite </button> </p> </li> </ul> </script>
  147. app.directive('imageList', function() { return { scope: { images: '=', setMainImage:

    '&onSetMain', triggerFavorite: '&onFavorite' }, templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { scope.setMainImage({ image: scope.images[0] }); scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.triggerFavorite({ image: image }); } }; } }; });
  148. app.directive('imageList', function() { return { scope: { images: '=', setMainImage:

    '&onSetMain', triggerFavorite: '&onFavorite' }, templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { scope.setMainImage({ image: scope.images[0] }); scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.triggerFavorite({ image: image }); } }; } }; });
  149. app.directive('imageList', function() { return { scope: { images: '=', setMainImage:

    '&onSetMain', triggerFavorite: '&onFavorite' }, templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { scope.setMainImage({ image: scope.images[0] }); scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.triggerFavorite({ image: image }); } }; } }; });
  150. app.directive('imageList', function() { return { scope: { images: '=', setMainImage:

    '&onSetMain', triggerFavorite: '&onFavorite' }, templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { scope.setMainImage({ image: scope.images[0] }); scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.triggerFavorite({ image: image }); } }; } }; });
  151. Isolate Scopes <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image in

    images"> <div ng-click="favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="setMainImage({ image: image })"> Set Main Image </button> </div> </script>
  152. Isolate Scopes <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image in

    images"> <div ng-click="favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="setMainImage({ image: image })"> Set Main Image </button> </div> </script>
  153. Isolate Scopes <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image in

    images"> <div ng-click="favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="setMainImage({ image: image })"> Set Main Image </button> </div> </script>
  154. Isolate Scopes Demo

  155. Isolate Scopes • Black box a directive to be self-contained

    and reusable • Think in terms of small pieces • Establish well-defined interface/API through HTML attributes • Juggle multiple points of state • More markup
  156. Events

  157. Events • Built in eventing with $emit, $broadcast, and $on

    methods of $scope. • Use events like a message bus to transfer data • Events travel vertically through scopes – Use parent scope event delegation for sibling directive communication
  158. Events $scope.$emit('select:greeting', 'hello world'); $scope.$broadcast('add:item', 42); // In a child

    directive var items = []; $scope.$on('add:item', function(e, n) { items.push(n); }); // In a parent directive $scope.$on('select:greeting', function(e, greeting) { console.log(greeting.toUpperCase()); });
  159. $scope.$emit('select:greeting', 'hello world'); $scope.$broadcast('add:item', 42); // In a child directive

    var items = []; $scope.$on('add:item', function(e, n) { items.push(n); }); // In a parent directive $scope.$on('select:greeting', function(e, greeting) { console.log(greeting.toUpperCase()); }); Events $emit sends events up to parent scopes.
  160. $scope.$emit('select:greeting', 'hello world'); $scope.$broadcast('add:item', 42); // In a child directive

    var items = []; $scope.$on('add:item', function(e, n) { items.push(n); }); // In a parent directive $scope.$on('select:greeting', function(e, greeting) { console.log(greeting.toUpperCase()); }); Events $broadcast sends events down to child scopes.
  161. $scope.$emit('select:greeting', 'hello world'); $scope.$broadcast('add:item', 42); // In a child directive

    var items = []; $scope.$on('add:item', function(e, n) { items.push(n); }); // In a parent directive $scope.$on('select:greeting', function(e, greeting) { console.log(greeting.toUpperCase()); }); Events $on listens for $emit and $broadcast events and triggers the provided callbacks.
  162. // In a child directive var items = []; $scope.$on('add:item',

    function(e, n) { items.push(n); }); // In a parent directive $scope.$on('select:greeting', function(e, greeting) { console.log(greeting.toUpperCase()); }); $scope.$emit('select:greeting', 'hello world'); $scope.$broadcast('add:item', 42); Events The first parameter is an event object like DOM events.
  163. // In a child directive var items = []; $scope.$on('add:item',

    function(e, n) { items.push(n); }); // In a parent directive $scope.$on('select:greeting', function(e, greeting) { console.log(greeting.toUpperCase()); }); $scope.$emit('select:greeting', 'hello world'); $scope.$broadcast('add:item', 42); Events Remaining parameters are the remaining arguments supplied to $emit and $broadcast.
  164. $scope.$emit('select:greeting', 'hello world'); $scope.$broadcast('add:item', 42); Events // In a child

    directive var items = []; $scope.$on('add:item', function(e, n) { items.push(n); }); // In a parent directive $scope.$on('select:greeting', function(e, greeting) { console.log(greeting.toUpperCase()); }); Stick to a naming convention like verb:noun.
  165. $scope.$emit('select:greeting', 'hello world'); $scope.$broadcast('add:item', 42); Events // In a child

    directive var items = []; $scope.$on('add:item', function(e, n) { items.push(n); }); // In a parent directive $scope.$on('select:greeting', function(e, greeting) { console.log(greeting.toUpperCase()); }); Verb
  166. $scope.$emit('select:greeting', 'hello world'); $scope.$broadcast('add:item', 42); Events // In a child

    directive var items = []; $scope.$on('add:item', function(e, n) { items.push(n); }); // In a parent directive $scope.$on('select:greeting', function(e, greeting) { console.log(greeting.toUpperCase()); }); Noun
  167. Events Apply to the image gallery now

  168. Events <script id="image-gallery.html" type="text/ng-template"> <main-image></main-image> <image-list images="images"></image-list> <image-favorites></image-favorites> </script>

  169. Events <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image in images">

    <div ng-click="favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="setMainImage(image)"> Set Main Image </button> </div> </script>
  170. Events <script id="image-list.html" type="text/ng-template"> <h3>Images</h3> <div class="image-info" ng-repeat="image in images">

    <div ng-click="favorite(image)"> <img ng-src="{{image.src}}"> </div> <p>{{image.text}}</p> <button ng-click="setMainImage(image)"> Set Main Image </button> </div> </script>
  171. app.directive('imageList', function() { return { scope: { images: '=', },

    templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { scope.setMainImage = function(image) { scope.$emit('delegate:set:mainImage', image); }; scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.$emit('delegate:favorite:image', image); } }; scope.setMainImage(scope.images[0]); } }; });
  172. app.directive('imageList', function() { return { scope: { images: '=', },

    templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { scope.setMainImage = function(image) { scope.$emit('delegate:set:mainImage', image); }; scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.$emit('delegate:favorite:image', image); } }; scope.setMainImage(scope.images[0]); } }; });
  173. app.directive('imageList', function() { return { scope: { images: '=', },

    templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { scope.setMainImage = function(image) { scope.$emit('delegate:set:mainImage', image); }; scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.$emit('delegate:favorite:image', image); } }; scope.setMainImage(scope.images[0]); } }; });
  174. app.directive('imageList', function() { return { scope: { images: '=', },

    templateUrl: 'templates/image-list.html', link: function(scope, el, attrs, ctrl) { scope.setMainImage = function(image) { scope.$emit('delegate:set:mainImage', image); }; scope.favorite = function(image) { if (!image.favorited) { image.favorited = true; scope.$emit('delegate:favorite:image', image); } }; scope.setMainImage(scope.images[0]); } }; });
  175. Events app.directive('imageGallery', function() { return { scope: { images: '='

    }, templateUrl: 'templates/image-gallery.html', link: function(scope) { scope.$on('delegate:set:mainImage', function(e, image) { scope.$broadcast('set:mainImage', image); }); scope.$on('delegate:favorite:image', function(e, image) { scope.$broadcast('favorite:image', image); }); } }; });
  176. Events app.directive('imageGallery', function() { return { scope: { images: '='

    }, templateUrl: 'templates/image-gallery.html', link: function(scope) { scope.$on('delegate:set:mainImage', function(e, image) { scope.$broadcast('set:mainImage', image); }); scope.$on('delegate:favorite:image', function(e, image) { scope.$broadcast('favorite:image', image); }); } }; });
  177. Events app.directive('mainImage', function() { return { scope: {}, templateUrl: 'templates/main-image.html',

    link: function(scope) { scope.mainImage = {}; scope.$on('set:mainImage', function(e, image) { scope.mainImage.src = image.src; }); } } });
  178. Events app.directive('mainImage', function() { return { scope: {}, templateUrl: 'templates/main-image.html',

    link: function(scope) { scope.mainImage = {}; scope.$on('set:mainImage', function(e, image) { scope.mainImage.src = image.src; }); } } });
  179. Events app.directive('mainImage', function() { return { scope: {}, templateUrl: 'templates/main-image.html',

    link: function(scope) { scope.mainImage = {}; scope.$on('set:mainImage', function(e, image) { scope.mainImage.src = image.src; }); } } });
  180. Events app.directive('mainImage', function() { return { scope: {}, templateUrl: 'templates/main-image.html',

    link: function(scope) { scope.mainImage = {}; scope.$on('set:mainImage', function(e, image) { scope.mainImage.src = image.src; }); } } });
  181. app.directive('imageFavorites', function() { return { scope: {}, templateUrl: 'templates/image-favorites.html', link:

    function(scope) { scope.imageFavorites = []; scope.$on('favorite:image', function(e, image) { scope.imageFavorites.push(image); }); scope.unfavorite = function(image) { var i = scope.imageFavorites.indexOf(image); if (i > -1) { image.favorited = false; scope.imageFavorites.splice(i, 1); } }; } }; });
  182. app.directive('imageFavorites', function() { return { scope: {}, templateUrl: 'templates/image-favorites.html', link:

    function(scope) { scope.imageFavorites = []; scope.$on('favorite:image', function(e, image) { scope.imageFavorites.push(image); }); scope.unfavorite = function(image) { var i = scope.imageFavorites.indexOf(image); if (i > -1) { image.favorited = false; scope.imageFavorites.splice(i, 1); } }; } }; });
  183. app.directive('imageFavorites', function() { return { scope: {}, templateUrl: 'templates/image-favorites.html', link:

    function(scope) { scope.imageFavorites = []; scope.$on('favorite:image', function(e, image) { scope.imageFavorites.push(image); }); scope.unfavorite = function(image) { var i = scope.imageFavorites.indexOf(image); if (i > -1) { image.favorited = false; scope.imageFavorites.splice(i, 1); } }; } }; });
  184. app.directive('imageFavorites', function() { return { scope: {}, templateUrl: 'templates/image-favorites.html', link:

    function(scope) { scope.imageFavorites = []; scope.$on('favorite:image', function(e, image) { scope.imageFavorites.push(image); }); scope.unfavorite = function(image) { var i = scope.imageFavorites.indexOf(image); if (i > -1) { image.favorited = false; scope.imageFavorites.splice(i, 1); } }; } }; });
  185. Demo Events

  186. Events • Decouple directives • Isolate state more easily •

    Cleaner markup • Indirection of data flow (who responds to an event?) • Have to remember and duplicate event names (may hurt directive reuse)
  187. Upshot

  188. Upshot Refactoring

  189. Refactoring • Easier to change or update app • Don’t

    have to touch entire app, just an individual directive – Touching multiple pieces may mean better SOC needed – SOC is evolutionary for the lifetime of the app
  190. Refactoring • Remove count • Remove image text • Sort

    images by text Alter image gallery favorites Demo
  191. Refactoring Alter image gallery favorites <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <ul>

    <li ng-repeat="image in imageFavorites | orderBy: 'text'"> <p> <img ng-src="{{image.src}}" width="40"> </p> <button ng-clik="unfavorite(image)"> Unfavorite </button> </li> </ul> </script>
  192. <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <ul> <li ng-repeat="image in imageFavorites |

    orderBy: 'text'"> <p> <img ng-src="{{image.src}}" width="40"> </p> <button ng-clik="unfavorite(image)"> Unfavorite </button> </li> </ul> </script> Refactoring Alter image gallery favorites Favorites count gone
  193. Refactoring Alter image gallery favorites <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <ul>

    <li ng-repeat="image in imageFavorites | orderBy: 'text'"> <p> <img ng-src="{{image.src}}" width="40"> </p> <button ng-clik="unfavorite(image)"> Unfavorite </button> </li> </ul> </script> Image text gone
  194. Refactoring Alter image gallery favorites <script id="image-favorites.html" type="text/ng-template"> <h3>Favorites</h3> <ul>

    <li ng-repeat="image in imageFavorites | orderBy: 'text'"> <p> <img ng-src="{{image.src}}" width="40"> </p> <button ng-clik="unfavorite(image)"> Unfavorite </button> </li> </ul> </script>
  195. Composition is a critical tool in software development.

  196. Thanks! jfairbank @elpapapollo blog.jeremyfairbank.com Demo and code github.com/jfairbank/orchestrating-apps-angular Slides: speakerdeck.com/jfairbank/connectjs-orchestrating-apps-by-composing-angular-directives