Easier Angular Directives with SweetJS

by Brian Rinaldi on April 21, 2014

The modern web is always changing, and this article is more than two years old.

By Vittorio Zaccaria

I’ll admit that AngularJS directives have always been a bit tough for me. Maybe because I am getting old – what do I know? The fact is that I’ve always struggled to understand and memorize its cryptic documentation. Besides, whenever I deal with Angular objects, I inevitably end up writing code that is bloated with $scope accesses. You get the idea.

However, creating a self-contained, custom HTML element that can be be reused inside all of my projects has always been in the back of my mind. So, I decided to give Angular’s directives another try. This time, however, I wanted to see whether rethinking the directives in terms of a language extension could result in a more synthetic and palatable approach for me (and for anybody else).

Enter SweetJS

I am playing with domain specific languages and macros for some time. The (almost) new kid on the block in the Javascript world is Mozilla’s SweetJS. SweetJS allows you to create language extensions to ‘sweeten up’ your Javascript, so I thought: wouldn’t be fantastic create a clean, trivial syntax for defining my Angularjs directives for use in my projects?

Aaron Powell had already tried to create macros for Angular factories and modules. This left me wondering if a similar approach could be applied to Angular directives.

Sweet Angle

Sweet Angle is the result of this challenge. It is based on macro expansion and covers the majority of the cases in which you may need a directive.

For example, let’s assume we want to write a directive for a progress bar:

<div id='pb' progress-bar value='statusToBeShown' color='blue' max='100'></div>

The directive
has three attributes:

  • value represents the current percentage to be shown on the progress bar.
  • color represents the name of the class to be used for coloring the bar.
  • max represents the maximum value that value can assume.

color and max are passed by value. Passing an attribute by value means that whatever change is done on that attribute by the directive will not be seen outside the directive itself. It’s a one-way bind in Angular’s world. In the case above, we pass two constants to color andmax. However, we could have passed a more complex Angular expression (where variables of parent scopes are surrounded by double braces).

value instead, is passed by reference. This means that, if, for whatever reason, the directive changes the value of that attribute, it will be seen outside of the directive. It’s a two-way bind in Angular’s world. This is why you can only specify the name of a property on a parent’s scope (statusToBeShown in this case).

See Also:  Getting started with Redux using the Mullet Stack

Let’s write the beginning of the directive in Sweet Angle:

directive progressBar {
    import                 /* none */;
    byref    value;        /* two-way angularjs bind */
    byval    color, max;   /* one-way angularjs bind */
    callback               /* none */; 
...

In the actual implementation, a Sweet Angle directive header should specify all of its features, even if it does not use them (I fought a battle with SweetJS on this, and it won!). Also, you should follow the exact order you see above.

The import keyword should be followed by optional Angular module names to be imported. An imported module will be automatically available as $scope.module is in the isolated scope created by the directive. If you wanted to import Q and Restangular, you would write:

...
import $q, $restangular;
...

The byref clause is used to create a local scope variable that mirrors another one in the parent scopes (it uses Angular’s scope qualifier ‘=‘ internally).

The byval clause is used to create a one way bind, from the parent scope(s) to the local one (it uses Angular’s scope qualifier ‘@‘ internally).

callback can be used to pass one or more callbacks that the directive could call during is lifetime (Sweet Angular uses Angular’s scope qualifier ‘&‘ internally for this).

Next we specify the HTML template by using Jade’s syntax. Sweet Angle tells Angular to completely replace the original element with the template specified:

 template `
div 
    .ui.progress.transition.active(ng-class='[visibility, color]')
    .bar(style='{{bsize}}')
`;

Note two additional scope variables used here: visibility and bsize. These will be initialized and modified by the methods of the directive.

As a last step, we define how the directive is created with the create method. This is converted by Sweet Angle into the directive’s link function with some de-sugaring:

  • create is invoked having this mapped to $scope.
  • @ can be used as an alias for this.

First, let’s save the parameter elem into the local scope, for convenience:

create(elem, attr) {
    @elem = elem; /* This is converted to $scope.elem = elem */
    ...

Then let’s start by making the progress bar invisible:

...
@visibility = 'hidden';
...

Next, define a directive method that enables us to toggle the visibility of the bar:

...
@toggle = function() { 
    if(@visibility === 'hidden') {
        @visibility = '';
    } else {
        @visibility = 'hidden';
    }
}.bind(@) // <<- Bound to `this`.
...

Now, we need to take care of the changes to $scope.value (or @value) to visually update the bar. First we define an event listener that updates $scope.bsize with the new bar size:

@onValueChange = function() { 
    var perc;
    perc = @value/parseFloat(@max)*window.jQuery(@elem).width();
    @bsize = "width: "+perc+"px;"
}.bind(@) // <<- Bound to `this`.

Second, we register the event listener on any change to value:

 ...
    @$watch('value', @onValueChange);

    } /* ends create */
}; /* ends the directive */
   ...

