Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Simon Free and Todd Sharp and Shannon Hicks and Charlie Arehart and Sean Corfield

My Approach To Building AngularJS Directives That Bind To JavaScript Events

By Ben Nadel on

As I've blogged before, AngularJS has a bit of learning curve. For me, the two biggest hurdles have been request routing and Directives. At this time, I think I have a pretty good handle on request routing and rendering nested views; but some aspects of Directives, on the other hand, are still a bit confusing. I see Directives as having two different (but overlapping) roles: binding JavaScript events to a given $scope and, transforming a given Document Object Model (DOM) tree. While I haven't done too much with regard to transforming or compiling the DOM tree, I'd like to share my current approach to building Directives whose main purpose is binding to JavaScript events.

For me, the key to building clean, cohesive Directives in AngularJS is simply to not build them at all; or rather, to delay building them for as long as possible. If you can build your Controller and your View code without your Directive, then it forces you to encapsulate your data transformations behind the behavior exposed by the Controller and the $scope. In doing so, you can ensure that your Directives remain "clients" of the Controller and do not accidentally become "extensions" to the Controller.

Part I - The Simple Demo


 
 
 

 
  
 
 
 

To explore this concept, let's look at a fairly straightforward example that outputs a list of friends on the screen. In this demo app, we can add a new friend to the list; and, we can remove an existing friend from the list.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • My Approach To Building AngularJS Directives
  • </title>
  •  
  • <style type="text/css">
  •  
  • ul.friends {
  • list-style-type: none ;
  • margin: 0px 0px 0px 0px ;
  • padding: 0px 0px 0px 0px ;
  • width: 400px ;
  • }
  •  
  • ul.friends li {
  • border: 1px solid #CCCCCC ;
  • margin: 0px 0px 7px 0px ;
  • padding: 10px 10px 10px 10px ;
  • position: relative ;
  • }
  •  
  • ul.friends a {
  • color: #CCCCCC ;
  • right: 10px ;
  • position: absolute ;
  • }
  •  
  • ul.friends a:hover {
  • color: #CC0000 ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  • <h1>
  • My Approach To Building AngularJS Directives
  • </h1>
  •  
  • <!-- BEGIN: New Friend Form. -->
  • <form ng-submit="addFriend()">
  •  
  • <p>
  • <input type="text" ng-model="newFriendName" size="30" />
  • <input type="submit" value="Add Friend" />
  • </p>
  •  
  • </form>
  • <!-- END: New Friend Form. -->
  •  
  • <!-- BEGIN: Friend List. -->
  • <ul class="friends">
  •  
  • <li ng-repeat="friend in friends">
  •  
  • {{ friend.name }}
  •  
  • <a ng-click="removeFriend( friend )">Remove</a>
  •  
  • </li>
  •  
  • </ul>
  • <!-- END: Friend List. -->
  •  
  •  
  • <!-- Load jQuery and AngularJS from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//code.jquery.com/jquery-2.0.0.min.js">
  • </script>
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
  • </script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define the root-level controller for the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • // I am the model for new friends.
  • $scope.newFriendName = "";
  •  
  • // Start off with a small set of default friends.
  • $scope.friends = [
  • {
  • id: 1,
  • name: "Tricia"
  • }
  • ];
  •  
  •  
  • // --
  • // PUBLIC METHODS.
  • // --
  •  
  •  
  • // I add a new friend to the collection.
  • $scope.addFriend = function() {
  •  
  • if ( ! $scope.newFriendName ) {
  •  
  • return;
  •  
  • }
  •  
  • $scope.friends.push({
  • id: ( new Date() ).getTime(),
  • name: $scope.newFriendName
  • });
  •  
  • $scope.newFriendName = "";
  •  
  • };
  •  
  •  
  • // I remove the given friend from the collection.
  • $scope.removeFriend = function( friend ) {
  •  
  • var index = $scope.friends.indexOf( friend );
  •  
  • if ( index === -1 ) {
  •  
  • return;
  •  
  • }
  •  
  • $scope.friends.splice( index, 1 );
  •  
  • };
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, we are using a basic ngRepeat to render the friends collection as an unordered-list. Beyond that, there are no constraints or special rendering requirements.

Part II - Adding Constraints And Behavior Without A Directive


 
 
 

 
  
 
 
 

Now that we have our basic demo in place, let's add some rendering constraints that will eventually require the use of a custom AngularJS Directive. Instead of outputting the entire list of friends, let's only show the top-portion of the collection (with an allusion to a subset of hidden items).

At some point, the capacity of the interface to show list items will be influenced by the size of the browser window; but, for now, all we want to think about is exposing some behavior on the $scope that will facilitate the changing of the list capacity.

In the code below, we've had to add some additional data structures and private methods to handle the splitting of the list up into it's visible and hidden portions. But, the real focal point of the code should be the new public method on the $scope: setFriendCapacity(). This method allows the client of the controller to change how many list items can be rendered a given time.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • My Approach To Building AngularJS Directives
  • </title>
  •  
  • <style type="text/css">
  •  
  • div.viewport {
  • border: 1px dashed #FF9999 ;
  • left: 9px ;
  • padding: 10px 10px 3px 10px ;
  • position: fixed ;
  • top: 113px ;
  • }
  •  
  • ul.friends {
  • list-style-type: none ;
  • margin: 0px 0px 0px 0px ;
  • padding: 0px 0px 0px 0px ;
  • width: 400px ;
  • }
  •  
  • ul.friends li {
  • border: 1px solid #CCCCCC ;
  • margin: 0px 0px 7px 0px ;
  • padding: 10px 10px 10px 10px ;
  • position: relative ;
  • }
  •  
  • ul.friends a {
  • color: #CCCCCC ;
  • right: 10px ;
  • position: absolute ;
  • }
  •  
  • ul.friends a:hover {
  • color: #CC0000 ;
  • }
  •  
  • div.teaser {
  • color: #999999 ;
  • font-style: italic ;
  • margin: 12px 0px 7px 0px ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  • <h1>
  • My Approach To Building AngularJS Directives
  • </h1>
  •  
  • <!-- BEGIN: New Friend Form. -->
  • <form ng-submit="addFriend()">
  •  
  • <p>
  • <input type="text" ng-model="newFriendName" size="30" />
  • <input type="submit" value="Add Friend" />
  • </p>
  •  
  • </form>
  • <!-- END: New Friend Form. -->
  •  
  • <!-- BEGIN: Friend List. -->
  • <div class="viewport">
  •  
  • <ul class="friends">
  •  
  • <li ng-repeat="friend in visibleFriends">
  •  
  • {{ friend.name }}
  •  
  • <a ng-click="removeFriend( friend )">Remove</a>
  •  
  • </li>
  •  
  • </ul>
  •  
  • <div ng-show="hiddenFriends.length" class="teaser">
  • ... and {{ hiddenFriends.length }} friend(s) not shown.
  • </div>
  •  
  • </div>
  • <!-- END: Friend List. -->
  •  
  •  
  • <!-- Load jQuery and AngularJS from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//code.jquery.com/jquery-2.0.0.min.js">
  • </script>
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
  • </script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define the root-level controller for the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • // I am the model for new friends.
  • $scope.newFriendName = "";
  •  
  • // Start off with a small set of default friends.
  • $scope.friends = [
  • {
  • id: 1,
  • name: "Tricia"
  • }
  • ];
  •  
  • // I am the collection of visible friends, based on
  • // the current collection and the capacity of the
  • // current interface.
  • $scope.visibleFriends = [];
  •  
  • // I am the collection of hidden friends, not shown
  • // because they exceed the capacity of the current
  • // interface.
  • $scope.hiddenFriends = [];
  •  
  • // I define the number of friends that should be
  • // visible in the current state of the interface.
  • var friendCapacity = 3;
  •  
  • // Divide the friends up into the hidden / visible
  • // breakdown based on the currently-hard-coded
  • // render capacity.
  • applyFriendCapacity();
  •  
  •  
  • // --
  • // PUBLIC METHODS.
  • // --
  •  
  •  
  • // I add a new friend to the collection.
  • $scope.addFriend = function() {
  •  
  • if ( ! $scope.newFriendName ) {
  •  
  • return;
  •  
  • }
  •  
  • $scope.friends.push({
  • id: ( new Date() ).getTime(),
  • name: $scope.newFriendName
  • });
  •  
  • $scope.newFriendName = "";
  •  
  • // Whenever the collection changes, we have to
  • // update the visible / hidden breakdown.
  • applyFriendCapacity();
  •  
  • };
  •  
  •  
  • // I remove the given friend from the collection.
  • $scope.removeFriend = function( friend ) {
  •  
  • var index = $scope.friends.indexOf( friend );
  •  
  • if ( index === -1 ) {
  •  
  • return;
  •  
  • }
  •  
  • $scope.friends.splice( index, 1 );
  •  
  • // Whenever the collection changes, we have to
  • // update the visible / hidden breakdown.
  • applyFriendCapacity();
  •  
  • };
  •  
  •  
  • // I set the capacity of the current interface.
  • $scope.setFriendCapacity = function( newCapacity ) {
  •  
  • friendCapacity = newCapacity;
  •  
  • // Apply the new capacity to the collections.
  • applyFriendCapacity();
  •  
  • };
  •  
  •  
  • // --
  • // PRIVATE METHODS.
  • // --
  •  
  •  
  • // I apply the current capacity to the friends to
  • // update the visible/hidden breakdown.
  • function applyFriendCapacity() {
  •  
  • // If the current interface capacity can hold
  • // of the current friends, simply funnel them all
  • // into the visible friends.
  • if ( friendCapacity >= $scope.friends.length ) {
  •  
  • $scope.visibleFriends = $scope.friends;
  • $scope.hiddenFriends = [];
  •  
  • // If we have more friends that the interface can
  • // hold, funnel the overflow into the hidden
  • // friends collection.
  • } else {
  •  
  • $scope.visibleFriends = $scope.friends.slice( 0, friendCapacity );
  • $scope.hiddenFriends = $scope.friends.slice( friendCapacity );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, this code still has no custom AngularJS directives. All it contains is the Controller, the $scope, and the View. If you look at the add/remove methods, you'll see that the Controller is continually updating the visible / hidden breakdown as the state of the friend collection is changed - but that's hidden away from us. We've also exposed the public method - setFriendCapacity() - but do not yet have any code that invokes it.

Part III - Creating A Directive That Is A Consumer Of $scope


 
 
 

 
  
 
 
 

So far, we've gone as far as we can go without a Directive. We've encapsulated all of our data transformations within our Controller which has forced us to expose a way to change the capacity of the list; but, we haven't actually built any code to consume this new method. This is good - this means that we've kept our responsibilities cleanly divided. And, now that we've enforced good structure, we an go in and add our custom Directive.

In the code below, the only real difference is the addition of the Directive. And, if you look at the Directive code, you'll see that it's only responsibility is to listen for and react to JavaScript events.

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • My Approach To Building AngularJS Directives
  • </title>
  •  
  • <style type="text/css">
  •  
  • div.viewport {
  • border: 1px dashed #FF9999 ;
  • bottom: 10px ;
  • left: 9px ;
  • padding: 10px 10px 3px 10px ;
  • position: fixed ;
  • top: 113px ;
  • }
  •  
  • ul.friends {
  • list-style-type: none ;
  • margin: 0px 0px 0px 0px ;
  • padding: 0px 0px 0px 0px ;
  • width: 400px ;
  • }
  •  
  • ul.friends li {
  • border: 1px solid #CCCCCC ;
  • margin: 0px 0px 7px 0px ;
  • padding: 10px 10px 10px 10px ;
  • position: relative ;
  • }
  •  
  • ul.friends a {
  • color: #CCCCCC ;
  • right: 10px ;
  • position: absolute ;
  • }
  •  
  • ul.friends a:hover {
  • color: #CC0000 ;
  • }
  •  
  • div.teaser {
  • color: #999999 ;
  • font-style: italic ;
  • margin: 12px 0px 7px 0px ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  • <h1>
  • My Approach To Building AngularJS Directives
  • </h1>
  •  
  • <!-- BEGIN: New Friend Form. -->
  • <form ng-submit="addFriend()">
  •  
  • <p>
  • <input type="text" ng-model="newFriendName" size="30" />
  • <input type="submit" value="Add Friend" />
  • </p>
  •  
  • </form>
  • <!-- END: New Friend Form. -->
  •  
  • <!-- BEGIN: Friend List. -->
  • <div bn-viewport class="viewport">
  •  
  • <ul class="friends">
  •  
  • <li ng-repeat="friend in visibleFriends">
  •  
  • {{ friend.name }}
  •  
  • <a ng-click="removeFriend( friend )">Remove</a>
  •  
  • </li>
  •  
  • </ul>
  •  
  • <div ng-show="hiddenFriends.length" class="teaser">
  • ... and {{ hiddenFriends.length }} friend(s) not shown.
  • </div>
  •  
  • </div>
  • <!-- END: Friend List. -->
  •  
  •  
  • <!-- Load jQuery and AngularJS from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//code.jquery.com/jquery-2.0.0.min.js">
  • </script>
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
  • </script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define the root-level controller for the application.
  • app.controller(
  • "AppController",
  • function( $scope ) {
  •  
  • // I am the model for new friends.
  • $scope.newFriendName = "";
  •  
  • // Start off with a small set of default friends.
  • $scope.friends = [
  • {
  • id: 1,
  • name: "Tricia"
  • }
  • ];
  •  
  • // I am the collection of visible friends, based on
  • // the current collection and the capacity of the
  • // current interface.
  • $scope.visibleFriends = [];
  •  
  • // I am the collection of hidden friends, not shown
  • // because they exceed the capacity of the current
  • // interface.
  • $scope.hiddenFriends = [];
  •  
  • // I define the number of friends that should be
  • // visible in the current state of the interface.
  • var friendCapacity = 3;
  •  
  • // Divide the friends up into the hidden / visible
  • // breakdown based on the currently-hard-coded
  • // render capacity.
  • applyFriendCapacity();
  •  
  •  
  • // --
  • // PUBLIC METHODS.
  • // --
  •  
  •  
  • // I add a new friend to the collection.
  • $scope.addFriend = function() {
  •  
  • if ( ! $scope.newFriendName ) {
  •  
  • return;
  •  
  • }
  •  
  • $scope.friends.push({
  • id: ( new Date() ).getTime(),
  • name: $scope.newFriendName
  • });
  •  
  • $scope.newFriendName = "";
  •  
  • // Whenever the collection changes, we have to
  • // update the visible / hidden breakdown.
  • applyFriendCapacity();
  •  
  • };
  •  
  •  
  • // I remove the given friend from the collection.
  • $scope.removeFriend = function( friend ) {
  •  
  • var index = $scope.friends.indexOf( friend );
  •  
  • if ( index === -1 ) {
  •  
  • return;
  •  
  • }
  •  
  • $scope.friends.splice( index, 1 );
  •  
  • // Whenever the collection changes, we have to
  • // update the visible / hidden breakdown.
  • applyFriendCapacity();
  •  
  • };
  •  
  •  
  • // I set the capacity of the current interface.
  • $scope.setFriendCapacity = function( newCapacity ) {
  •  
  • friendCapacity = newCapacity;
  •  
  • // Apply the new capacity to the collections.
  • applyFriendCapacity();
  •  
  • };
  •  
  •  
  • // --
  • // PRIVATE METHODS.
  • // --
  •  
  •  
  • // I apply the current capacity to the friends to
  • // update the visible/hidden breakdown.
  • function applyFriendCapacity() {
  •  
  • // If the current interface capacity can hold
  • // of the current friends, simply funnel them all
  • // into the visible friends.
  • if ( friendCapacity >= $scope.friends.length ) {
  •  
  • $scope.visibleFriends = $scope.friends;
  • $scope.hiddenFriends = [];
  •  
  • // If we have more friends that the interface can
  • // hold, funnel the overflow into the hidden
  • // friends collection.
  • } else {
  •  
  • $scope.visibleFriends = $scope.friends.slice( 0, friendCapacity );
  • $scope.hiddenFriends = $scope.friends.slice( friendCapacity );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I pipe the changes in window size into the parent controller.
  • app.directive(
  • "bnViewport",
  • function() {
  •  
  • // I link the DOM events to the current scope.
  • function link( $scope, element, attributes ) {
  •  
  • // I detect the UI interface capacity and tell the
  • // parent controller about the dynamic value.
  • function updateFriendCapacity() {
  •  
  • // For the sake of the demo, the height of
  • // the items and the teaser are hard-coded
  • // and are not calculated on the fly.
  • var itemHeight = 46;
  • var teaserHeight = 36;
  •  
  • // Get the inner height of the viewport.
  • var viewportHeight = element.height();
  •  
  • // Again, for the sake of the demo, we'll just
  • // say that the available viewport is always
  • // taking the "teaser" into account, even if
  • // it is not going to be rendered.
  • var availableHeight = ( viewportHeight - teaserHeight );
  •  
  • // Add back the amount of "margin collapse"
  • // height that will take place between the
  • // list and the teaser.
  • availableHeight += 5;
  •  
  • // Then, the capacity is simply the number of
  • // items that will fit in the available height.
  • var capacity = Math.floor( availableHeight / itemHeight );
  •  
  • // Let the parent controller know about the
  • // new capacity of the viewport interface.
  • $scope.setFriendCapacity( capacity );
  •  
  • }
  •  
  •  
  • // Update the capacity of the interface.
  • updateFriendCapacity();
  •  
  • // NOTE: Since we are in the link function
  • // execution, we do not need to call the $apply()
  • // method - we are already in the middle of a
  • // monitored lifecycle.
  •  
  •  
  • // When the window resizes, the viewport will also
  • // be resized and capacity of the interface may be
  • // changed.
  • $( window ).on(
  • "resize.bnViewport",
  • function( event ) {
  •  
  • $scope.$apply( updateFriendCapacity );
  •  
  • }
  • );
  •  
  •  
  • // When the scope is destroyed, be sure to unbind
  • // event handler can cause issues.
  • $scope.$on(
  • "$destroy",
  • function() {
  •  
  • $( window ).off( "resize.bnViewport" );
  •  
  • }
  • );
  •  
  • }
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

Our custom AngularJS directive - bnViewport - consumes the DOM and the $scope. When the DOM raises events (ie. window-resize), the Directive queries the DOM state and calls the setFriendCapacity() method (on the $scope) with an appropriate value. Note that the Directive does not directly alter the $scope in any way. Instead, it treats the $scope as an interface that it can consume. The Directive remaines nothing more than a "client" of the $scope.

If we had built the Directive as part of our initial approach, we might have asked it to do all sorts of crazy things like set a friendCapacity variable or even directly change the visibleFriends and hiddenFriends collections. But this would have burdened the Directive with too many responsibilities. By deferring the creation of the Directive for as long as possible, we've ensured that the implementation of the Controller has not leaked into the Directive. This allows both the Controller and the Directive to evolve independently. Directives are definitely complicated; but, using this approach has been a huge help for me.




Reader Comments

An excellent post with an excellent moral.

I have been using AngularJS for a data mining and visualization portal, and deciding where to put logic has been a constant argument (with myself). Say you have to aggregate data on the client side, such as calculating means and variances over several data sets, because you don't want to (or can't) add it to the back end.

You could make an argument for doing it in the service, as that's the interface closest to the API. It would provide the cleanest implementation point, as all controllers and directives would see the statistics. But if the aggregate statistics are expensive to calculate, or if not every consumer would need them, or if the aggregates depend on factors at a higher level (such as view state), then the model isn't appropriate.

Your next argument would be to put the aggregate crunching in the controller that consumes the data, in a fashion similar to what you have done here. This is probably the most modular implementation point, as it would only run when needed, it could apply view state cleanly, and (as you have shown) provides a nice abstraction layer for any directives. But the down side is figuring out which controller scope to put this logic in, as it could be used by multiple controllers and you wouldn't want to duplicate code.

Then, of course, is the argument to put it in the directive. This comes up when the stats are so specific to the view that you feel you must provide a completely self-contained component. This has the highest potential for duplicate code, and for inefficient implementation as it can easily lead to the stats being calculated repeatedly as the component is destroyed and recreated.

And as I type all of this out, I know I'll never get around to putting such a comparison into a blog post. /sigh

Reply to this Comment

@Rick,

100% agreed - deciding where to put things is always a bit of a battle. I know that I end up putting a lot of stuff into the Controller, and I end up duplicating some logic between various controllers. But, I do try to defer some of the logic to non-data services.

For example, in our app, we pass the date/time values from the server to the client as UTC milliseconds (rather than a date/time string). Then, in the Controller, I take that UTC millisecond value and I format it for readbility in the Controller; but, I do so by deferring to a date/time helper service:

  • // someModel.createdAt --> UTC milliseconds.
  • someModel.dateLabel = dateHelper.formatRecentDate( someModel.createdAt );

So, my Controller says, "Ok, take that UTC and inject a new model property with a user-readable date/time string in the browser's local timezone.

I end up doing things like that a lot.

I know there are a lot of formatting filters that people like to use in their view directly; but, I try to remove the invocation of methods on $digest lifecycles as much as possible.

Reply to this Comment

https://hellojs.backliftapp.com/ is insane.
It's a collection of examples for Javascript libraries including Angular.

But get this: you click on which one you want and it builds it into your dropbox folder (with permission of course).

Then if you make a change, the change takes effect immediately.

Reply to this Comment

This might sound like a dumb question (and forgive me if it is) but how do you know when looking at a mockup or coming up with an idea, what belongs in the controller and what needs to be a self-contained directive?

Using your example, could the entire FriendsList not have been an element directive?

  • <bnViewport></bnViewport>

I'm not at all debating just trying to understand what goes where?

Also, maybe aside maybe related, if I were going to open-source my directive or make it drag-and-drop reuseable in another project what is the best approach? This approach seems like the best one but it assumes the consumer knows they need to implement a

  • setFriendCapacity

function within their controller.

Reply to this Comment

Great post. What I like about this approach is that this Directive is not so much about DOM manipulation as it is about DOM `inspection` and `calculation` in order to present the controller with the correct values for the view. That way the rendering is done in the nice, normal, AngularJS way with {{two-way-data-binding}}.

Reply to this Comment

Would love to see just a few less blank lines in the code so as to make it more readable. I use a small screen and there are some sections of your code where all I see in my screen is just comments and blank lines. Otherwise keep up the great work and thanks for all the great tips.

Reply to this Comment

@Phillip,

I love the idea of deploying things automatically to Dropbox - pretty slick.

@Mike,

That's a super excellent question. When I first got into AngularJS, I definitely put WAY TOO much stuff in Directives. I couldn't tell if Directives were supposed to be really big; or if they were supposed to be tiny (like ngClick). And so, I was all over the map. Some of my directives had their own templates (even if they weren't intended to be reused); and some only had like 4 lines of JavaScript in them.

As I've experimented with all these various approaches, I've come to feel that putting too much in a directive ends up leading to friction. Now, I try really hard to not use Directives as much as possible. I ask:

1. Can I do this with directives that already come with AngularJS?

2. Can I replace my need of a directive with the use of an ngRepeat-based Controller?

If not, then I create the Directive that I need; but, using the methodology outlined in the post, I try to delay the actual creation of the directive for as long as possible. The philosophy of AngularJS seems to be that it is explicit. And, by putting fewer things inside of a Directive, I think it forces you to make code that is more easily readable.

As for how to package things for distribution, I can't speak to that to much. I recently tried to distribute a "utility class" for AngularJS, and the only way I could really think to do it was to package it as a module:

http://www.bennadel.com/blog/2472-HashKeyCopier-An-AngularJS-Utility-Class-For-Merging-Cached-And-Live-Data.htm

Then, the class just gets put into the same Dependency-Injection space as the app that requires is. The class exposes an API that the consumer knows how to invoke. So, going back to your question about what the consumer needs to do - the more you can create a "interface" for your code, the less the consumer needs to know about - as in, they simply need to know the API, and not have to worry about the underlying implementation, which is what I've been trying to go for.

Reply to this Comment

@John,

Word up! I'm trying to keep the "good parts" of AngularJS in full effect - the explicit DOM generation and data binding. The directive is there merely to facilitate communication.

Reply to this Comment

@Richard,

I definitely get that a lot. I've been trying to reduce some of my whitespace usage, but old habits die hard :)

Reply to this Comment

hi Ben,

This question might be a little off theme except it is about directives.

Could you please answer a question about passing parameters with limited scope to a directive. I'm currently building an image uploader directive with PHP image processing on the back-end.

I know that you can limit the scope of the controllers model and passed parameters by something like:

scope: {
fileInfo: "&fileInfo",
ngModel: "=ngModel"
},

where fileInfo happens to be generated by a $scope structure in the model which finally gets passed to the PHP to tell it how to process the uploaded images (read-only by the directive) and ngModel is the controllers model (it's bound to an image source in the directives' template) which the directive can write the uploaded file name to; the fileName is generated by the PHP.

Too complicated to post the whole code but this limiting of scope in the directive doesn't stop the writer of the directive to access the controllers scope by the following construct or similar.

var evalStr = $scope.$parent.$parent.modelVariableName = 'something';
eval(evalStr);

Nasty I know but AngularJS can't stop someone doing this. Shouldn't the controller scope be completely hidden from the directive? How can I stop it being visible (and enforce other programmers to adopt good practice)?

Thanks
Nick

Reply to this Comment

This was a great 3peat. Thanks for the workd.

I'd like to point out what got me stuck, not coming from an angular background.

When a directive is named bnViewport it actually is declared in the html as bn-viewport. Per the "normalization" instructions here:
http://docs.angularjs.org/guide/directive#matching-directives

You'll find bn-viewport declared on a div in the html above.
That how angular knows to bind it to the controller's $scope.

Reply to this Comment

I don't want to kick in any windows.
But why do you use jquery in every angularjs solution??
It kinda looks like cheating the challenge to me....

Unless of course you rename your titles and description appropriately to include the use of other javascript libraries/frameworks.

Reply to this Comment

@DrAnders,

I am not sure I understand what you are saying? AngularJS and jQuery are tightly integrated. If you include jQuery, AngularJS will automatically use it when it wraps up its DOM references. Even if you don't include it, AngularJS exposes jQLite, which is a subset of jQuery.

Can you further explain what you are trying to say?

Reply to this Comment

@Nick,

I know this is an old question, and you've probably moved on, but I just wanted to say that I don't have too much experience with Isolate scope directives. When I first started building directives, I tried to use Isolate scope; but, I found that it ended up causing more headaches. Probably because my use-case wasn't the one intended for Isolate scope (which I believe is oriented towards directives that transclude templates).

That said, my understanding is that an isolated scope isn't entirely out of the scope chain - it simply branches the scope chain. Meaning, it can still access the parent scope; but, the DOM-related children don't necessarily inherit *its scope*.

I think about it like creating a "sibling" scope, not a "detached" scope... but again, I don't have any real experience with Isolate; so, what I'm saying may not be accurate.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.