├── app ├── main.js ├── controllers │ ├── slave-controller.js │ └── master-controller.js ├── css │ └── demo.css └── directives │ ├── slave.js │ └── master.js ├── README.md ├── LICENSE.md └── index.htm /app/main.js: -------------------------------------------------------------------------------- 1 | (function( ng ) { 2 | 3 | "use strict"; 4 | 5 | // Define our AngularJS application module. 6 | window.demo = ng.module( "Demo", [] ); 7 | 8 | })( angular ); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # AngularJS Directive Controllers 3 | 4 | by [Ben Nadel][1] 5 | 6 | [View the demo on GitHub Pages][2]. This is an exploration of the use of Controllers in 7 | Directives as a means to facilitate inter-directive communication. 8 | 9 | 10 | [1]: http://www.bennadel.com 11 | [2]: http://bennadel.github.com/AngularJS-Directive-Controllers -------------------------------------------------------------------------------- /app/controllers/slave-controller.js: -------------------------------------------------------------------------------- 1 | (function( ng, app ) { 2 | 3 | "use strict"; 4 | 5 | app.controller( 6 | "SlaveController", 7 | function( $scope ) { 8 | 9 | 10 | // -- Define Scope Methods. --------------------- // 11 | 12 | 13 | // I remove the current slave from the collection. 14 | $scope.remove = function() { 15 | 16 | // Pass this responsibility up the scope chain to the master controller (and its 17 | // collection of slave instances). 18 | $scope.removeSlave( $scope.slave ); 19 | 20 | }; 21 | 22 | 23 | // I reposition the current slave. 24 | $scope.reposition = function( x, y ) { 25 | 26 | $scope.slave.x = x; 27 | $scope.slave.y = y; 28 | 29 | }; 30 | 31 | 32 | } 33 | ); 34 | 35 | })( angular, demo ); -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # The MIT License (MIT) 3 | 4 | Copyright (c) 2013 [Ben Nadel][1] 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | [1]: http://www.bennadel.com -------------------------------------------------------------------------------- /app/controllers/master-controller.js: -------------------------------------------------------------------------------- 1 | (function( ng, app ) { 2 | 3 | "use strict"; 4 | 5 | app.controller( 6 | "MasterController", 7 | function( $scope ) { 8 | 9 | 10 | // -- Define Controller Methods. ---------------- // 11 | 12 | 13 | // I get the next available ID for a new slave. 14 | function getNextID() { 15 | 16 | if ( ! $scope.slaves.length ) { 17 | 18 | return( 1 ); 19 | 20 | } 21 | 22 | var lastSlave = $scope.slaves[ $scope.slaves.length - 1 ]; 23 | 24 | return( lastSlave.id + 1 ); 25 | 26 | } 27 | 28 | 29 | // I get a random coordinate based on the given constraints. 30 | function getRandomCoordinate( minCoordinate, maxCoordinate ) { 31 | 32 | var delta = ( maxCoordinate - minCoordinate ); 33 | 34 | return( 35 | minCoordinate + Math.floor( Math.random() * delta ) 36 | ); 37 | 38 | } 39 | 40 | 41 | // -- Define Scope Methods. --------------------- // 42 | 43 | 44 | // I add a new slave at the given position. 45 | $scope.addSlave = function( x, y ) { 46 | 47 | $scope.slaves.push({ 48 | id: getNextID(), 49 | x: x, 50 | y: y 51 | }); 52 | 53 | }; 54 | 55 | 56 | // I remove the given slave from the collection. 57 | $scope.removeSlave = function( slave ) { 58 | 59 | // Find the slave in the collection. 60 | var index = $scope.slaves.indexOf( slave ); 61 | 62 | // Splice out slave. 63 | $scope.slaves.splice( index, 1 ); 64 | 65 | }; 66 | 67 | 68 | // -- Set Scope Variables. ---------------------- // 69 | 70 | 71 | // This is our list of slaves and their coordinates. Starting with an initial 72 | // collection of one (at a random position). 73 | $scope.slaves = [ 74 | { 75 | id: 1, 76 | x: getRandomCoordinate( 50, 400 ), 77 | y: getRandomCoordinate( 50, 200 ) 78 | } 79 | ]; 80 | 81 | 82 | } 83 | ); 84 | 85 | })( angular, demo ); -------------------------------------------------------------------------------- /app/css/demo.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: tahoma, geneva, sans-serif ; 4 | margin: 0px 0px 0px 0px ; 5 | padding: 0px 0px 0px 0px ; 6 | } 7 | 8 | h1 { 9 | color: #F0F0F0 ; 10 | left: 20px ; 11 | position: fixed ; 12 | top: 20px ; 13 | z-index: 1 ; 14 | } 15 | 16 | div.master { 17 | bottom: 0px ; 18 | left: 0px ; 19 | position: fixed ; 20 | right: 0px ; 21 | top: 0px ; 22 | z-index: 2 ; 23 | } 24 | 25 | ol.handles { 26 | bottom: 0px ; 27 | cursor: pointer ; 28 | left: 0px ; 29 | list-style-type: none ; 30 | margin: 0px 0px 0px 0px ; 31 | padding: 0px 0px 0px 0px ; 32 | position: fixed ; 33 | right: 200px ; 34 | top: 0px ; 35 | } 36 | 37 | ol.handles li { 38 | background-color: #F0F0F0 ; 39 | border: 1px solid #CCCCCC ; 40 | border-radius: 35px 35px 35px 35px ; 41 | cursor: pointer ; 42 | font-weight: bold ; 43 | height: 35px ; 44 | line-height: 34px ; 45 | margin: -17.5px 0px 0px -17.5px ; 46 | padding: 0px 0px 0px 0px ; 47 | position: absolute ; 48 | text-align: center ; 49 | width: 35px ; 50 | } 51 | 52 | ol.leaderboard { 53 | background-color: #F0F0F0 ; 54 | bottom: 0px ; 55 | list-style-type: none ; 56 | margin: 0px 0px 0px 0px ; 57 | overflow: auto ; 58 | padding: 0px 0px 0px 0px ; 59 | position: fixed ; 60 | right: 0px ; 61 | top: 0px ; 62 | width: 200px ; 63 | } 64 | 65 | ol.leaderboard li { 66 | border-bottom: 1px solid #CCCCCC ; 67 | font-size: 16px ; 68 | margin: 0px 0px 0px 0px ; 69 | padding: 5px 0px 0px 0px ; 70 | text-align: center ; 71 | } 72 | 73 | ol.leaderboard div.label { 74 | border: 1px dotted #CCCCCC ; 75 | border-radius: 35px 35px 35px 35px ; 76 | font-weight: bold ; 77 | height: 35px ; 78 | line-height: 34px ; 79 | margin: 0px auto 0px auto ; 80 | width: 35px ; 81 | } 82 | 83 | ol.leaderboard div.position {} 84 | 85 | ol.leaderboard div.position:after { 86 | clear: both ; 87 | content: "" ; 88 | display: block ; 89 | } 90 | 91 | ol.leaderboard span.coordinate { 92 | float: left ; 93 | padding: 5px 0px 10px 0px ; 94 | width: 50% ; 95 | } -------------------------------------------------------------------------------- /index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Using Controllers In Directives In AngularJS 7 | 8 | 9 | 10 | 11 | 12 |

13 | Using Controllers In Directives In AngularJS 14 |

15 | 16 | 17 | 18 |
22 | 23 | 24 | 25 |
    26 |
  1. 32 | 33 | {{ slave.id }} 34 | 35 |
  2. 36 |
37 | 38 | 39 | 40 | 41 |
    42 |
  1. 43 | 44 |
    45 | {{ slave.id }} 46 |
    47 | 48 |
    49 | {{ slave.x }}px 50 | {{ slave.y }}px 51 |
    52 | 53 |
  2. 54 |
55 | 56 | 57 | 58 |
59 | 60 | 61 | 62 | 63 | 67 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/directives/slave.js: -------------------------------------------------------------------------------- 1 | (function( ng, app ) { 2 | 3 | "use strict"; 4 | 5 | app.directive( 6 | "bnSlave", 7 | function( $document ) { 8 | 9 | 10 | // I provide a way for directives to interact using the exposed API. 11 | function Controller( $scope, $element, $attrs ) { 12 | 13 | // -- Define Controller Methods. ------------ // 14 | 15 | 16 | // I move the current element to the given position (delta). Notice that I 17 | // update the CSS of the element directly, rather than using the slave properties. 18 | // This is because the moveTo() will NOT happen inside a $digest (for 19 | // performance reasons). As such, the ngStyle on the element will not have any 20 | // effect on the position resulting from the mouse movement. 21 | function moveTo( deltaX, deltaY ) { 22 | 23 | $element.css({ 24 | left: ( $scope.slave.x + deltaX + "px" ), 25 | top: ( $scope.slave.y + deltaY + "px" ) 26 | }); 27 | 28 | } 29 | 30 | 31 | // I reposition the current slave to the given position (delta). This updates 32 | // the slave directly, as this WILL happen inside of a $digest. 33 | function reposition( deltaX, deltaY ) { 34 | 35 | $scope.reposition( 36 | ( $scope.slave.x + deltaX ), 37 | ( $scope.slave.y + deltaY ) 38 | ); 39 | 40 | } 41 | 42 | 43 | // -- Define Controller Variables. ---------- // 44 | 45 | 46 | // Return public API. 47 | return({ 48 | moveTo: moveTo, 49 | reposition: reposition 50 | }); 51 | 52 | } 53 | 54 | 55 | // I link the $scope to the DOM element and UI events. 56 | function link( $scope, element, attributes, controllers ) { 57 | 58 | 59 | // -- Define Link Methods. ------------------ // 60 | 61 | 62 | // I keep track of the initial click and start tracking movement. 63 | function handleMouseDown( event ) { 64 | 65 | $document.on( "mousemove.bnSlave", handleMouseMove ); 66 | $document.on( "mouseup.bnSlave", handleMouseUp ); 67 | 68 | } 69 | 70 | 71 | // I keep track of whether or not the mouse has been moved; if it has, we are 72 | // no longer going to care about the position of the mouse upon release - we'll 73 | // consider the marker "activated". 74 | function handleMouseMove( event ) { 75 | 76 | $document.off( "mousemove.bnSlave" ); 77 | $document.off( "mouseup.bnSlave" ); 78 | 79 | } 80 | 81 | 82 | // I keep track of the final mouse release - and, remove the slave. If this 83 | // event handler has fired, it means that the mouse-move event was not triggered, 84 | // which means the element has not been moved. 85 | function handleMouseUp( event ) { 86 | 87 | $document.off( "mousemove.bnSlave" ); 88 | $document.off( "mouseup.bnSlave" ); 89 | 90 | // Break the connection to the master controller so the master controller 91 | // cannot send any further communications. 92 | masterController.unbind( slaveController ); 93 | 94 | // Remove the slave from the collection. 95 | $scope.$apply( 96 | function() { 97 | 98 | $scope.remove(); 99 | 100 | } 101 | ); 102 | 103 | } 104 | 105 | 106 | // -- Define Link Variables. ---------------- // 107 | 108 | 109 | // Get the required controllers from the link arguments. 110 | var slaveController = controllers[ 0 ]; 111 | var masterController = controllers[ 1 ]; 112 | 113 | // Listen to position updates from the master controller. When you bind to the 114 | // master controller, we expect to have our controller's moveTo() and reposition() 115 | // methods called. 116 | masterController.bind( slaveController ); 117 | 118 | // Listen to the mouse click in order to start tracking movement changes. 119 | element.on( "mousedown.bnSlave", handleMouseDown ); 120 | 121 | // When the scope is destroyed, make sure to unbind all event handlers to help 122 | // prevent a memory leak. 123 | $scope.$on( 124 | "$destroy", 125 | function( event ) { 126 | 127 | // Clean up the master-slave binding in case this element is removed outside 128 | // of our internal event handling. 129 | masterController.unbind( slaveController ); 130 | 131 | // Clear any existing mouse bindings. 132 | element.off( "mousedown.bnSlave" ); 133 | $document.off( "mousemove.bnSlave" ); 134 | $document.off( "mouseup.bnSlave" ); 135 | 136 | } 137 | ); 138 | 139 | } 140 | 141 | 142 | // Return the directives configuration. 143 | return({ 144 | controller: Controller, 145 | link: link, 146 | require: [ "bnSlave", "^bnMaster" ], 147 | restrict: "A" 148 | }); 149 | 150 | 151 | } 152 | ); 153 | 154 | })( angular, demo ); -------------------------------------------------------------------------------- /app/directives/master.js: -------------------------------------------------------------------------------- 1 | (function( ng, app ) { 2 | 3 | "use strict"; 4 | 5 | app.directive( 6 | "bnMaster", 7 | function() { 8 | 9 | 10 | // I provide a way for directives to interact using the exposed API. 11 | function Controller( $scope ) { 12 | 13 | 14 | // -- Define Controller Methods. ------------ // 15 | 16 | 17 | // I am utility method that applies the given method to the each item using 18 | // the given arguments. 19 | function applyForEach( collection, methodName, methodArguments ) { 20 | 21 | ng.forEach( 22 | collection, 23 | function( item ) { 24 | 25 | item[ methodName ].apply( item, methodArguments ); 26 | } 27 | ); 28 | 29 | } 30 | 31 | 32 | // I add the given listern to the list of subscribers. Each listener must expose 33 | // two methods: moveTo( deltaX, deltaY ) and reposition( deltaX, deltaY ). 34 | function bind( listener ) { 35 | 36 | listeners.push( listener ); 37 | 38 | } 39 | 40 | 41 | // I invoke the moveTo() method on each bound listener. 42 | function moveTo( deltaX, deltaY ) { 43 | 44 | applyForEach( listeners, "moveTo", [ deltaX, deltaY ] ); 45 | 46 | } 47 | 48 | 49 | // I invoke the reposition() method on each bound listener. 50 | function reposition( deltaX, deltaY ) { 51 | 52 | applyForEach( listeners, "reposition", [ deltaX, deltaY ] ); 53 | 54 | } 55 | 56 | 57 | // I unbind the given listener from the list of subscribers. 58 | function unbind( listener ) { 59 | 60 | var index = listeners.indexOf( listener ); 61 | 62 | if ( index === -1 ) { 63 | 64 | return; 65 | 66 | } 67 | 68 | listeners.splice( index, 1 ); 69 | 70 | } 71 | 72 | 73 | // -- Define Controller Variables. ---------- // 74 | 75 | 76 | // I am the collection of listeners that want to know about updated coordinates. 77 | var listeners = []; 78 | 79 | 80 | // Return public API. 81 | return({ 82 | bind: bind, 83 | moveTo: moveTo, 84 | reposition: reposition, 85 | unbind: unbind 86 | }); 87 | 88 | } 89 | 90 | 91 | // I link the $scope to the DOM element and UI events. 92 | function link( $scope, element, attributes, controller ) { 93 | 94 | 95 | // -- Define Link Methods. ------------------ // 96 | 97 | 98 | // I keep track of the initial mouse click on the master. The behavior differs 99 | // depending on whether a slave was clicked; or, the master canvas was clicked. 100 | function handleMouseDown( event ) { 101 | 102 | var target = $( event.target ); 103 | 104 | // Prevent default behavior to stop text selection. 105 | event.preventDefault(); 106 | 107 | // The user clicked on a slave. 108 | if ( target.is( "li.slave" ) ) { 109 | 110 | // Record the initial position of the mouse so we can calculate the 111 | // coordinates of the reposition (using deltas). 112 | initialPageX = event.pageX; 113 | initialPageY = event.pageY; 114 | 115 | // Bind to the movement so we can broadcast new coordinates. 116 | element.on( "mousemove.bnMaster", handleMouseMove ); 117 | element.on( "mouseup.bnMaster", handleMouseUp ); 118 | 119 | // The user clicked on the master canvas directly. We'll use this as an invite 120 | // to create a new slave handle. 121 | } else { 122 | 123 | $scope.$apply( 124 | function() { 125 | 126 | $scope.addSlave( event.pageX, event.pageY ); 127 | 128 | } 129 | ); 130 | 131 | } 132 | 133 | } 134 | 135 | 136 | // I listen for mouse movements to broadcast new position deltas to all of the slaves. 137 | function handleMouseMove( event ) { 138 | 139 | controller.moveTo( 140 | ( event.pageX - initialPageX ), 141 | ( event.pageY - initialPageY ) 142 | ); 143 | 144 | } 145 | 146 | 147 | // I listen for mouse ups to determine when movement has ceased and positions 148 | // of the slaves need to be finalized. 149 | function handleMouseUp( event ) { 150 | 151 | // Now that the user has finished moving the mouse, unbind the mouse events. 152 | element.off( "mousemove.bnMaster" ); 153 | element.off( "mouseup.bnMaster" ); 154 | 155 | // Check to see if the elements have moved at all. If they have not, then there 156 | // is nothing more that the master needs to do. 157 | if ( ! hasMoved( event.pageX, event.pageY ) ) { 158 | 159 | return; 160 | 161 | } 162 | 163 | // Tell all the slaves to finalize positions. 164 | $scope.$apply( 165 | function() { 166 | 167 | controller.reposition( 168 | ( event.pageX - initialPageX ), 169 | ( event.pageY - initialPageY ) 170 | ); 171 | 172 | } 173 | ); 174 | 175 | } 176 | 177 | 178 | // I determine if the given coorindates indicate movement from the original position. 179 | function hasMoved( pageX, pageY ) { 180 | 181 | return( 182 | ( pageX !== initialPageX ) || 183 | ( pageY !== initialPageY ) 184 | ); 185 | 186 | } 187 | 188 | 189 | // -- Define Link Variables. ---------------- // 190 | 191 | 192 | // I hold the initial position of the mouse click. 193 | var initialPageX = null; 194 | var initialPageY = null; 195 | 196 | // Bind to the mouse down event so we can interact with the slaves. 197 | element.on( "mousedown.bnMaster", handleMouseDown ); 198 | 199 | // When the scope is destroyed, make sure to unbind all event handlers to help 200 | // prevent a memory leak. 201 | $scope.$on( 202 | "$destroy", 203 | function( event ) { 204 | 205 | element.off( "mousedown.bnMaster" ); 206 | element.off( "mousemove.bnMaster" ); 207 | element.off( "mouseup.bnMaster" ); 208 | 209 | } 210 | ); 211 | 212 | } 213 | 214 | 215 | // Return the directives configuration. 216 | return({ 217 | controller: Controller, 218 | link: link, 219 | require: "bnMaster", 220 | restrict: "A" 221 | }); 222 | 223 | 224 | } 225 | ); 226 | 227 | })( angular, demo ); --------------------------------------------------------------------------------