Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Reusable Components in Angular: Architecture, transclusion, and more

1,228 views

Published on

There is a very small limit of characters on the Slideshare description. For a full transcript, see the presentation at SpeakerDeck.

https://speakerdeck.com/morewry/reusable-components-in-angular-architecture-transclusion-and-more

We’re hiring!
opentable.com/careers/
OpenTable is hiring! http://opentable.com/careers/

Questions?

Rachael L Moore
UI Engineer
Twitter: @morewry
Github: morewry

Kara Erickson
Web Engineer
Github: kara

About Us

[Rachael L Moore] At OpenTable we have two sides to our products, restaurant and diner. Since most of us here probably don't own restaurants, behind me is a small tour of what that looks like.

Our restaurant team is currently converting a large, feature-rich legacy system for restaurants into a web application called Guest Center. Guest Center is a growing application with a large backlog of features.

Since the functionality of those features is pretty self-contained, because they're highly task-specific, we realized we could get great results if we approached our product as a series of micro sites, or an app of apps, created and maintained by small teams.

But for that to really work, at the scale we need, we have to have really seamless and hopefully painless integration between those applications. For our own sake, because why make our work hours miserable; but more importantly because we have to have a consistent user experience.

There can’t be any hiccups in the end user's experience navigating through the product, even though different teams work in on different parts of the application in different repositories.

One way we can achieve this is by creating re-usable user interface components.

About UI Components

What does that mean, exactly? Well, let's take a step back for a moment.

Graphical web user interfaces are a chimeric combination of script, style, markup, assets, and text. Currently, what we have in most UI libraries are a bunch of separate files that we’re hoping people will combine as we intend and/or to their own satisfaction. Each one of those separate files may use language-specific components, but we currently lack any way to ensure that all this code which is required to work together in concert actually does so.

Benefits of UI Components

Ideally, what we would want is something that can combine all those pieces associated with the user interface. AngularJS directives do this for script and HTML in ways that fit very well within our already-Angular applications, so it's a good place to start to use these in production while learning concepts that will be useful to us in the future.

Because, when we create UI components, we gain a number of benefits....

