Using Controllers In Directives In AngularJS

Posted February 1, 2013 at 5:46 PM by Ben Nadel

Tags: Javascript / DHTML

In AngularJS, you have your Views, which present data to the user; you have your Controllers, which manage the $scope (ie. view model) and expose behavior to the View; and, you have your Directives, which link user interactions to $scope behaviors. But then you also have a special kind of Controller - a Directive Controller. The Directive Controller is defined within the context of one directive; but, it can be injected into other directives as a means to facilitate inter-directive communication.


 
 
 

 
  
 
 
 

View this code on GitHub.

View this demo on GitHub.

When you start using AngularJS, you definitely need to change the way you think about application architecture. AngularJS enforces a very strict separation of responsibilities, making sure that your DOM updates are abstracted away from your model updates. Directives act as the layer that keeps these two aspects loosely coupled. One one hand, directives translate data into user interfaces; and, on the other hand, directives translate user interactions back into $scope behaviors.

So, where do Directive Controllers fit into this model? Well, I'm still not 100% sure. So far, I've only just begun to really play around with directive controllers and how inter-directive communication works. As such, I'm not sure I can even codify underlying rules. Here's what I think as of this writing:

  • Link functions capture user behavior.
  • Link functions execute $scope.$apply() calls.
  • Directive controllers can assume an active $digest, given the rule above.
  • Directive controllers can alter the DOM, but should defer to the Link functions for user interactions.

Those are just some raw thoughts, so it may not make too much sense. That said, between Views, Controllers, Directives, and Directive Controllers, how do you figure out what functionality goes where?

As a rule of thumb, I've been trying to build my views as if I didn't have any JavaScript available - as if I was simply rendering data. Then, I use directives to add the JavaScript-driven features back in. To demonstrate, I've created a Master-Slave, drag-drop project. In it, I have a master canvas and collection of slave "handles." The user can click on one of the slave handles and move it around. The master canvas then forces all slave handles to move in unison.

This requires a good bit of JavaScript interaction. But, how did I build the page? I built it as if I had no JavaScript; I built it as if I simply had a list of items:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>Using Controllers In Directives In AngularJS</title>
  •  
  • <link rel="stylesheet" type="text/css" href="app/css/demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Using Controllers In Directives In AngularJS
  • </h1>
  •  
  •  
  • <!-- BEGIN: Master Canvas. -->
  • <div
  • ng-controller="MasterController"
  • bn-master
  • class="master">
  •  
  •  
  • <!-- BEGIN: Slave Handles. -->
  • <ol class="handles">
  • <li
  • ng-repeat="slave in slaves"
  • ng-controller="SlaveController"
  • bn-slave
  • class="slave"
  • ng-style="{ left: ( slave.x + 'px' ), top: ( slave.y + 'px' ) }">
  •  
  • {{ slave.id }}
  •  
  • </li>
  • </ol>
  • <!-- END: Slave Handles. -->
  •  
  •  
  • <!-- BEGIN: Slave Leaderboard. -->
  • <ol class="leaderboard">
  • <li ng-repeat="slave in slaves">
  •  
  • <div class="label">
  • {{ slave.id }}
  • </div>
  •  
  • <div class="position">
  • <span class="coordinate">{{ slave.x }}px</span>
  • <span class="coordinate">{{ slave.y }}px</span>
  • </div>
  •  
  • </li>
  • </ol>
  • <!-- END: Slave Leaderboard. -->
  •  
  •  
  • </div>
  • <!-- END: Master Canvas. -->
  •  
  •  
  • <!-- Load jQuery and AngularJS from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//code.jquery.com/jquery-1.9.0.min.js">
  • </script>
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js">
  • </script>
  •  
  • <!-- Load the app module and its classes. -->
  • <script type="text/javascript" src="app/main.js"></script>
  • <script type="text/javascript" src="app/controllers/master-controller.js"></script>
  • <script type="text/javascript" src="app/controllers/slave-controller.js"></script>
  • <script type="text/javascript" src="app/directives/master.js"></script>
  • <script type="text/javascript" src="app/directives/slave.js"></script>
  •  
  • </body>
  • </html>

