Skip to main content
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Cara Beverage and Taraneh Kossari
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Cara Beverage Taraneh Kossari

Creating A Custom Show / Hide Directive In AngularJS

By
Published in Comments (35)

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.

Want to use code from this post? Check out the license.

Reader Comments

2 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?

2 Comments

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 )
						;
					}
				}
			);
		}
	}
);
7 Comments

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 :)

15,848 Comments

@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.

15,848 Comments

@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?

15,848 Comments

@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!

7 Comments

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

15,848 Comments

@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.

4 Comments

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.

2 Comments

@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.

15,848 Comments

@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.

15,848 Comments

@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.

1 Comments

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.

15,848 Comments

@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.

1 Comments

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.

1 Comments

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!

96 Comments

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 ...

15,848 Comments

@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!

15,848 Comments

@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.

1 Comments

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.

15,848 Comments

@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).

1 Comments

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.

1 Comments

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

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel