Sunday, 7 April 2013

AngularJS directive: Overview

During the last weeks, I gave an overview of most important features of the custom directives feature of AngularJS.
In this blog text, I’ll give a short overview of each of them.
  • template: You can replace the HTML element with a html fragment. It is most suited to add functionality (like validation) or use a short notation for a fragment you need multiple times. You can’t (see next) use the contents of the element in the result
  • transclude: With the tranclude property you can specify that you like to grab the content of the html element where the directive is placed on and use it on a certain location in the template html fragment.
  • compile function: instead of using a html template fragment, you can use the compile function to generate the required DOM changes in JavaScript. Compile function is the most suitable function when you need DOM changes.
  • options: with the attrs parameter of the compile function, you have access to the attributes defined on the element you used the custom directive on.  You can use it the configure your widget.
  • link function: If you need access to the model values defined in the scope object, you can use the link function of the directive.  Be aware that in case you perform DOM changes to your document, AngularJS expressions might not react anymore to the changes ion the model.
  • $watch: When you perform DOM changes in the link function and you need to react on changes in the model/scope object, you can use the scope.watch() function to restore the ‘binding’ again, programmatically.


In the last few weeks I gave an overview of the things that I learned when creating the AngularPrime library.

From using the simple attribute template to add functionality to the advanced usages of scope.$watch() to have the panel contents collapse programmatically.

Have fun with it.

Saturday, 6 April 2013

AngularJS directives: Using $watch

Introduction

In the previous blog text, we switched from the compile to the link function of the directive.  This allowed us to use the scope object and have access to model values.  The downside of it, our AngularJS expressions in the title attribute aren’t picked up anymore.
Nevertheless, we continue this path as it will allow us to programaticly collapse and expand the panel contents.

$watch

The scope object has a very usefull function, $watch.  It allows us to add a watch function and react on it when the value changes. It is the programmatic way of defining an AngularJS expression.

           scope.$watch(attrs.title, function (title) {
               myPanel.setTitle(title);
           })

The above is all that is needed to watch for a change and call the, newly created, title() function in the myPanel object. Using the above form, we can only monitor the changes in a simple expression like this
 
<div my-dir6="panelOptions" title="panelTitle" >

Where panelTitle is defined in the scope object.
 
$scope.panelTitle = 'Change me';

The more complex constructions like the one we had previously (title: {{panelTitle}}) can also be supported but requires a little bit more work.  For those that are interested in it, I used it in the AngularPrime panel widget and involves the use of the $interpolate service. You can have a look at the code if you need that kind of functionality.

To be complete, this is then the title() function to change the title of the panel header.

    setTitle: function(titleValue) {
        this.titleSpan.html(titleValue);
    }

Programmatic collapse


The programmatic collapse can be programmed in a similar way.  We define a collapsed value within the scope as follows
 
 $scope.panelOptions = {
        collapsed : false
    };

This can be manipulated in JavaScript in anyway we like and we are familiar with. And the change can be watched in a similar way as we did with the title attribute.

        scope.$watch(attrs.myDir6 + '.collapsed', function (value) {
            if (value === false) {
                myPanel.show(element);
            } else {
                myPanel.hide(element);
            }
        });

As you can see, we are able to define the string expressions ourself which needs to be watched.
The end result is that we can define other elements, like buttons, that manipulate the value of collapsed and as a result, the panel contents can be collapsed or exapnded depending on the value we set.

The complete code can be found in the example 6 of the code on github.

Conclusion

By using the link function, we have to resort to the AngularJS function $watch to restore the functionality we had previously.  But it opened a new way of integration paths.  We can react on a simple change of a value and let it result in a visual effect on the screen.

Thursday, 4 April 2013

AngularJS directives: Integrate with $scope

Introduction

In the last blog item we saw a way to configure our panel widget so that we could create a collapsable and non collapsable version.
Until now, we didn’t integrate it yet with the model values, contained in the $scope object. This blog item shows you how you can do it.

Link function

Until now, we used the compile function of the directive to make our changes to he DOM structure. As stated in the documentation of the directive, this is also the best place to perform such actions.
But in that function, we can’t have access to the scope object. Within the link function we have, so lets try it that way.
demo.controller('Ctrl', function Ctrl($scope) {

    $scope.panelOptions = {
        collapsed : false
    };
});

As a starting point, suppose we have the above code as the controller. We defined an object panelOptions that indicates if the panel is collapsed or not. This is already in preparation for our next step that we like the panel contents to collapse as we change the scope value.

In the html code we like to refer to this scope information in the following way
<div my-dir5="panelOptions" title="title : {{panelTitle}}" >
   Contents of the div. Angular Expression result : {{contentField}}
</div>

To get access to the actual object defined in the scope, we need to execute the scope.$eval function as shown in the directive code.
directive('myDir5', function version5 () {
        return {
            restrict: 'A'
            , link: function (scope, element, attrs) {

                var options = scope.$eval(attrs.myDir5) || {};

                console.log('options value');
                console.dir(options);
              }
        };
    });

