Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Angular JS - Using Directives to Create Custom Attributes

4.44/5 (7 votes)
30 May 2018CPOL11 min read 87.3K   4  
Introduction to Angular's Directives and how you can use them to create your own attributes to define behavior in a highly reusable way

Introduction

Angular is a fairly new JavaScript library that provides declarative data binding functionality to web interface development. This allows you to develop a web UI using the MVVM design patterns. This first iteration of this article doesn't spend time to explain Angular's core points or of the MVVM design pattern. Angularjs.org can get you started though. I haven't decided if I want to take on a full description of Angular's functionality for an introduction or not. I also haven't decided if I would make that part of this article or write a separate article as a companion to this one.

For the purposes of this article, I use the concept of a custom attribute interchangeably with the Angular specific term "directive."

Background

Having personally done a lot of work both in web development, and with MVVM design in WPF, I was particularly excited to check out Angular. I have been very pleased with some of the things it makes possible. I have only begun exploring Angular's features and capabilities, but I have been particularly impressed with the ability Angular gives to add custom attributes in a way that makes mark up light and keeps the JavaScript you write simple.

Project Explained

This article is not intended to be a full explanation of how to use Angular. Unfortunately, I feel that the current documentation available on http://www.angularjs.org is not as thorough as it could be, but it certainly can get you started. That being said, I will provide some brief thoughts/explanations about the example project I uploaded.

demo.js: This is the JavaScript file that has our view model or controller. Notice that the same exact view model can result in distinct behavior as driven by the mark up. This is a rather simple example with a collection of items, a title, and a pointer to the active item. If you are doing Angular correctly, your ViewModel will be completely agnostic of the UI. Dom manipulation, markup creation, etc. should not be in your ViewModel.

directives: Your custom attributes should be done in as general a way as possible. They are where DOM and HTML related work is appropriate, and they should be agnostic of any ViewModel to keep those attributes completely modular and reusable. This article will focus on the directives.js, so more on that later in the article.

HTML Files: For clarity, I worked with a few conventions. Any attribute that starts with "ng-" indicates it is an attribute that invokes angular functionality and that is a convention that Angular itself uses. In order to prevent any collisions with existing or future Angular attributes, I opted to start any of my custom directives with "ng-ds-". I keep all my directives or attribute driven behaviors in the file Directives.js. That allows you to build a single library of attributes that you can use throughout your project in a very modular and powerful way.

jquery: One of the examples uses the jquery modal. Other than that, I make use of basic jquery in the directives. It would be trivial to use any other modal of your preference. You could find yourself using jquery for Ajax calls in your ViewModel, but that really isn't necessary because Angular provides some Ajax utilities of its own. (That is beyond the scope of this article though.) Note that you could easily use Angular without jquery.

Creating An Attribute/Directive

Directives.js must be included after angular.js. That allows you to then declare your own Angular module.

JavaScript
var dsApp = angular.module('dsApp', []);  

I haven't dug too much into this, but the first attribute is the name to match the value you assign to your ng-app attribute. The second is an array of dependencies (beyond the scope of this article, and something I haven't investigated much yet). The variable name obviously doesn't have to match the name in the attribute.

Once you have created your module, you then declare each directive on the module. These directives become attributes that Angular will process and wire up. So let's look at our first directive.

JavaScript
dsApp.directive('ngDsFade', function () {
    return function (scope, element, attrs) {
        element.css('display', 'none');
        scope.$watch(attrs.ngDsFade, function (value) {
            if (value) {
                element.fadeIn(200);
            } else {
                element.fadeOut(100);
            }
        });
    }
});

This directive will set up an element to fade in when the value evaluated in the attribute is true, and fade it out when it is false. So you should see ng-ds-fade used in the HTML in several places for both of the examples.

The first parameter defines the attribute. Your parameter name here should be camel-cased. Angular then translates that into dash-separated lowercase attributes. So ngDsFade => ng-ds-fade as an attribute you can use. The second parameter is the function that returns the code that Angular uses to wire up your attribute's behavior. The function you return evaluates on the viewmodel (scope), dom node with the attribute (element), and any other attributes this module has (attrs).

The first thing you can do is set up some initialization. In this case, we hide the element to initialize. It is worth noting that at this point in the code, the order the attributes appear on the dom element is significant. Any attributes that come after the one in question (ng-ds-fade in this case) will not be present in attrs yet. So if you need to use multiple attributes, your order becomes important if you are doing things outside of the watcher you set up the majority of the time. By the time the watch has evaluated, all attributes are present. So most of the time, that is not an issue. I would discourage creating things where the order of the attributes becomes a requirement for functionality. It would obviously result in code that is not easily maintained, or modular.

scope.$watch is where the power comes in. This sets it up so that Angular will evaluate the function whenever the value in the attribute changes. In this case, a true or not-null value will fade in, and a false or null value will fade out using jquery's fade.

Another equally simple directive/attribute in the example is ng-ds-active. That one simply adds or removes the active class on the element based on the value bound to the attribute. It's another simple, but very useful attribute you can create and reuse everywhere.

Modal Directive

Ok so let's take a look at a more interesting one. How about something that opens or closes a modal with the contents of your dom element in the modal. Thus by simply putting the attribute ng-ds-modal="EditMode" in your mark up (where EditMode is a boolean on your ViewModel), you can control the modal by simply changing the value of EditMode on your ViewModel. Interestingly, you can do some basic boolean syntax in the value of your attribute and the watcher handles it well. For example, in the modal example HTML, we set ng-ds-modal="ActiveItem!=null". Ok, so let's look at this directive's JavaScript now.