Result

The complete directive is here:

directive progressBar for application {
 import   /* none */;
 byref    value;
 byval    color, max;
 callback /* none */; 

 template `
div 
 .ui.progress.transition.active(ng-class='[visibility, color]')
   .bar(style='{{bsize}}')
`;

 create(elem, attr) {
   @visibility = 'hidden';
   @elem = elem;

   @toggle = function() { 
     if(@visibility === 'hidden') {
      @visibility = '';
     } else {
      @visibility = 'hidden';
     }
     @onValueChange();
   }

   @onValueChange = function() { 
     var perc;
     perc = @value/parseFloat(@max)*window.jQuery(@elem).width();
     @bsize = "width: "+perc+"px;"
   }

   @$watch('value', @onValueChange);

 } /* ends create */

};

In order to convert this into vanilla JavaScript use the command:

> sweet-angle directive.sa -a application > directive.js

The result is the following:

angular.module('application').directive('progressBar', function () {
    console.log(arguments);
    var ret = {};
    ret.restrict = 'A';
    ret.template = '<div><div ng-class="[visibility, color]" class="ui progress transition active"><div style="{{bsize}}" class="bar"></div></div></div>';
    ret.replace = true;
    ret.scope = {};
    ret.scope.value = '=';
    ret.scope.color = '@';
    ret.scope.max = '@';
    ret.link = function (s) {
        (function (elem, attr) {
            this.visibility = 'hidden';
            this.elem = elem;
            this.toggle = function () {
                if (this.visibility === 'hidden') {
                    this.visibility = '';
                } else {
                    this.visibility = 'hidden';
                }
                this.onValueChange();
            };
            this.onValueChange = function () {
                var perc;
                perc = this.value / parseFloat(this.max) * window.jQuery(this.elem).width();
                this.bsize = 'width: ' + perc + 'px;';
            };
            this.$watch('value', this.onValueChange);
        }.apply(s, Array.prototype.slice.call(arguments, 1)));
    };
    return ret;
});

Accessing the directive scope

To get to the isolated scope of the directive, e.g., to invoke its methods, you can use the following helper:

window.$$ = function(sel) {
    turn angular.element(document.querySelector(sel)).isolateScope();
}

So, for example, you can toggle the progress bar visibility by using:

$$('#pb').toggle()

Limits of this approach

Sweet Angle is a preliminary experiment, so it has its own limits. In particular, I didn’t find a simple way to provide different shapes of the directive without copying and pasting the code of the template. For example, if I wanted to provide a templateURL instead of template, I’d
have to write two macros. This problem increases exponentially if you consider the ordering of the directive features (import, byref, etc.). So I think that SweetJS’ capabilities for modularization should be somewhat improved.

See Also:  Why you should limit JavaScript — and how to do it

Nevertheless, this has significantly improved my ability to create Angular directives. Now I am much more focused on the functionality of the directive itself instead of being trapped into Angular’s cryptic documentation and the somewhat redundant resulting code.