If we look at the console, we can see that the options object is the one that we have defined in the controller (collapsed is false).

attrs.$observe


In the AngularJS documentation, they mention the attrs.$observe as a way to retrieve the value of angular expressions defined in the attribute values. And in some use cases it can be useful.


  • You don’t need to have access to the scope object to get the value, but it seems that it only works from within a compile function.
  • You need to write it explicitly as an expression, so in our example it would be my-dir5=”{{panelOptions}}”. This is not the common notation as you are familiar with the brackets less version with ng-model and alike directives.
  • The function passed as parameter to the attrs.$observe method is executed asynchronously. That means that you have to wait until you receive the actual value before you can proceed.

For the above last 2 reasons, I prefer to use the scope.$eval method. And you might think that evaluating something at runtime is unsafe (as it is in standard JavaScript). Within angularJS it is a common practice, also used by the library itself, as you are always restricted to the scope content (a kind of sandbox if you like)

Dead end?


Now that we have access to the objects defined in the scope with the $eval() function, we can now call our create() method of the panel helper we have written in the previous blog item.

And to our surprise, the widget isn’t working properly anymore. The look is still ok, but the header of the panel still contains the Angular expression which isn’t resolved (title : {{panelTitle}} )

How can we explain that?

Our link function is called after the AngularJS code determines where there are expressions which need to be ‘watched’. Watched means that they get replaced by the actual value whenever the content is changed.

Because AngularJS has already identified the DOM elements to change, our newly created element isn’t on that list and thus not known by AngularJS.

The solution will be discussed in the next blog item.

Conclusion


If we like to access the scope object, we need to abandon the compile function and switch to the link function. But at first glance, it is a step back as the title isn’t set correctly anymore.  But as we will see towards the end of this series, this switch will pay off as we are able to collapse/expand the panel contents programmatically without any user intervention.

Tuesday, 2 April 2013

AngularJS directives: Specifying options

Introduction

In the last blog item we created our panel widget by writing a few DOM manipulations with the JQuery light version included in AngularJS. Now that we have our directive working with code, we can think about the configuration of the collapse functionality.

Attributes

AngularJS gives you full access to all attributes that are specified on the element. In the previous version of our directive, you maybe saw already the attrs parameter of the compile function which we didn’t use. This is the list of all attributes on the element.
So for testing our directive, we create 2 panels, one where we like a ‘button’ to collapse the contents, and one where we don’t need it.
If we don’t need it, the html is the same as we already used in the previous blog items.

<div my-dir4 title="title : {{panelTitle}}" >
   Contents of the div. Angular Expression result : {{contentField}}
</div>

And when we need the collapsible contents, we like to write it like this:
<div my-dir4="collapsable" title="title : {{panelTitle}}" >
   Contents of the div. Angular Expression result : {{contentField}}
</div>

Reading config


By using the attrs parameter of the compile function, we can determine if the developer specified the ‘collapsable’ text within the directive. This allows us to create an options object that we can pass to the create function we have created the last time. And thus taking into account that we need to ‘render’ a button in the panel header.

The directive code looks like this
directive('myDir4', function version4() {
        return {
            restrict: 'A'
            , compile: function (element, attrs) {
                var options = {
                    collapse: 'collapsable' === attrs.myDir4
                };
                myPanel.create(element, options);
            }
        };

Now we have the collapse boolean property that tell us if we need to render the collapse button.
We are not limited to reading only the attribute of the directive itself.  We could for instance also retrieve the value of the title attribute, which is not a directive or a standard attribute like class.

Rendering the collapse button


The create function can be updated to ‘render’ an optional collapse button.
this.header = angular.element(Helper.findChild(element, 'pui-panel-titlebar'));

if(options.collapse) {
   this.toggler = angular.element('<a href="#"> X</a>').bind('click', function (e) {
      Helper.findChild(element, 'pui-panel-content').style.display = "none";
      e.preventDefault();
   });

   this.titleSpan = angular.element(Helper.findChild(this.header, 'ui-panel-title'));
   this.titleSpan.after(this.toggler);
}

When we need the ‘toggler’ (options.collapse is true), we create a DOM element (angular.element) and attach a handler for the click event.  When the users clicks the X marker, we find the element that has the pui-panel-content CSS class and set the display options to none.

The Helper.findChild is a helper method to find a child element with a certain CSS class name. With the JQuery light version, we can specify element.children(), but we can’t supply a selector with it. So element.children(‘pui-panel-titlebar’) returns all children. The helper function loops over all children and returns the one, if any, that has the class defined.

A more complete version of our panel widget can be found in the demo code on github. There you can find a more complete version of the collapse functionality where we render a plus or minus sign that allows us not only to collapse the panel contents but also expand it later on again.

Conclusion


Now we can react based on the information we find in the directive or other attributes on the element. This gives us the tools to create more dynamic solutions and have a configuration possibility.

In the next item, the last of the series, we see how we can access the scope within the directives and how the code looks like to have a programmatic collapse and expand of the content.