As you can see, this HTML renders an ordered list. It's the directives - bnMaster and bnSlave - that add all of the user-interaction JavaScript. There's a decent amount of code that goes into this demo, so I'll leave that on the GitHub repo rather than cover it here.

That said, there is one underlying concept that I want to drive home: as you add user interaction behavior, minimize the number of $digests that run. While AngularJS allows you to render a view based on $scope data, you should try to use direct DOM manipulation as much as possible while in the context of a directive. Defer updating the $scope and invoking $scope.$apply() as long as possible. This will keep your page feeing much more responsive.

If you look at the code above, you can see that the position of each slave handle is defined by an ngStyle directive; this maps the $scope data onto CSS properties. However, if you look at the video (or use try the code), you'll notice that the X/Y coordinates in the "leaderboard" don't update until you actually release the mouse. This is because I am deferring $scope updates (and subsequent $digests) until the movement has concluded.

The more I dig into the AngularJS, the more I really love it! The separation of concerns is awesome; and, I think it leads to a much more maintainable application. Figuring out what goes where, however, can be a little confusing at times. Hopefully this exploration may help.




Reader Comments

Feb 2, 2013 at 1:12 PM // reply »
4 Comments

All very cool Ben!
At twpm we've been using knockout.js for a few months now and are blown away with it. The syntax is very elegant and intuitive. Have you look into it?


Feb 3, 2013 at 2:00 PM // reply »
11,238 Comments

@Topper,

I can't say that I've really given it much of a look; but, I have heard a number of people sing it's praises. From some quick Googling, it looks like they have a number of similar philosophies.


Feb 7, 2013 at 5:13 AM // reply »
1 Comments

Hi Ben, first let me say thank you very much for these posts on Angular. I have just started with the framework and reading your articles has helped.

I am still very confused with directive controllers, however. In particular I am still not clear on exactly what scopes/controllers are being fetched with the

  • require: ["bnSlave", "^bnMaster"]

line in the slave directive. Is the slave getting the master's directive controller or the master-controller? I also found it interesting that removing the

  • require: ["bnMaster"]

from the master directive seemed to make no difference to the behaviour of the program whatsoever. Interesting but also confusing :)

I am trying to draw a dependency diagram for your program but still not 100% sure on the links between the six things: two regular controllers, two directive controllers and the directives themselves.

All the best,
Chad.


Feb 13, 2013 at 9:09 PM // reply »
11,238 Comments

@Chad,

I'm glad that you're finding these posts useful! AngularJS had / has a steep learning curve for me, so I'm thrilled that sharing some of the trickier concepts is helpful.

When a Directive requires a Controller, it is only requiring a "directive controller". The directive doesn't have any access to the ngController-based controllers. The only thing that a directive and an ngController has is that they share (or at least share by default) the same $scope reference. The $scope is the way a directive can interact with the ngController controller.

I am surprised that removing "require: bnMaster" allowed the program to continue to run. I would think that would render the "controller" argument passed to the bnMaster link() function to be undefined... maybe a directive always receives its own controller if it is defined?? Not really sure :)

I hope that helps clear it up a bit. I know the terminology is confusing with the various "controller" types being talked about.


Feb 26, 2013 at 12:19 AM // reply »
6 Comments

  • element.on( "mousedown.bnMaster", handleMouseDown );

. From the code above, i was of the thought that the master canvas element will be having a css class called 'bnMaster', but i could not find such a class in it. What does 'mousedown.bnMaster' refers to and how does that differ from 'mousedown'. I have the same doubt for the slaves controller as well. Searching in google gave me only the 'mousedown', 'mouseup' things. Could you pls. explain?