Reusable Components in Angular: Architecture, transclusion, and more

  1. 1. SF Angular Meetup OpenTable January 12, 2015 MTV Angular Meetup Google January 20, 2015 Reusable Components in Angular Architecture, transclusion, and more
  2. 2. Who We Are Rachael L Moore UI Engineer morewry Kara Erickson Web Engineer kara
  3. 3. UI Components What they are Why to use them How to design them
  4. 4. Implementations Multi-transclude Attribute passing Sub-directives
  5. 5. Dropdown isolate scopes
  6. 6. 16:9 video tour of our app UI
  7. 7. UI Components What and why
  8. 8. FIREWORKS NIGHT CC BY GPS Benefits of UI ComponentsBenefits of UI Components
  9. 9. ConsistencyConsistency
  10. 10. Faster DevelopmentFaster Development
  11. 11. QualityQuality TROPHIES CC BY BRAD K
  12. 12. Better DeploymentBetter Deployment T-10 D PARACHUTE CC BY PROGRAM EXECUTIVE OFFICE
  13. 13. AccuracyAccuracy
  14. 14. Better FocusBetter Focus
  15. 15. FIREWORKS NIGHT CC BY GPS Consistency Faster development Quality Benefits of UI Components Better deployment Accuracy Better focus
  16. 16. Custom ElementsCustom Elements BRIGHT YELLOW FLOWER CROCHET CC BY SINGLE STITCHES
  17. 17. Coherent ConfigCoherent Config
  18. 18. FIREWORKS NIGHT CC BY GPS Consistency Faster development Quality Better deployment Accuracy Better focus Custom elements Coherent config Benefits of UI Components
  19. 19. How to Design UI ComponentsHow to Design UI Components
  20. 20. Prior ArtPrior Art
  21. 21. Natural LanguageNatural Language
  22. 22. VolatilityVolatility ETNA VOLCANO PAROXYSMAL ERUPTION CC BY GNUCKX
  23. 23. Prior art Natural language Volatility How To Design UI Components Visual patterns Behavioral patterns Technical limitations
  24. 24. Site Layout Design
  25. 25. Visual PatternsVisual Patterns SANTAS AT CITY HALL CC BY DENNIS YANG
  26. 26. Technical LimitationsTechnical Limitations FACEPALM CC BY BRANDON GRASLEY
  27. 27. <div> <header /> <nav /> <div /> <footer /> </div> <directive-1> <directive-2 /> <directive-3 /> <directive-4 /> <directive-5 /> </directive-1> <self /> closing tags for brevity only (do not try at home)
  28. 28. <ot-site> </ot-site> site.html
  29. 29. Site Layout Implementation
  30. 30. Body  Menu  Header 
  31. 31. <ot-site></ot-site> HTML attributes Transclusion Site scaffold interface
  32. 32. <ot-site current-app= "Marketing"> </ot-site> HTML attributes Transclusion Site scaffold interface
  33. 33. <ot-site current-app=" Marketing" license=" 2015 Copyright OpenTable" account- manager="true" app- switcher="false" site- title="AppCtrl.title" sidebar="See more options" card-layout=" true" card-values=" AppCtrl.cards"></ot- site> HTML attributes Transclusion Site scaffold interface
  34. 34. Marketing"license=" 2015CopyrightOpenTable" account-manager="true" app-switcher="false" site-title="AppCtrl. title"sidebar=" Seemoreoptions"sidebar- items="AppCtrl. sidebarList"card- layout="true"card- values="AppCtrl.cards" rid-selector="true" dropdown-theme="dark" sub-header="Themes" tabs="true"tabs-items=" AppCtrl.tabs"sidebar- content="Here is very HTML attributes Transclusion Site scaffold interface
  35. 35. <ot-site> <div>Transcluded content</div> </ot-site> HTML attributes Transclusion Site scaffold interface
  36. 36. <ot-site> </ot-site> index.html
  37. 37. <ot-site> <div> I’m transcluding the header. </div> <div> I’m transcluding the menu. </div> <div> I’m transcluding the body. </div> </ot-site> index.html
  38. 38. Body  Menu  Header 
  39. 39. .directive("otSite", () => { return { scope: {}, template: `` }; }); site.js
  40. 40. .directive("otSite", () => { return { scope: {}, template: `` . }; }); site.js
  41. 41. Quick ES6 Review
  42. 42. "<div>" + "<p ng-class="{'highlight': selected}">Hey</p>" + "</div>" `<div> <p ng-class="{'highlight': selected}">Hey</p> </div>` ES6 Review - Template Strings ES5 ES6
  43. 43. ES6 Review - Template Strings var name = "Kara"; var greeting = "Hi " + name + "!"; // Hi Kara! var name = "Kara"; var greeting = `Hi ${name}!`; // Hi Kara! ES5 ES6
  44. 44. ES6 Review - Fat Arrows var that = this; var toggleMenu = function() { that.menuShowing = !that.menuShowing; }; var toggleMenu = () => { this.menuShowing = !this.menuShowing; }; ES5 ES6
  45. 45. .directive("otSite", () => { return { scope: {}, template: `` }; }); site.js
  46. 46. .directive("otSite", () => { return { scope: {}, template: ` <div> <header></header> <nav></nav> <main></main> <footer></footer> </div>` }; site.js ...
  47. 47. Code Demo
  48. 48. <ot-site> <div> I’m transcluding the header. </div> <div> I’m transcluding the menu. </div> <div> I’m transcluding the body. </div> </ot-site> index.html
  49. 49. .directive("otSite", () => { return { scope: {}, template: ` <div> <header></header> <nav></nav> <main></main> <footer></main> </div>` }; site.js ...
  50. 50. .directive("otSite", () => { return { scope: {}, transclude: true, template: ` <div> <header></header> <nav></nav> <main></main> <footer></main> </div>` site.js ...
  51. 51. .directive("otSite", () => { return { scope: {}, transclude: true, template: ` <div> <header ng-transclude></header> <nav ng-transclude></nav> <main ng-transclude></main> <footer></footer> </div>` site.js ...
  52. 52. Code Demo
  53. 53. $transclude((function(clone)) { $element.empty(); $element.append(clone); }); } ng-transclude
  54. 54. .directive("otSite", () => { return { scope: {}, transclude: true, template: ` <div> <header ng-transclude></header> <nav ng-transclude></nav> <main ng-transclude></main> <footer></footer> </div>` site.js
  55. 55. .directive("otSite", () => { return { scope: {}, transclude: true, template: ` <div> <header></header> <nav></nav> <main></main> <footer></footer> </div>` site.js
  56. 56. <ot-site> <div> I’m transcluding the header. </div> <div> I’m transcluding the menu. </div> <div> I’m transcluding the body. </div> </ot-site> index.html
  57. 57. <ot-site> <div transclude-to="site-head"> I’m transcluding the header. </div> <div transclude-to="site-menu"> I’m transcluding the menu. </div> <div transclude-to="site-body"> I’m transcluding the body. </div> </ot-site> index.html
  58. 58. .directive("otSite", () => { return { scope: {}, transclude: true, template: ` <div> <header></header> <nav></nav> <main></main> <footer></footer> </div>` site.js ...
  59. 59. .directive("otSite", () => { return { scope: {}, transclude: true, template: ` <div> <header transclude-id="site-head"></header> <nav transclude-id="site-menu"></nav> <main transclude-id="site-body"></main> <footer></footer> </div>` site.js ...
  60. 60. Custom transclusion For element in clone: 1. Get target ID 2. Find target element with that ID 3. Append clone element to target
  61. 61. angular.forEach(clone, (cloneEl) => { // get desired target ID // find target element with that ID // append element to target }); custom-transclude.js
  62. 62. angular.forEach(clone, (cloneEl) => { // get desired target ID var tId = cloneEl.attributes["transclude-to"].value; // find target element with that ID // append element to target }); custom-transclude.js
  63. 63. angular.forEach(clone, (cloneEl) => { // get desired target ID var tId = cloneEl.attributes["transclude-to"].value; // find target element with that ID var target = templ.find(`[transclude-id="${tId}"]`); // append element to target }); custom-transclude.js
  64. 64. angular.forEach(clone, (cloneEl) => { // get desired target ID var tId = cloneEl.attributes["transclude-to"].value; // find target element with that ID var target = templ.find(`[transclude-id="${tId}"]`); // append element to target target.append(cloneEl); }); custom-transclude.js
  65. 65. angular.forEach(clone, (cloneEl) => { ... target.append(cloneEl); }); custom-transclude.js
  66. 66. angular.forEach(clone, (cloneEl) => { ... if (target.length) { target.append(cloneEl); } }); custom-transclude.js
  67. 67. angular.forEach(clone, (cloneEl) => { ... if (target.length) { target.append(cloneEl); } else { cloneEl.remove(); } }); custom-transclude.js
  68. 68. angular.forEach(clone, (cloneEl) => { ... if (target.length) { target.append(cloneEl); } else { cloneEl.remove(); throw new Error( `Target not found. Please specify the correct transclude-to attribute.` ); } custom-transclude.js ...
  69. 69. Two parameters: ○ scope (optional) ○ callback function □ passes clone Transclude function transclude (clone) => # DOM manipulation
  70. 70. angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); custom-transclude.js
  71. 71. transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); custom-transclude.js
  72. 72. Transclude function access points compile: (tElem, tAttrs, transclude) => link: (scope, iElem, iAttrs, ctrl, transclude) => controller: ($scope, $element, $transclude) =>
  73. 73. Transclude function access points compile: (tElem, tAttrs, transclude) => link: (scope, iElem, iAttrs, ctrl, transclude) => controller: ($scope, $element, $transclude) =>
  74. 74. site.js.directive("otSite", () => { return { scope: {}, transclude: true, template: ` <div> <header transclude-id="site-head"></header> <nav transclude-id="site-menu"></nav> <main transclude-id="site-body"></main> <footer></footer> </div>` ...
  75. 75. site.js.directive("otSite", () => { ... });
  76. 76. site.js.directive("otSite", () => { ... compile: (tElem, tAttrs, transclude) => { } });
  77. 77. site.js.directive("otSite", () => { ... compile: (tElem, tAttrs, transclude) => { transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  78. 78. site.js.directive("otSite", ($rootScope) => { ... compile: (tElem, tAttrs, transclude) => { transclude($rootScope.$new(), (clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  79. 79. transclusion scope using $rootScope.$new()
  80. 80. proper transclusion scope
  81. 81. site.js.directive("otSite", ($rootScope) => { ... compile: (tElem, tAttrs, transclude) => { transclude($rootScope.$new(), (clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  82. 82. site.js.directive("otSite", () => { ... compile: (tElem, tAttrs, transclude) => { transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  83. 83. site.js.directive("otSite", () => { ... compile: (tElem, tAttrs, transclude) => { return (scope, iElem, iAttrs) => { transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); ...
  84. 84. site.js.directive("otSite", () => { ... compile: (tElem, tAttrs, transclude) => { return (scope, iElem, iAttrs) => { transclude(scope.$parent.$new(), (clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); ...
  85. 85. Code Demo
  86. 86. site.js.directive("otSite", () => { ... compile: (tElem, tAttrs, transclude) => { return (scope, iElem, iAttrs) => { transclude(scope.$parent.$new(), (clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); ...
  87. 87. Transclude function availability compile: (tElem, tAttrs, transclude) => link: (scope, iElem, iAttrs, ctrl, transclude) => controller: ($scope, $element, $transclude) =>
  88. 88. site.js.directive("otSite", () => { ... compile: (tElem, tAttrs, transclude) => { return (scope, iElem, iAttrs) => { transclude(scope.$parent.$new(), (clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); ...
  89. 89. site.js.directive("otSite", () => { ... (scope, iElem, iAttrs) => { transclude(scope.$parent.$new(), (clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  90. 90. site.js.directive("otSite", () => { ... link: (scope, iElem, iAttrs, ctrl, transclude) => { transclude(scope.$parent.$new(), (clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  91. 91. site.js.directive("otSite", () => { ... link: (scope, iElem, iAttrs, ctrl, transclude) => { transclude(scope.$parent.$new(), (clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  92. 92. site.js.directive("otSite", () => { ... link: (scope, iElem, iAttrs, ctrl, transclude) => { transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  93. 93. Code Demo
  94. 94. Transclude function access points compile: (tElem, tAttrs, transclude) => link: (scope, iElem, iAttrs, ctrl, transclude) => controller: ($scope, $element, $transclude) =>
  95. 95. Directive Life Cycle Parent compile 1 2 Child compile Parent controller 3 Parent pre-link 4 5 Child controller 6 Child pre-link 7 Child post-link Parent post-link 8
  96. 96. Transclude function access points compile: (tElem, tAttrs, transclude) => link: (scope, iElem, iAttrs, ctrl, transclude) => controller: ($scope, $element, $transclude) =>
  97. 97. Transclude function in ... link / controller.... ○ Auto-generates scope ○ No wrapper ○ Link: safest for DOM manipulation compile... ○ Scope issues ○ Wrapper code ○ Deprecated
  98. 98. .factory("MultiTransclude", () => { return { }; }); multi-transclude.js
  99. 99. .factory("MultiTransclude", () => { return { transclude: (elem, transcludeFn) => { } }; }); multi-transclude.js
  100. 100. multi-transclude.js.factory("MultiTransclude", () => { return { transclude: (elem, transcludeFn) => { transcludeFn((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  101. 101. site.js.directive("otSite", () => { ... link: (scope, iElem, iAttrs, ctrl, transclude) => { transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  102. 102. site.js.directive("otSite", () => { ... link: (scope, iElem, iAttrs, ctrl, transclude) => { } }; });
  103. 103. site.js.directive("otSite", (MultiTransclude) => { ... link: (scope, iElem, iAttrs, ctrl, transclude) => { } }; });
  104. 104. site.js.directive("otSite", (MultiTransclude) => { ... link: (scope, iElem, iAttrs, ctrl, transclude) => { MultiTransclude.transclude(iElem, transclude); } }; }); Code Demo
  105. 105. Multi-transclusion model Pros Cons Flexible UI Open system All in one directive Requires custom DOM manipulation Extensible Less “HTML-like” Re-usable for any component
  106. 106. Selectable List Design
  107. 107. Selectable List Design
  108. 108. Selectable List Design
  109. 109. Behavioral PatternsBehavioral Patterns
  110. 110. <ot-list /> list.html
  111. 111. <ot-list items="" /> list.html
  112. 112. <ot-list items="" selected="" /> list.html
  113. 113. <ot-list items="" selected="" on-select="" /> list.html
  114. 114. <ot-list items="" selected="" on-select="" theme="" /> list.html
  115. 115. <ot-list items="" selected="" on-select="" theme="" /> list.html
  116. 116. <ot-list items="" selected="" /> list.html
  117. 117. Selectable List Implementation
  118. 118. Areas Navigation
  119. 119. .controller("AppController", ($scope) => { $scope.areas = { list: [ "Floorplan", "Combinations", "Schedule", "Publish" ], current: "Floorplan" }; }); app.js
  120. 120. <ot-site> <div transclude-to="site-head"> I’m transcluding the header. </div> <div transclude-to="site-menu"> I’m transcluding the menu. </div> <div transclude-to="site-body"> I’m transcluding the body. </div> </ot-site> index.html
  121. 121. <ot-site> </ot-site> index.html
  122. 122. <ot-site> <ot-list></ot-list> </ot-site> index.html
  123. 123. <ot-site> <ot-list items="{{ areas.list }}" selected="{{ areas.current }}"></ot-list> </ot-site> index.html
  124. 124. <ot-site> <ot-list items="{{ areas.list }}" selected="{{ areas.current }}" transclude-to="site-body"></ot-list> </ot-site> index.html
  125. 125. .directive("otList", () => { return { template: `` }; }); list.js
  126. 126. list.js.directive("otList", () => { return { template: ` <ul> <li ng-repeat="item in items" ng-bind="item" ng-class="{'ot-selected': item === selected}" ng-click="selectItem(item)"> </li> </ul>` }; ...
  127. 127. list.js.directive("otList", () => { return { template: ... }; });
  128. 128. list.js.directive("otList", () => { return { template: ..., link: (scope, elem, attrs) => { } }; });
  129. 129. list.js.directive("otList", () => { return { template: ..., link: (scope, elem, attrs) => { scope.selectItem = (item) => { scope.selected = item; }; } }; ...
  130. 130. list.js.directive("otList", () => { return { template: ..., link: (scope, elem, attrs) => { scope.items = JSON.parse(attrs.items); scope.selectItem = (item) => { scope.selected = item; }; } }; ...
  131. 131. list.js.directive("otList", () => { return { template: ..., link: (scope, elem, attrs) => { scope.items = JSON.parse(attrs.items); scope.selected = attrs.selected; scope.selectItem = (item) => { scope.selected = item; }; } ...
  132. 132. Code Demo
  133. 133. Shared scope is the default behavior Need to set the scope property No scope set... When asked if shared scope was a good idea...
  134. 134. Apps Navigation
  135. 135. app.js.controller("AppController", ($scope) => { $scope.areas = { list: [ "Floorplan", "Combinations", "Schedule", "Publish" ], current: "Floorplan" }; });
  136. 136. app.js.controller("AppController", ($scope) => { $scope.areas = {...}; });
  137. 137. app.js.controller("AppController", ($scope) => { $scope.areas = {...}; $scope.apps = { list: [ "Marketing", "Planning", "Reservations", "Settings" ], current: "Marketing" }; ...
  138. 138. <ot-site> <ot-list items="{{ areas.list }}" selected="{{ areas.current }}" transclude-to="site-body"></ot-list> </ot-site> index.html
  139. 139. <ot-site> <ot-list items="{{ areas.list }}" selected="{{ areas.current }}" transclude-to="site-body"></ot-list> <ot-list items="{{ apps.list }}" selected="{{ apps.current }}" transclude-to="site-body"></ot-list> </ot-site> index.html
  140. 140. Code Demo
  141. 141. List component, no scope set
  142. 142. list.js.directive("otList", () => { return { template: ..., link: (scope, elem, attrs) => { scope.items = JSON.parse(attrs.items); scope.selected = attrs.selected; scope.selectItem = (item) => { scope.selected = item; }; } ...
  143. 143. list.js.directive("otList", () => { return { template: ..., scope: true, link: (scope, elem, attrs) => { scope.items = JSON.parse(attrs.items); scope.selected = attrs.selected; scope.selectItem = (item) => { scope.selected = item; }; ...
  144. 144. List component, scope: true
  145. 145. Code Demo
  146. 146. List component, scope: true (leak)
  147. 147. list.js.directive("otList", () => { return { template: ..., scope: true, link: (scope, elem, attrs) => { scope.items = JSON.parse(attrs.items); scope.selected = attrs.selected; scope.selectItem = (item) => { scope.selected = item; }; ...
  148. 148. list.js.directive("otList", () => { return { template: ..., scope: {}, link: (scope, elem, attrs) => { scope.items = JSON.parse(attrs.items); scope.selected = attrs.selected; scope.selectItem = (item) => { scope.selected = item; }; ...
  149. 149. List component, scope: { }
  150. 150. list.js.directive("otList", () => { return { template: ..., scope: {}, link: (scope, elem, attrs) => { scope.items = JSON.parse(attrs.items); scope.selected = attrs.selected; scope.selectItem = (item) => { scope.selected = item; }; ...
  151. 151. list.js.directive("otList", () => { return { template: ..., scope: {}, link: (scope, elem, attrs) => { scope.selectItem = (item) => { scope.selected = item; }; } }; });
  152. 152. list.js.directive("otList", () => { return { template: ..., scope: { items: "=items", selected: "=selected" }, link: (scope, elem, attrs) => { scope.selectItem = (item) => { scope.selected = item; }; ...
  153. 153. list.js.directive("otList", () => { return { template: ..., scope: { items: "=", selected: "=" }, link: (scope, elem, attrs) => { scope.selectItem = (item) => { scope.selected = item; }; ...
  154. 154. <ot-site> <ot-list items="{{ areas.list }}" selected="{{ areas.current }}" transclude-to="site-body"></ot-list> <ot-list items="{{ apps.list }}" selected="{{ apps.current }}" transclude-to="site-body"></ot-list> </ot-site> index.html
  155. 155. <ot-site> <ot-list items="areas.list" selected="areas.current" transclude-to="site-body"></ot-list> <ot-list items="apps.list" selected="apps.current" transclude-to="site-body"></ot-list> </ot-site> index.html
  156. 156. <ot-site> <ot-list items="areas.list" selected="areas.current" transclude-to="site-menu"></ot-list> <ot-list items="apps.list" selected="apps.current" transclude-to="site-body"></ot-list> </ot-site> index.html
  157. 157. Code Demo
  158. 158. Attribute passing model Pros Cons Simple interface Not very extensible Closed system Less flexibility for users
  159. 159. Dropdown Design
  160. 160. <ot-dropdown> <ot-trigger /> <ot-target /> </ot-dropdown> dropdown.html
  161. 161. Dropdown Implementation
  162. 162. Apps Navigation
  163. 163. <ot-site> <ot-list items="areas.list" selected="areas.current" transclude-to="site-menu"></ot-list> <ot-list items="apps.list" selected="apps.current" transclude-to="site-body"></ot-list> </ot-site> index.html
  164. 164. <ot-site> ... <ot-list items="apps.list" selected="apps.current" transclude-to="site-body"></ot-list> </ot-site> index.html
  165. 165. <ot-site> ... <ot-dropdown> </ot-dropdown> <ot-list items="apps.list" selected="apps.current" transclude-to="site-body"></ot-list> </ot-site> index.html
  166. 166. <ot-site> ... <ot-dropdown> <ot-list items="apps.list" selected="apps.current" transclude-to="site-body"></ot-list> </ot-dropdown> </ot-site> index.html
  167. 167. <ot-site> ... <ot-dropdown> <ot-list items="apps.list" selected="apps.current" transclude-to="site-body"></ot-list> </ot-dropdown> </ot-site> index.html
  168. 168. <ot-site> ... <ot-dropdown> <ot-list items="apps.list" selected="apps.current"></ot-list> </ot-dropdown> </ot-site> index.html
  169. 169. <ot-site> ... <ot-dropdown transclude-to="site-head"> <ot-list items="apps.list" selected="apps.current"></ot-list> </ot-dropdown> </ot-site> index.html
  170. 170. <ot-site> ... <ot-dropdown> <span ng-bind="apps.current"></span> <ot-list items="apps.list" selected="apps.current"></ot-list> </ot-dropdown> </ot-site> index.html
  171. 171. <ot-site> ... <ot-dropdown> <span ng-bind="apps.current" transclude-to="trigger"></span> <ot-list items="apps.list" selected="apps.current" transclude-to="target"></ot-list> </ot-dropdown> </ot-site> index.html
  172. 172. <ot-site> ... <ot-dropdown> <span ng-bind="apps.current" transclude-to="trigger"></span> <ot-list items="apps.list" selected="apps.current" transclude-to="target"></ot-list> </ot-dropdown> </ot-site> index.html
  173. 173. <ot-site> ... <ot-dropdown> <ot-trigger> <span ng-bind="apps.current"></span> </ot-trigger> <ot-target> <ot-list items="apps.list" selected="apps.current"></ot-list> </ot-target> </ot-dropdown> index.html ...
  174. 174. <ot-site> ... <ot-dropdown> <ot-trigger> <span ng-bind="apps.current" /> </ot-trigger> <ot-target> <ot-list items="apps.list" ... /> </ot-target> </ot-dropdown> </ot-site> index.html
  175. 175. Multi-transclude vs sub-directives ○ “HTML-like” ○ Built with separate parts □ Maintain style of parts separately □ Split options up into separate tags
  176. 176. dropdown.js.directive("otDropdown", () => { return { }; });
  177. 177. dropdown.js.directive("otDropdown", () => { return { scope: {} }; });
  178. 178. dropdown.js.directive("otDropdown", () => { return { scope: {}, transclude: true }; });
  179. 179. dropdown transclusion index.html trigger transclusion target transclusion <ot-site> ... <ot-dropdown> <ot-trigger> <span ng-bind="apps.current" /> </ot-trigger> <ot-target> <ot-list items="apps.list" ... /> </ot-target> </ot-dropdown> </ot-site>
  180. 180. dropdown.js.directive("otDropdown", () => { return { scope: {}, transclude: true, template: ` <div> </div>` }; });
  181. 181. dropdown.js.directive("otDropdown", () => { return { scope: {}, transclude: true, template: ` <div ng-transclude> </div>` }; });
  182. 182. dropdown.js.directive("otDropdown", () => { return { scope: {}, transclude: true, template: ` <div ng-transclude ng-click="toggleTarget()"> </div>` }; });
  183. 183. dropdown.js.directive("otDropdown", () => { return { scope: {}, transclude: true, template: ` <div ng-click="toggleTarget()" ng-transclude> </div>`, link: (scope) => { scope.toggleTarget = () => { scope.targetOpen = !scope.targetOpen; }; ...
  184. 184. .directive("otTarget",()=>{ return { }; }); dropdown.js .directive("otTrigger",()=>{ return { }; });
  185. 185. .directive("otTarget",()=>{ return { transclude: true }; }); dropdown.js .directive("otTrigger",()=>{ return { transclude: true }; });
  186. 186. .directive("otTarget",()=>{ return { transclude: true, template: ` <div> </div>` }; }); dropdown.js .directive("otTrigger",()=>{ return { transclude: true, template: ` <div> </div>` }; });
  187. 187. .directive("otTarget",()=>{ return { transclude: true, template: ` <div ng-transclude> </div>` }; }); dropdown.js .directive("otTrigger",()=>{ return { transclude: true, template: ` <div ng-transclude> </div>` }; });
  188. 188. .directive("otTarget", () => { return { transclude: true, template: ` <div ng-transclude> </div>` }; }); dropdown.js
  189. 189. .directive("otTarget", () => { return { transclude: true, template: ` <div ng-show="targetOpen" ng-transclude> </div>` }; }); dropdown.js
  190. 190. Code Demo
  191. 191. .directive("otTarget", () => { return { transclude: true, template: ` <div ng-show="targetOpen" ng-transclude> </div>` }; }); dropdown.js
  192. 192. Dropdown - trigger/target share scope
  193. 193. Directive to directive communication ○ Controller as directive API ○ “Require” controller from other directives □ link: (scope, elem, attrs, controller) => □ Caveat: must be on element or parents
  194. 194. ○ Controller as directive API ○ “Require” controller from other directives □ link: (scope, elem, attrs, controller) => □ Caveat: must be on element or parents Directive to directive communication
  195. 195. dropdown.js.directive("otDropdown", () => { ... template: ` <div ng-click="toggleTarget()" ng-transclude> </div>`, link: (scope) => { scope.toggleTarget = () => { scope.targetOpen = !scope.targetOpen; }; } });
  196. 196. dropdown.js.directive("otDropdown", () => { ... template: ` <div ng-click="toggleTarget()" ng-transclude> </div>`, controller: function ($scope) { scope.toggleTarget = () => { scope.targetOpen = !scope.targetOpen; }; } });
  197. 197. dropdown.js.directive("otDropdown", () => { ... template: ` <div ng-click="toggleTarget()" ng-transclude> </div>`, controller: function ($scope) { $scope.toggleTarget = () => { scope.targetOpen = !scope.targetOpen; }; } });
  198. 198. dropdown.js.directive("otDropdown", () => { ... template: ` <div ng-click="toggleTarget()" ng-transclude> </div>`, controller: function ($scope) { $scope.toggleTarget = () => { scope.targetOpen = !scope.targetOpen; }; } });
  199. 199. dropdown.js.directive("otDropdown", () => { ... template: ` <div ng-click="toggleTarget()" ng-transclude> </div>`, controller: function ($scope) { $scope.toggleTarget = () => { this.targetOpen = !this.targetOpen; }; } });
  200. 200. ○ Controller as directive API ○ “Require” controller from other directives □ link: (scope, elem, attrs, controller) => □ Caveat: must be on element or parents Directive to directive communication
  201. 201. .directive("otTarget", () => { return { transclude: true, template: ` <div ng-show="targetOpen" ng-transclude> </div>` }; }); dropdown.js
  202. 202. dropdown.js.directive("otTarget", () => { return { transclude: true, require: "^otDropdown", template: ` <div ng-show="targetOpen" ng-transclude> </div>` }; });
  203. 203. dropdown.js.directive("otTarget", () => { return { transclude: true, require: "^otDropdown", template: ` <div ng-show="targetOpen" ng-transclude> </div>`, link: (scope, elem, attrs, ctrl) => { } }; ...
  204. 204. dropdown.js.directive("otTarget", () => { return { transclude: true, require: "^otDropdown", template: ` <div ng-show="targetOpen" ng-transclude> </div>`, link: (scope, elem, attrs, ctrl) => { scope.ctrl = ctrl; } }; ...
  205. 205. dropdown.js.directive("otTarget", () => { return { transclude: true, require: "^otDropdown", template: ` <div ng-show="ctrl.targetOpen" ng-transclude> </div>`, link: (scope, elem, attrs, ctrl) => { scope.ctrl = ctrl; } }; ...
  206. 206. Code Demo
  207. 207. Dropdown - trigger/target share scope
  208. 208. .directive("otTrigger",()=>{ return { transclude: true, template: ` <div ng-transclude> </div>` }; }); .directive("otTarget",()=>{ return { transclude: true, require: "^otDropdown", template: ` <div ng-show="ctrl.targetOpen" ng-transclude> </div>`, link: ... dropdown.js
  209. 209. .directive("otTrigger",()=>{ return { transclude: true, scope: {}, template: ` <div ng-transclude> </div>` }; }); .directive("otTarget",()=>{ return { transclude: true, scope: {}, require: "^otDropdown", template: ` <div ng-show="ctrl.targetOpen" ng-transclude> </div>`, ... dropdown.js
  210. 210. Dropdown isolate scopes
  211. 211. Sub-directive model Pros Cons Flexible UI Open system HTML-ish More boilerplate Can modularize parts of the component Not extensible for new entry points
  212. 212. Application Scaffold Conclusion
  213. 213. <ot-site> <ot-dropdown transclude-to="site-head"> <ot-trigger /> <ot-target> <ot-list items="apps" /> </ot-target> </ot-dropdown> <ot-dropdown transclude-to="site-head"> <ot-trigger /> <ot-target> ... index.html
  214. 214. ... <div class="profile" /> <a href="/logoff" /> </ot-target> </ot-dropdown> <ot-list transclude-to="site-menu" items="areas" /> <div transclude-to="site-body"> <!-- app content --> </div> </ot-site> index.html
  215. 215. <ot-app> <!-- app content --> </ot-app> index.html
  216. 216. <ot-app></ot-app>
  217. 217. Barry Wong Simon Attley Caleb Morrell Sara Rahimian David Amusin ALL OTHER PHOTOS CC0 / SPECIAL THANKS TO RYAN MCGUIRE
  218. 218. We’re hiring! opentable.com/careers/ We’re hiring! opentable.com/careers/
  219. 219. Questions? Rachael L Moore UI Engineer morewry Kara Erickson Web Engineer kara

×