Using Controllers In Directives In AngularJS
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.
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.
Want to use code from this post? Check out the license.
Reader 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?
@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.
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
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
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.
@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.
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?
@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 :)
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...
@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:
Now, imagine that I was actually using a Filter in that ngRepeat:
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 :)
@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
@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.
@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.
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?
@Jonah,
Sounds cool. I've not played with D3 myself, but I just looked it up and the examples look awesome!!
@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.
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 :)
@Shanthi,
Ha ha, thanks, I like Ben too :)
Hey Ben,
I am currently digging through your AngularJS posts and I know I'm late to the party. Big thanks and keep up the nice work. I learnt a ton reading them =)
Now to the topic of discussion.
In slave directive controller (slave.js) you call $scope.reposition() in the reposition function. This I understand cause I know you used ng-controller="SlaveController" in index.htm and SlaveController augments the scope with this method.
This is kind of a problem imo. When I as fellow dev needs to fix something in code like this I need to know alot of stuff. I need to open the page and see the ng-controller attr to understand where that method might come from. I also need to open the SlaveController and see the augmentation.
I guess what I really want to say is that I feel dirty using scope as a transfering object. But is this the ng way to do stuff?
What do you think about requiring the ngController in the DDO instead? e.g.:
require: [ "bnSlave", "^bnMaster", "ngController" ]
Great explanations! I did notice that you are using jQuery. I have tried to avoid using it because it's so tempting to do DOM manipulation with it which is a bad practice when using Angular. (I do wish jqLite would have namespacing though).
Anyway, I just wanted to point out that you can modify the code to eliminate the need for jquery. Just get rid of the namespaces, replace the "off()" methods with "unbind()" and the "on()" methods with "bind()", removing the namespacing of course. Then in the master directive, you have
That can be replaced with:
Great post Ben! Your thoughts on how to approach development using AngularJS is a valuable contribution. Keep up the good work.
I think that directive controllers are essentially the same as regular controllers. i.e. they're the place to manage the scope.
Really the only reason you use a directive controller is that, in order to add a full stack of client side to an HTML element, you only need to add the tag for the directive. The directive then links to its controller.
If you couldn't do this you'd have to add the controller and the directive to the HTML element.
This reminds me of 'RenderAction' in ASP.net MVC. You're in a view when you call it and its like its running a whole page request within your main page request - you get a service call, preparing the ui's data and then some ui rendered.
That's my theory. ;) What do you think of it?