Upgrade to Pro — share decks privately, control downloads, hide ads and more …

CodeStock: Orchestrating Apps by Composing Angu...

CodeStock: Orchestrating Apps by Composing Angular Directives

Jeremy Fairbank

July 11, 2015
Tweet

More Decks by Jeremy Fairbank

Other Decks in Programming

Transcript

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

    • Me • You http://www.commitstrip.com/en/2015/05/29/always-stuck-somewhere-in-my-head/
  2. – 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?
  3. – Wikipedia “The way in which something is put together

    or arranged. The combination of parts or elements that make up something.” What is composition?
  4. Design Principles • Modularity • Separation of concerns (SOC) •

    Loose coupling • High cohesion • Composition over inheritance • Ad nauseam…
  5. Directives Review • Attach behavior to the DOM via attributes,

    elements, comments, or CSS classes • Componentize functionality with HTML • Create custom HTML elements
  6. 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 en="es"></my-hello-world> <name-tag name="Jeremy"></name-tag>
  7. 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'; } } }; });
  8. 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'; } } }; });
  9. 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'; } } }; });
  10. 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'; } } }; });
  11. 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>
  12. 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>
  13. 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>
  14. Directives Review return { restrict: 'E', scope: { name: '@'

    }, template: 'Hello, my name is {{name}}', link: function(scope) { if (!scope.name) { scope.name = 'Joe Schmoe'; } } };
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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>
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. Prototypal Scoping app.directive('imageGallery', function() { return { scope: true, templateUrl:

    'image-gallery.html' }; }); Create a child scope that “inherits” from parent scope
  26. 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') ]; });
  27. 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') ]; });
  28. 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') ]; });
  29. 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>
  30. 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>
  31. 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>
  32. 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>
  33. 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>
  34. 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>
  35. 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>
  36. 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); } }; } }; });
  37. 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); } }; } }; });
  38. 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>
  39. 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>
  40. 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') ]; });
  41. 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); } }; } }; });
  42. 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); } }; } }; });
  43. 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); } }; } }; });
  44. 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') ]; });
  45. 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') ]; });
  46. 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
  47. 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; } }; });
  48. 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; } }; });
  49. 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
  50. 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
  51. 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; } }; });
  52. 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; } }; });
  53. Requiring Directive Controllers app.directive('imageGallery', function() { return { scope: true,

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

    templateUrl: 'templates/image-gallery.html', controller: function() { // ... } }; });
  55. 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); } }; }
  56. 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); } }; }
  57. 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); } }; }
  58. 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; } }; });
  59. 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>
  60. 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>
  61. 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>
  62. 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>
  63. 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)
  64. 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' }
  65. 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
  66. 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
  67. 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
  68. 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"
  69. 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).
  70. 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; } }; });
  71. 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>
  72. 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>
  73. 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>
  74. 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>
  75. 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>
  76. <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.
  77. 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') ]; });
  78. Isolate Scopes app.directive('imageGallery', function() { return { scope: { images:

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

    '=' }, templateUrl: 'templates/image-gallery.html', link: function(scope) { // ... } }; });
  80. 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); } }; }
  81. 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); } }; }
  82. 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); } }; }
  83. 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>
  84. 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>
  85. 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>
  86. 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>
  87. 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>
  88. 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>
  89. 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>
  90. 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>
  91. 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>
  92. 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>
  93. Isolate Scopes app.directive('mainImage', function() { return { scope: { mainImage:

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

    '=image' }, templateUrl: 'templates/main-image.html' } });
  95. 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 }); }; } }; });
  96. 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 }); }; } }; });
  97. 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 }); }; } }; });
  98. 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>
  99. 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>
  100. 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>
  101. 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 }); } }; } }; });
  102. 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 }); } }; } }; });
  103. 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 }); } }; } }; });
  104. 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 }); } }; } }; });
  105. 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>
  106. 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>
  107. 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>
  108. 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
  109. 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
  110. 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()); });
  111. $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.
  112. $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.
  113. $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.
  114. // 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.
  115. // 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.
  116. $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.
  117. $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
  118. $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
  119. 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>
  120. 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>
  121. 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]); } }; });
  122. 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]); } }; });
  123. 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]); } }; });
  124. 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]); } }; });
  125. 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); }); } }; });
  126. 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); }); } }; });
  127. 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; }); } } });
  128. 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; }); } } });
  129. 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; }); } } });
  130. 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; }); } } });
  131. 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); } }; } }; });
  132. 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); } }; } }; });
  133. 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); } }; } }; });
  134. 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); } }; } }; });
  135. 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)
  136. 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
  137. • Remove count • Remove image text • Sort images

    by text Alter image gallery favorites Refactoring Demo
  138. 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>
  139. <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
  140. 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
  141. 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>