JavaScript
dsApp.directive('ngDsModal', function () {
    return function (scope, element, attrs) {
        var diag = $(element).dialog(
        {autoOpen:false,
        close:function()
            {
                try
                {
                var functionName= attrs.ngDsModalClosed;
                //alert(functionName);
                if(typeof scope[functionName]=="function")
                    setTimeout(function(){scope.$apply(scope[functionName]);},100);
                }
                catch(err)
                {}
            }
        });
        scope.$watch(attrs.ngDsModal, function (value) {
            if (value) {
                $(element).dialog("open");
            } 
            else 
            {
                $(element).dialog("close");                
            }
        });
    }
});  

The initialization code here sets up a jquery dialog from the dom node passed in. As part of that, we set up the close event handler. By the time that function evaluates, the attrs object has all the attributes, so the attribute order doesn't matter. Notice that the close function looks for a function name. If we have a function defined on the scope, then we call it. Two tricks became necessary here.

  1. scope.$apply is necessary in order for the function executed to result in Angular re-evaluating binding values. If you don't call via scope.$apply, then any changes to your ViewModel won't result in binding updates reflecting on the DOM, etc.
  2. I found out the hard way (and through some Googling) that a timeout is required here in order to prevent an error in Angular. Something about the digest already being in progress or something like that. I don't remember the specifics or pretend to understand exactly why we get the error.

So we needed the close modal handler because it is possible to close the jquery modal via jquery's close button which is not bound in any way to our viewmodels. In other words, we want to have a way for jquery's closed modal to notify the ViewModel that it has closed so the ViewModel can update any properties it should.

We set up the ViewModel with a handler that will clear out the ActiveItem pointer. Notice how we use a second attribute (ng-ds-modal-closed) to define what to call when the modal is closed. That allows us to use the same directive for any ViewModel, and even use it against multiple properties on different elements in one ViewModel.

Inline Edit Directive

Ok, so if I explained that well enough for you to wrap your head around it, then we can move on to one that is more significant and involved. I wanted one that would let us define a template to use for inline editing. In order to do that, we set up three attributes, used in a few places.

Inline Template

By putting this attribute on the template mark up (what we put in the modal in the other example), we invoke some initialization on that element. This attribute doesn't need any watcher, so it just sets up some very simple initialization. Let's take a look at that now.

JavaScript
 dsApp.directive('ngDsInlineTemplate',function(){
    return function(scope,element,attrs)
    {
        var templateElement = $(element);
        var name= templateElement.attr("ng-ds-inline-template");
        $(element).hide();//hide our template.
        scope[name]=element;//save a pointer to the template node in the      
    }
});    

We first take the element and save its name. I didn't find the value in the attrs array, but was able to get it by simply inspecting the attributes on the template. We hide the template. We then save a property on the ViewModel with the same name. That allows us to potentially have multiple inline edit templates working on a single ViewModel if we needed. So the purpose of this attribute and directive is simply to give us a pointer to our template. It is saved on the scope, but in reality the ViewModel never consumes it. We use it in another directive.

InlineEdit and TemplateName

ng-ds-template-name sets the name that ds-inline-edit looks for to find the dom element used in our template.

JavaScript
dsApp.directive('ngDsInlineEdit',function()
{
    return function(scope,element,attrs)
    {
        //$(element).hide();//hide our template.
        scope.$watch(attrs.ngDsInlineEdit,function(value)
        {
            //we need to get our template element to move it and show, or just hide it.
            if(!attrs.ngDsTemplateName)
                return;//we don't have a template yet.
            var template = $(scope[attrs.ngDsTemplateName]);
            if(value)
            {
                setTimeout(function(){
                //move the template here and show it.
                $(element).after(template);
                template.fadeIn(200);
                },10);
            }
            else
            {
                template.hide();
            }            
        });    
    }
});

This one feels backwards from the modal example. We decide where to put our template based on the item. In the modal, we put the property that evaluates with the watcher on the modal's contents. In this example, we put the condition for where to put and show the template on the specific item. So when it is true for a specific item, the template is placed immediately after that element. The wait was actually necessary for properly handling switching active items. Otherwise, we could get timing issues where we move it and then the hide finishes and the inline edit is not visible. When false, it is hidden.

So for this set of directives to work, you have the following attributes:

  • ng-ds-inline-template='somename': placed on the dom node that is your inline edit template
  • ng-ds-template-name='somename': placed on any item you want to move that template to when that item is in edit mode
  • ng-ds-inline-edit=condition: The condition that when evaluated to true results in the template being shown immediately after the item you put this attribute on.

Limitations

This inline edit directive is a mutually exclisve model. That is to say it can only use the template in one place at a time. It would be an interesting, and not terribly difficult exercise to do a version that lets you edit multiple items. That however would also require your ViewModel to know about more than one active item.

I am also aware that the example markup is technically not correct. By putting a div in the middle of an unordered list, I am being naughty. The inline edit example is probably most useful with tables. Or I could have rearranged the mark up slightly so that the placement of the edit template was inside the li tag, not a sibling to it. In my lazy example however, that would have made everything bold. Again, those are simple enough things to remedy but not critical for the purpose of this article.

Conclusion

I hope that this gives you an idea for some of the useful and powerful things you can do with Angular by creating your own directives/attributes. When you do things correctly, you end up with behaviors that can be used throughout your projects. You can see how the same ViewModel can be used for various behaviors and experiences without modification. The mark up remains almost identical, and even when considering the JavaScript for the directive's behavior, things remain pretty simple.

Acknowledgement

Tony Spencer (blog URL coming soon) was invaluable in getting me started on creating custom attributes. At this point in Angular's life, there isn't nearly as rich a community as would be nice, and examples are hard to come by. Tony created fade and active in this example project and answered a lot of my questions.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)