AngularJS dynamic directives

Michael Bailly
Linagora Engineering
5 min readNov 19, 2015

--

Designing an application in a modular way is nowadays admitted as the way to go. The node.js simple yet powerful module system, for example, allowed the platform to grow insanely fast, resulting in NPM being known as the biggest package repository.

Obviously, modules bring a lot of benefits, which are not the main aim of this article. One thing that modules can provide is adding a feature to another already-existing module. That’s exactly one of our goal here at OpenPaaS, on the front end side, and using Angular.js.

AngularJS modules and declarative programming

AngularJS applications obviously are modules: the only way to add Angular code is to begin by writing angular.module(“my-module”, [])… Sadly, Angular doesn’t support on-the-fly module loading, so the developer has to specify, when the web application starts, all the modules that are involved. But that’s certainly the topic for another story.

Angular provides a module system, and also it is a declarative framework. Declarative programming means that you don’t use code to enable features, you declare them.

Let’s illustrate it with a simplistic code, written using jQuery (imperative programming) and AngularJS.

The jQuery version happens in JavaScript:

function doSometing() {..}$('div').click(doSometing);

and the Angular version in HTML:

<div ng-click="doSomething()" />

Knowing all that, let’s explain our goals, and how we did it, by using the dynamic directives.

Composable User Interface

Our application, like tons of others, has a user menu. We want modules to be able to add custom entries in that menu. So basically we have some HTML, like:

<ul class="userMenu">
<li ng-click="something()">Menu entry 1</li>
</ul>

Now, we have a new and shiny avatar module, and we want to add a user menu entry “add avatar”. If we add it in the userMenu HTML, it breaks loose coupling between modules, because the user module then brings a reference to the avatar module. We thought of using a service, that allows registering some data (a directive, or a menu link data structure). However, the user menu is one place out of dozens, in the UI, where we want to provide extensibility.

We decided to take the problem in a really generic way. The software interface should be able to declare anchor points, where the modules can add content. And that’s exactly what dynamic directive provides:

  1. declaration of anchor points in the web interface (HTML)
  2. ability for modules to add content in a specific anchor point

Dynamic directive overview

I will go quickly on the setup instructions. Use bower to get the module:

bower install dynamic-directive

Include it in your application, after Angular:

<script src='dynamic-directive/dist/dynamic-directive.min.js'></script>

Add it to the list of dependencies of your application:

angular.module('your-app', ['op.dynamicDirective'])

You’re ready to go.

Provide anchor points

Anchor points are declared by adding a dynamic-directive attribute to your HTML element. Taking back the user menu example:

<ul class="userMenu" dynamic-directive="user-menu">
<li ng-click="something()">Menu entry 1</li>
</ul>

That’s it. The user menu now provides an anchor point, names user-menu, where any other module can refer to in order to add content.

Inject content at specified anchor points

The dynamic directive module provides a service, to plug a regular directive to an anchor point.

Let’s define our avatar module:

angular.module('avatar', ['op.dynamicDirective'])
.directive('avatar', function() {
return {
restrict: 'E',
replace: true,
template: '<li ng-click="avatarThing()">Add avatar</li>',
link: function(scope) {
scope.avatarThing = function() { .... };
}
};
});

Until now, nothing special, we create an avatar directive that got its own template, and other Angular specifics. One thing to note is that we use the replace feature of the AngularJS directive system, resulting in Angular removing the <avatar> HTML tag from the DOM.

We’ll now use the dynamicDirectiveService to inject the avatar directive into the user-menu anchor point:

angular.module('avatar')
.run(function(DynamicDirective, dynamicDirectiveService) {
var dd = new DynamicDirective(true, 'avatar');
dynamicDirectiveService.addInjection('user-menu', dd);
});

And now we have it. Anytime there will be an anchor point named user-menu, the dynamic directive system will inject the avatar directive.

Conditional injection

Dynamic directive system supports conditional injection. The signature of the DynamicDirective constructor is:

DynamicDirective (shouldIBeInjectedFunction, nameOfTheHTMLTag);

The first argument can be a function that tells whether the directive should be injected, or the Boolean true, which is a shortcut for the function:

function() { return true; }

That function receives the current scope as argument, which is good idea, because, in Angular, all environment related data are exposed from the JavaScript to the HTML using scopes. Let’s say that the user menu exposes the following scope:

{
user: {
name: "Michael",
editable: true
}
}

We can tell the dynamic directive system to inject the avatar directive only if the user is editable:

angular.module('avatar')
.run(function(DynamicDirective, dynamicDirectiveService) {
function shouldIBeInjected(scope) {
return scope.user.editable;
}

var dd = new DynamicDirective(shouldIBeInjected, 'avatar');
dynamicDirectiveService.addInjection('user-menu', dd);
});

Custom attributes

The dynamic directive system allows adding custom attributes to the HTML element that will be injected. For example:

angular.module('avatar')
.run(function(DynamicDirective, dynamicDirectiveService) {
var customAttributes = [
{name: 'class', value: 'btn'},
{name: 'some-fancy-one', value: 'Mr Robot'}
];

var dd = new DynamicDirective(true, 'avatar', {
attributes: customAttributes
}
);
dynamicDirectiveService.addInjection('user-menu', dd);
});

Which will result in the dynamic directive service to build the HTML element:

<avatar class="btn" some-fancy-one="Mr Robot" />

And then inject it into the DOM, and run AngularJs HTML compilation process on it.

Priority

When dealing with user interface, it’s important to have full control on how the widgets are displayed. The dynamic directive system ensure that the widgets will always appear in the same order, without relying on the order that the addInjection() calls are made. However, you can choose to have an injected widget appear before the others by setting a priority.

angular.module('avatar')
.run(function(DynamicDirective, dynamicDirectiveService) {
var dd = new DynamicDirective(true, 'avatar', {
priority: 100
});
dynamicDirectiveService.addInjection('user-menu', dd);
});

The priority property should be an integer. Default priority is 0.

Summary

We’re extremely pleased so far with that system we shaped, we find it elegant, and it solves a number of AngularJS application development use cases. At Linagora, our quest for the perfect modular system is far from over, but this building block is an important step ahead. You can find the library on Github (and yes, it’s ES6 baby), fork it, patch it, send us love and pull requests !

--

--

Michael Bailly
Linagora Engineering

VP of Engineering, loves everything related to development.