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

Reusable Components in Angular: Architecture, t...

Reusable Components in Angular: Architecture, transclusion, and more

Avatar for Rachael L Moore

Rachael L Moore

January 20, 2015
Tweet

More Decks by Rachael L Moore

Other Decks in Programming

Transcript

  1. SF Angular Meetup OpenTable January 12, 2015 MTV Angular Meetup

    Google January 20, 2015 Reusable Components in Angular Architecture, transclusion, and more
  2. FIREWORKS NIGHT CC BY GPS Consistency Faster development Quality Benefits

    of UI Components Better deployment Accuracy Better focus
  3. FIREWORKS NIGHT CC BY GPS Consistency Faster development Quality Better

    deployment Accuracy Better focus Custom elements Coherent config Benefits of UI Components
  4. Prior art Natural language Volatility How To Design UI Components

    Visual patterns Behavioral patterns Technical limitations
  5. <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)
  6. <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
  7. 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
  8. <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
  9. ES6 Review - Template Strings var name = "Kara"; var

    greeting = "Hi " + name + "!"; // Hi Kara! var name = "Kara"; var greeting = `Hi ${name}!`; // Hi Kara! ES5 ES6
  10. ES6 Review - Fat Arrows var that = this; var

    toggleMenu = function() { that.menuShowing = !that.menuShowing; }; var toggleMenu = () => { this.menuShowing = !this.menuShowing; }; ES5 ES6
  11. .directive("otSite", () => { return { scope: {}, template: `

    <div> <header></header> <nav></nav> <main></main> <footer></footer> </div>` }; site.js ...
  12. <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
  13. .directive("otSite", () => { return { scope: {}, template: `

    <div> <header></header> <nav></nav> <main></main> <footer></main> </div>` }; site.js ...
  14. .directive("otSite", () => { return { scope: {}, transclude: true,

    template: ` <div> <header></header> <nav></nav> <main></main> <footer></main> </div>` site.js ...
  15. .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 ...
  16. .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
  17. .directive("otSite", () => { return { scope: {}, transclude: true,

    template: ` <div> <header></header> <nav></nav> <main></main> <footer></footer> </div>` site.js
  18. <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
  19. <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
  20. .directive("otSite", () => { return { scope: {}, transclude: true,

    template: ` <div> <header></header> <nav></nav> <main></main> <footer></footer> </div>` site.js ...
  21. .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 ...
  22. Custom transclusion For element in clone: 1. Get target ID

    2. Find target element with that ID 3. Append clone element to target
  23. angular.forEach(clone, (cloneEl) => { // get desired target ID //

    find target element with that ID // append element to target }); custom-transclude.js
  24. 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
  25. 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
  26. 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
  27. 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 ...
  28. Two parameters: ◦ scope (optional) ◦ callback function □ passes

    clone Transclude function transclude (clone) => # DOM manipulation
  29. angular.forEach(clone, (cloneEl) => { var tId = ... var target

    = ... if (target.length) {...} else {...} }); custom-transclude.js
  30. transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId =

    ... var target = ... if (target.length) {...} else {...} }); }); custom-transclude.js
  31. Transclude function access points compile: (tElem, tAttrs, transclude) => link:

    (scope, iElem, iAttrs, ctrl, transclude) => controller: ($scope, $element, $transclude) =>
  32. Transclude function access points compile: (tElem, tAttrs, transclude) => link:

    (scope, iElem, iAttrs, ctrl, transclude) => controller: ($scope, $element, $transclude) =>
  33. 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>` ...
  34. site.js .directive("otSite", () => { ... compile: (tElem, tAttrs, transclude)

    => { transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  35. 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 {...} }); }); ...
  36. 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 {...} }); }); ...
  37. site.js .directive("otSite", () => { ... compile: (tElem, tAttrs, transclude)

    => { transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  38. 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 {...} }); ...
  39. 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 {...} }); ...
  40. 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 {...} }); ...
  41. Transclude function availability compile: (tElem, tAttrs, transclude) => link: (scope,

    iElem, iAttrs, ctrl, transclude) => controller: ($scope, $element, $transclude) =>
  42. 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 {...} }); ...
  43. site.js .directive("otSite", () => { ... (scope, iElem, iAttrs) =>

    { transclude(scope.$parent.$new(), (clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  44. 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 {...} }); }); ...
  45. 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 {...} }); }); ...
  46. site.js .directive("otSite", () => { ... link: (scope, iElem, iAttrs,

    ctrl, transclude) => { transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  47. Transclude function access points compile: (tElem, tAttrs, transclude) => link:

    (scope, iElem, iAttrs, ctrl, transclude) => controller: ($scope, $element, $transclude) =>
  48. 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
  49. Transclude function access points compile: (tElem, tAttrs, transclude) => link:

    (scope, iElem, iAttrs, ctrl, transclude) => controller: ($scope, $element, $transclude) =>
  50. Transclude function in ... link / controller.... ◦ Auto-generates scope

    ◦ No wrapper ◦ Link: safest for DOM manipulation compile... ◦ Scope issues ◦ Wrapper code ◦ Deprecated
  51. multi-transclude.js .factory("MultiTransclude", () => { return { transclude: (elem, transcludeFn)

    => { transcludeFn((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  52. site.js .directive("otSite", () => { ... link: (scope, iElem, iAttrs,

    ctrl, transclude) => { transclude((clone) => { angular.forEach(clone, (cloneEl) => { var tId = ... var target = ... if (target.length) {...} else {...} }); }); ...
  53. site.js .directive("otSite", (MultiTransclude) => { ... link: (scope, iElem, iAttrs,

    ctrl, transclude) => { MultiTransclude.transclude(iElem, transclude); } }; }); Code Demo
  54. 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
  55. .controller("AppController", ($scope) => { $scope.areas = { list: [ "Floorplan",

    "Combinations", "Schedule", "Publish" ], current: "Floorplan" }; }); app.js
  56. <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
  57. 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>` }; ...
  58. list.js .directive("otList", () => { return { template: ..., link:

    (scope, elem, attrs) => { scope.selectItem = (item) => { scope.selected = item; }; } }; ...
  59. list.js .directive("otList", () => { return { template: ..., link:

    (scope, elem, attrs) => { scope.items = JSON.parse(attrs.items); scope.selectItem = (item) => { scope.selected = item; }; } }; ...
  60. 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; }; } ...
  61. Shared scope is the default behavior Need to set the

    scope property No scope set... When asked if shared scope was a good idea...
  62. app.js .controller("AppController", ($scope) => { $scope.areas = { list: [

    "Floorplan", "Combinations", "Schedule", "Publish" ], current: "Floorplan" }; });
  63. app.js .controller("AppController", ($scope) => { $scope.areas = {...}; $scope.apps =

    { list: [ "Marketing", "Planning", "Reservations", "Settings" ], current: "Marketing" }; ...
  64. <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
  65. 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; }; } ...
  66. 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; }; ...
  67. 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; }; ...
  68. 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; }; ...
  69. 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; }; ...
  70. list.js .directive("otList", () => { return { template: ..., scope:

    {}, link: (scope, elem, attrs) => { scope.selectItem = (item) => { scope.selected = item; }; } }; });
  71. list.js .directive("otList", () => { return { template: ..., scope:

    { items: "=items", selected: "=selected" }, link: (scope, elem, attrs) => { scope.selectItem = (item) => { scope.selected = item; }; ...
  72. list.js .directive("otList", () => { return { template: ..., scope:

    { items: "=", selected: "=" }, link: (scope, elem, attrs) => { scope.selectItem = (item) => { scope.selected = item; }; ...
  73. <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
  74. <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
  75. Multi-transclude vs sub-directives ◦ “HTML-like” ◦ Built with separate parts

    □ Maintain style of parts separately □ Split options up into separate tags
  76. 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>
  77. dropdown.js .directive("otDropdown", () => { return { scope: {}, transclude:

    true, template: ` <div ng-transclude> </div>` }; });
  78. dropdown.js .directive("otDropdown", () => { return { scope: {}, transclude:

    true, template: ` <div ng-transclude ng-click="toggleTarget()"> </div>` }; });
  79. dropdown.js .directive("otDropdown", () => { return { scope: {}, transclude:

    true, template: ` <div ng-click="toggleTarget()" ng-transclude> </div>`, link: (scope) => { scope.toggleTarget = () => { scope.targetOpen = !scope.targetOpen; }; ...
  80. .directive("otTarget",()=>{ return { transclude: true, template: ` <div> </div>` };

    }); dropdown.js .directive("otTrigger",()=>{ return { transclude: true, template: ` <div> </div>` }; });
  81. .directive("otTarget",()=>{ return { transclude: true, template: ` <div ng-transclude> </div>`

    }; }); dropdown.js .directive("otTrigger",()=>{ return { transclude: true, template: ` <div ng-transclude> </div>` }; });
  82. .directive("otTarget", () => { return { transclude: true, template: `

    <div ng-transclude> </div>` }; }); dropdown.js
  83. .directive("otTarget", () => { return { transclude: true, template: `

    <div ng-show="targetOpen" ng-transclude> </div>` }; }); dropdown.js
  84. .directive("otTarget", () => { return { transclude: true, template: `

    <div ng-show="targetOpen" ng-transclude> </div>` }; }); dropdown.js
  85. 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
  86. ◦ 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
  87. dropdown.js .directive("otDropdown", () => { ... template: ` <div ng-click="toggleTarget()"

    ng-transclude> </div>`, link: (scope) => { scope.toggleTarget = () => { scope.targetOpen = !scope.targetOpen; }; } });
  88. dropdown.js .directive("otDropdown", () => { ... template: ` <div ng-click="toggleTarget()"

    ng-transclude> </div>`, controller: function ($scope) { scope.toggleTarget = () => { scope.targetOpen = !scope.targetOpen; }; } });
  89. dropdown.js .directive("otDropdown", () => { ... template: ` <div ng-click="toggleTarget()"

    ng-transclude> </div>`, controller: function ($scope) { $scope.toggleTarget = () => { scope.targetOpen = !scope.targetOpen; }; } });
  90. dropdown.js .directive("otDropdown", () => { ... template: ` <div ng-click="toggleTarget()"

    ng-transclude> </div>`, controller: function ($scope) { $scope.toggleTarget = () => { scope.targetOpen = !scope.targetOpen; }; } });
  91. dropdown.js .directive("otDropdown", () => { ... template: ` <div ng-click="toggleTarget()"

    ng-transclude> </div>`, controller: function ($scope) { $scope.toggleTarget = () => { this.targetOpen = !this.targetOpen; }; } });
  92. ◦ 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
  93. .directive("otTarget", () => { return { transclude: true, template: `

    <div ng-show="targetOpen" ng-transclude> </div>` }; }); dropdown.js
  94. dropdown.js .directive("otTarget", () => { return { transclude: true, require:

    "^otDropdown", template: ` <div ng-show="targetOpen" ng-transclude> </div>` }; });
  95. dropdown.js .directive("otTarget", () => { return { transclude: true, require:

    "^otDropdown", template: ` <div ng-show="targetOpen" ng-transclude> </div>`, link: (scope, elem, attrs, ctrl) => { } }; ...
  96. 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; } }; ...
  97. 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; } }; ...
  98. .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
  99. .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
  100. 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
  101. <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
  102. ... <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
  103. Barry Wong Simon Attley Caleb Morrell Sara Rahimian David Amusin

    ALL OTHER PHOTOS CC0 / SPECIAL THANKS TO RYAN MCGUIRE