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 the jQuery Conference 2010 (Boston, MA) with:

Creating A Custom Show / Hide Directive In AngularJS

By Ben Nadel on

As I expressed earlier, I've been loving AngularJS. It's a powerful JavaScript web application framework; but, it does have a fairly steep learning curve. One of the most difficult things for me to really wrap my head around was how to best leverage Directives. In AngularJS, a directive is where your application's custom DOM (Document Object Model) manipulation goes. Essentially, a directive allows you to pipe user-based events into your AngularJS context.


 
 
 

 
  
 
 
 

There's no doubt that Directives are the most complex part of AngularJS; as such, I've found that you want to try to keep your directives as small and cohesive as possible. Some get rather large (depending on what they do); but, when you first start learning how directives work, start small and iterate!

To demonstrate, I thought I'd show you how to create a custom show/hide directive in AngularJS. Now, AngularJS already has the ngShow and ngHide directives; but, these work by simply setting the "display" CSS of the element. Often times, we want our changes in visibility to be a bit more elegant, using something like jQuery's slideDown() or fadeIn() effects. For this demo, we'll create a custom directive that uses the slideDown() and slideUp() methods to show and hide an element, respectively.

Our directive will use the name, "bnSlideShow." I know - it's a misleading name. The bnSlideShow directive will toggle the state of an element based on the value of a truthy $scope (ie. View Model) expression. When the $scope expression is false, the element will slideUp(); when the $scope expression is true, the element will slideDown().

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="AppController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>Creating A Custom "Show" Directive In AngularJS</title>
  •  
  • <style type="text/css">
  •  
  • ul {
  • height: 100px ;
  • list-style-type: none ;
  • margin: 0px 0px 0px 0px ;
  • padding: 0px 0px 0px 0px ;
  • }
  •  
  • li {
  • border: 1px solid #333333 ;
  • float: left ;
  • height: 100px ;
  • line-height: 100px ;
  • margin-right: 10px ;
  • overflow: hidden ;
  • text-align: center ;
  • width: 200px ;
  • }
  •  
  • </style>
  • </head>
  • <body>
  •  
  •  
  • <h1>
  • Creating A Custom "Show" Directive In AngularJS
  • </h1>
  •  
  • <p>
  • <a ng-click="toggle()">Toggle Elements</a>
  • </p>
  •  
  • <ul>
  • <li bn-slide-show="isVisible" slide-show-duration="2000">
  •  
  • Using bnSlideShow
  •  
  • </li>
  • <li ng-show="isVisible">
  •  
  • Using ngShow
  •  
  • </li>
  • </ul>
  •  
  •  
  • <!--
  • Load jQuery andAngularJS from the CDN. In order for
  • AngularJS to use jQuery instead of its own jQLite, we
  • have to make sure jQuery is loaded first.
  • -->
  • <script
  • type="text/javascript"
  • src="//code.jquery.com/jquery-1.8.3.min.js">
  • </script>
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js">
  • </script>
  • <script type="text/javascript">
  •  
  •  
  • // Create an application module for our demo.
  • var Demo = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Define our root-level controller for the application.
  • Demo.controller(
  • "AppController",
  • function( $scope, $route, $routeParams ){
  •  
  • // I toggle the value of isVisible.
  • $scope.toggle = function() {
  •  
  • $scope.isVisible = ! $scope.isVisible;
  •  
  • };
  •  
  • // Default the blocks to be visible.
  • $scope.isVisible = true;
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I hide and show elements based on the given model value.
  • // However, rather than using "display" style, I use jQuery's
  • // slideDown() / slideUp().
  • Demo.directive(
  • "bnSlideShow",
  • function() {
  •  
  • // I allow an instance of the directive to be hooked
  • // into the user-interaction model outside of the
  • // AngularJS context.
  • function link( $scope, element, attributes ) {
  •  
  • // I am the TRUTHY expression to watch.
  • var expression = attributes.bnSlideShow;
  •  
  • // I am the optional slide duration.
  • var duration = ( attributes.slideShowDuration || "fast" );
  •  
  •  
  • // I check to see the default display of the
  • // element based on the link-time value of the
  • // model we are watching.
  • if ( ! $scope.$eval( expression ) ) {
  •  
  • element.hide();
  •  
  • }
  •  
  •  
  • // I watch the expression in $scope context to
  • // see when it changes - and adjust the visibility
  • // of the element accordingly.
  • $scope.$watch(
  • expression,
  • function( newValue, oldValue ) {
  •  
  • // Ignore first-run values since we've
  • // already defaulted the element state.
  • if ( newValue === oldValue ) {
  •  
  • return;
  •  
  • }
  •  
  • // Show element.
  • if ( newValue ) {
  •  
  • element
  • .stop( true, true )
  • .slideDown( duration )
  • ;
  •  
  • // Hide element.
  • } else {
  •  
  • element
  • .stop( true, true )
  • .slideUp( duration )
  • ;
  •  
  • }
  •  
  • }
  • );
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

When mapping a $scope expression onto the state of an element, we need to worry about two things: the initial state and the change in state. Within our directive, we can test the initial state by evaluating the passed-in expression (ie. isVisible) in the context of the $scope. We can then use the $watch() method to observe changes in state over time. Notice that our initial "hide" uses .hide() where as our subsequent "hide" uses .slideUp(). This is because we don't want our DOM to start out by sliding-up if the View Model expression is false; rather, we want it to start out hidden.

Maintaining a 100% complete separation between your Controller and your DOM (Document Object Model) can, at times, feel like a Herculean task. The key to this approach requires a solid understanding of AngularJS Directives and how they can map View Model values onto the DOM, and vice-versa. Hopefully, examples like this will start to make that task seem easier.




Reader Comments

Ben, keep up the great work! We've been using AngularJS in one of our projects and all of your posts have been truly helpful.

Wouldn't it be awesome if we had some kind of AngularJS directive library where people can submit their own directives?

Reply to this Comment

This is how I would write this directive:

  • Demo.directive(
  • "bnSlideShow",
  • function() {
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A",
  • scope: {
  • show: '=bnSlideShow',
  • duration: '=slideShowDuration'
  • }
  • });
  •  
  • // I allow an instance of the directive to be hooked
  • // into the user-interaction model outside of the
  • // AngularJS context.
  • function link( $scope, element, attributes ) {
  • console.log($scope.show, $scope.duration);
  • // I watch the expression in $scope context to
  • // see when it changes - and adjust the visibility
  • // of the element accordingly.
  • $scope.$watch(
  • 'show',
  • function( newValue, oldValue ) {
  •  
  • // Ignore first-run values since we've
  • // already defaulted the element state.
  • if ( newValue === oldValue ) {
  • return;
  • }
  •  
  • // Show element.
  • if ( newValue ) {
  • element
  • .stop( true, true )
  • .slideDown( $scope.duration )
  • ;
  • // Hide element.
  • } else {
  •  
  • element
  • .stop( true, true )
  • .slideUp( $scope.duration )
  • ;
  • }
  • }
  • );
  • }
  • }
  • );

Reply to this Comment

KnockoutJS has a similiar concept of a 'bindingHandler' - where you have DOM specific code.

The key here is that testing the DOM is hard. By wrapping up DOM access in Directives, one can more easily test the application.

By creating 'widgets' or wrapping 3rd party widgets (ie jQuery) it will create a better environment. The painful part is basically having to write that code to wrap all the controls :)

Reply to this Comment

@Rick,

Very interesting stuff. We started out using something called AngularUI, which has some bootstrap and jQuery integration points as well as some additional features like being able to use "ui-if" instead of "ng-show" to actually remove DOM elements from the markup.

http://angular-ui.github.com/

I think a number of the libraries are good to get you going. But, what I found was that as soon as your requirements start to get a bit more complicated, you start needing to roll your own stuff that takes care of edge cases.

That said, I do use AngularUI's ui-if quite a bit.

Reply to this Comment

@Edgar,

I see that you're using the scope isolation. I go back and forth on that. Do you have any rules on thumb for how often you do it? Or how you decide when to do it and when not to bother?

Reply to this Comment

@Steve,

I've heard some good things about KnockoutJS, but have not had a chance to play around with it yet. And testing is definitely something that I want to get much better at. Right now, I've only dabbled in it a bit with MXUnit and Jasmine. But, I've not really ever written tests for a "production" app :(

I try to have "2" different stories, when I can: the blog post and the code... and then the video. I feel like they compliment each other; and I find that it's nice to watch the video to get the lay-of-the-land before I dive into the text and code. Thanks!

Reply to this Comment

Thanks, and don't get me wrong, not 'promoting' KnockoutJs, but in a large SPA project I worked on when using KnockoutJs, one of our principles was to make sure we weren't using DOM accessors without being wrapped in a bindingHandler. Testing becomes soooo much easier.

I really like to see this same mentality with the AngularJs directives. I'm leaning toward AngularJs for future projects because I believe it has more 'application building' aspects that I had to create on top of KnockoutJs (and I see the same in frameworks such as Backbone) - they provide building blocks, but you end up including additional libraries, etc... (ie. for KnockoutJs there is nothing for routing).

I've been looking at Ember and Angular, I like what I see in both, but leaning toward AngularJs

Reply to this Comment

@Steve,

I hear people also say great things about Ember. One thing that I really like about AngularJS, however, is that it works (much) of its magic through brute-force dirty-checking. I think a number of the other frameworks use getter/setter interception to know if data has changed. But AngularJS simply checks your structures in each $digest to see if things have changed. This allows you to keep your data structures very simple!

The trade-off, of course, is that you have to be careful / mindful about what you need to check in each $digest. At best, it's all simple-value checks - nothing that requires calculation otherwise the browse may begin to choke.

Reply to this Comment

Ben, thanks, I enjoy your blog. Since you started covering Angular, I have checked it out, but I'm wondering what the benefits are over just using jQuery. I'm just wondering if the learning curve is worth the benefit.

Reply to this Comment

@Ben

Well, I really hadn't thought about it too much. I see that angular-strap library doesn't use isolated scopes, angular-ui mostly does. Most code examples I've seen uses isolated scopes, so I have done it this way, at least until now, mostly because it looks cleaner to me. I should dig into this cause I'm building a heavy data SPA on Angular.

Reply to this Comment

@Derek,

For one, I think it forces you to really organize your application. Because AngularJS "enforces" a strong separation between your Controller and your Views, you really have to think about your data differently. I know that when I was using just jQuery / JavaScript, my code was a pile of DOM interactions and model updates and server-side communication. With AngularJS, I now have much better separation which allows me to maintain code more easily.

That said, I think it's definitely the right tool for the job. What I am building right now is a single page application (SPA). It's rather complex. If I was doing simple stuff, I would probably just still use small sets of jQuery and JavaScript. You *can* mix AngularJS into an application. But, unless I was really going whole-hog, I'd probably just revert back to a less complex solution.

Reply to this Comment

@Edgar,

I should probably learn more about them. I've used them in the case of transcluding with ngTransclude; but only lightly. I don't transclude a lot of stuff - one of the things in AngularJS I don't have a super solid mental hold on yet.

Reply to this Comment

Congratulations on your initiative, AngularJS is amazing and is starved of good content like this. The documentation is lacking and how your work will surely be very appreciated.

Reply to this Comment

@Livingstone,

Thank! The documentation is good, but it is a bit hit and miss depending on what feature you're dealing with. I'll try to keep putting some good stuff out there.

Reply to this Comment

I've been searching all day for a way to make an element fadein (using jquery and angularjs) once created or added to an array.
There are some examples out there but yours really helped.

Reply to this Comment

I am not sure anyone wants to be working with Java right now as more and more bugs , vulnerabilities and other user problems keep surfacing. I for one am running more towards Flash but well see where to the web goes!

Reply to this Comment

Re: "I am not sure anyone wants to be working with Java right now" ...

There are bugs in every language - some less than others - Java the_programming_language has it's drawbacks but performance and stability typically aren't one of them.

Did you mean Java Applets? Applets are a very old technology that has been outdated for quite some time ... I don't see many java developers using Applets these days ... More info please ...

Reply to this Comment

@Niel,

Glad this was useful. Directives were a hard nut to crack. It really requires you to think very differently about the separation between your DOM and your data. But, it forces you to build things better, I think!

Reply to this Comment

@Tiny, @Edward,

I think maybe you're confusing Java and "JavaScript". Similar names, nothing in common.

Reply to this Comment

@Edward,

Sorry, that was directed at @Tiny - I included you only cause you already responded to him/her.

Reply to this Comment

@Ben,

No offense taken :-)

I did get a bit confused what he was actually directing his comment about ... Angular? Grails? I dunno' ...

Reply to this Comment

@Sherry,

Thanks! I hope to get a good amount of AngularJS blogging in the next couple of months. It's been my baby for the last few months.

Reply to this Comment

how do you get newValue and oldValue for your comparison inside the watch? I take it is provided to you by angular during watch cycle? By the way thanks a lot for great blog on directive. Keep'em coming.

Reply to this Comment

@Unni,

Yes, the callback that you pass to the $watch() method gets the newValue and oldValue passed to it as the first and second arguments, respectively. When you set up a $watch() callback, it will typically fire at least once, the first time with the newValue and oldValue being the same (something to do with the fact that $watch() callback is called asynchronously).

Reply to this Comment

you rock. I was looking for an example accomplishing this simple task. Your blog is becoming my go-to as I begin to learn AngularJS.

Reply to this Comment

Lifesaver! Thanks for this... really needed to get something like this up and running fast without having to get to grips with directives again :-)

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.