Mar 23, 2013 at 9:54 AM // reply »
11,238 Comments

@Rajkamal,

The ".bnMaster" is a "namespace" for the event binding. This is a core feature of the jQuery event publish/subscribe mechanism and is not specific to AngularJS is anyway.

The namespace allows for events to be unbound without the reference to the original function reference.

Imagine that I had an element that had several different event bindings of the same event type:

A.on( "click", handlerA );
A.on( "click.bThing", handlerB );
A.on( "click", handlerC );

With this configuration, imagine that you wanted to unbind ONLY the handlerB subscriber. Since it has a ".bThing" namespace, all you have to do is:

A.off( "click.bThing" )

This will unbind the handlerB, while leaving the other 2 event handlers in place.

Now, imagine you wanted to unsubscribe the handlerC? It has no namespace. As such, you canNOT just call A.off( "click" ) as this will unbind the other two event handlers as well. What you would have to do is call off() with the original handler reference:

A.off( "click", handlerC );

This is the only way you can unbind the non-namespace event type without accidentally unbind all the other click handlers on the same element.

Here is a quote from the http://api.jquery.com/off/ page:

If a simple event name such as "click" is provided, all events of that type (both direct and delegated) are removed from the elements in the jQuery set. When writing code that will be used as a plugin, or simply when working with a large code base, best practice is to attach and remove events using namespaces so that the code will not inadvertently remove event handlers attached by other code. All events of all types in a specific namespace can be removed from an element by providing just a namespace, such as ".myPlugin". At minimum, either a namespace or event name must be provided.

Hope that helps a bit :)


Mar 26, 2013 at 10:43 PM // reply »
11 Comments

Hi Ben,

Love these tutorials. Quick question: Did you try a version where all the numbers updated constantly during mouse move, as opposed to only updating on mouse up? You mentioned performance considerations, but, if you think about it, that kind of updating really shouldn't be too resource intensive. If it is, it makes me hesitant to use angular for anything that requires much animation.

As an example, d3.js could handle that kind of updating quite easily on its own. Is the angular engine doing things in an inefficient way?

Thanks for any thoughts on this...


Mar 28, 2013 at 8:24 PM // reply »
11,238 Comments

@Jonah,

I don't think there's a definitive answer for that - it's going to depend on how "expensive" your digest cycles are. That's one thing that you really have to wrap your head around when it comes to AngularJS and "dirty checking."

Right now, the page is a just a white-page example, so you are probably correct - updating the values on every mouse-move using the AngularJS data binding probably wouldn't be a problem. But, since AngularJS using dirty checking (as opposed to "observers"), you can't localize your changes.

By that, I mean that there is no way to say to AngularJS, "Apply the digest to this set of variables, but NOT to these other set of variables." It's and all or nothing approach. That means that for every mouse move that forces the $digest to update the bindings, AngularJS is going to re-test ALL data bindings currently be rendering on the page.

To give that some context, look at my ngRepeat statement:

  • ng-repeat="slave in slaves"

Now, imagine that I was actually using a Filter in that ngRepeat:

  • ng-repeat="slave in slaves | limitTo:5"

Here, I'm using the built-in filter ( http://docs.angularjs.org/api/ng.filter:limitTo ) to make sure that my slaves array never exceeds 5 in length.

Ok, now, going back to the $digest concept, if I have to trigger a $digest on every mouse move, that filter will ALSO get re-executed for every mouse move. This is because, for every single digest, AngularJS has to check the slaves array and re-apply the filter to make sure that nothing has changed.

The more data bindings, the more expensive this can become.

Now, JavaScript can do a boat load of calculations in a short time - I think the AngularJS team says that anything below 20,000 JavaScript operations a second shouldn't be noticeable. But, the more bindings with things like filters, the faster this tipping point is reached.

So, going back to your question, there's nothing inherently good or bad about updating the values with each mouse move using AngularJS - it depends on how much work each $digest has to do.

Hope that helps clarify the thinking - hope I didn't ramble too much :)


Mar 29, 2013 at 3:56 AM // reply »
11 Comments

@Ben,

If by "ramble" you mean "produce the most thorough response I've seen in all my years replying to blog posts," the yes :)

I couldn't have hoped for a better answer, and really appreciate your taking the time to write it up.

It sounds like the standard premature optimization advice might be applicable to Angular as well: Just use angular as you want to until you see performance degrade, and then worry about hacking around it.

In this case, "hacking around it" might mean moving the code that updates the coordinates as the mouse moves outside the angular lifecycle -- eg, using d3 drag events instead of watching for changes with angular. You obviously lose the cleanliness of having everything within the angular system, but it seems like a decent workaround for cases where it's needed, right?

Thanks again for the great answer,
Jonah


Apr 2, 2013 at 5:06 PM // reply »
11,238 Comments

@Jonah,

Ha ha, thanks :) Glad it was helpful then!

I think you're right about premature optimization. In general, I just go with the flow in an AngularJS application. That said, drag-n-drop is something that I have actually seen cause some issues with "choppiness" of the drag animation. But, this was in a context that had a ton of filters being fired on every $digest.

We've been slowly refactoring the way some of the problematic UI was built. If there weren't so many filters, then the drag-n-drop may have never been symptomatic.


Apr 2, 2013 at 5:41 PM // reply »
11 Comments

@Ben,

Thanks again. Just as a slightly OT addendum, in case you (or anyone reading this) uses d3, you actually have to be a little careful even there to get good performance on drag events.

In particular, some of the examples I've come across have you using d3.select(this) within the listener function, but that can be a CPU hog. I've been instead creating a separate function for each element's listener (via a factory function), and the CPU usage is about 1/2 to 1/3 of the d3.select(this) method, presumably because an expensive object creation (creation of the selection) is avoided.


Apr 3, 2013 at 9:03 AM // reply »
2 Comments

hi, i am new to this angularjs, may i know how many ways we can define application module?
1>var myApp = angular.module('myApp',[]);
myApp.config(....)
myApp.directive(---)
2> (function( ng ) {

"use strict";

// Define our AngularJS application module.
window.demo = ng.module( "Demo", [] );

})( angular );
3>angular.module('myApp',[]).
directive('directiveName',function(){})
-------------------------------
1st and 3rd are similar,almost same, but 2nd is the code you have written in main.js file. It seems like jquery representation, may i know what is the difference between 1st and 2nd??

Are there any other ways to define application module?


Apr 3, 2013 at 9:24 AM // reply »
11,238 Comments

@Jonah,

Sounds cool. I've not played with D3 myself, but I just looked it up and the examples look awesome!!


Apr 3, 2013 at 9:25 AM // reply »
11,238 Comments

@Shanthi,

I think all of those examples are doing the same thing. You have to call:

angular.module()

... in order to define your AngularJS module. The only difference between those three examples is where you're storing the module reference; and, whether or not you are creating an alias for the "angular" object.


Apr 3, 2013 at 9:45 AM // reply »
2 Comments

Hi ben, thanks for the quick reply, yeah am seeing all parts, it is different way of representation, but the purpose is same! In angular, people using different types of representations, so getting confused some times!! By the way, i like the name Ben :)


Apr 4, 2013 at 9:18 AM // reply »
11,238 Comments

@Shanthi,

Ha ha, thanks, I like Ben too :)



Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 19, 2013 at 2:31 PM
My Experience With AngularJS - The Super-heroic JavaScript MVW Framework
It's funny really just how well that image describes the way I would imagine most people that go with angular for some project is. I have had a similar roller-coaster ride with it as well, but not qu ... read »
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 10:37 PM
Very Simple Pusher And ColdFusion Powered Chat
hi id making plz easy ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools