Extending angular directives

Posted by Gjermund Bjaanes on June 7, 2015

In this post I will explain some techniques for extending Angular Directives and how to design for extensibility.

It might not always make sense to go crazy and extend directives everywhere. Try not to use this more often than necessary. It’s no point making huge generic components just to call thing reusable. More often than not, it’s not even worth it to make things that generic. It all depends on the use case of course. Try to keep things small and reusable instead.

That said, when we do want to create some directive that is reusable and extendable, there exists many ways to do that. It all depends on you code style and what you actually want to extend.

All code created for this blog post is available on Github:

https://github.com/bjaanes/Extending-Directives-Blog-Post-Code

 

Require Directive for extension and decoration of controller

Let’s say you have a base directive of some sorts and you want to create a directive that extends the functionality of that base directive. Depending on what you want, using ‘require’ to decorate the base controller might solve your problems.

There are two very similar ways to do this:

 

Separate Directive

Base Directive:

(function () {
    'use strict';

    angular
        .module('app')
        .directive('require', requireDirective);

    function requireDirective() {
        return {
            scope: {},
            restrict: 'AE',
            templateUrl: 'require/require.html',
            controller: RequireController,
            controllerAs: 'vm'
        };
    }

    function RequireController() {
        var vm = this;

        vm.data = [1, 2, 3];
        vm.doSomething = function () {
            console.log('RequireController Does Something');
            console.log(vm.data.toString());
        };

        vm.doSomethingElse = function () {
            console.log('RequireController does something else (?)')
        }
    }

})();

Directive template HTML:

<h1>Require Directive</h1>
<button ng-click="vm.doSomething()">Do Something</button>
<button ng-click="vm.doSomethingElse()">Do Something Else</button>

 

Extension directive:

(function () {
    'use strict';

    angular
        .module('app')
        .directive('requireExtension', requireExtensionDirective);

    function requireExtensionDirective() {
        return {
            restrict: 'A',
            require: 'require',
            link: function (scope, element, attributes, RequireController) {
                var originalDoSomething = RequireController.doSomething;

                RequireController.doSomething = function() {
                    console.log('Extension Does Something');
                    var number = RequireController.data.length + 1;
                    RequireController.data.push(number);
                    originalDoSomething();
                };

                RequireController.doSomethingElse = function () {
                    console.log('ONLY extension does something');
                }
            }
        };
    }

})();

Actual Usage:

<require require-extension></require>

 

How it looks:

Require Screenshot

What happens when you click Do Something:

Console Screenshot Do Something

 

What happens when you click Do Something Else:

Screenshot require Do Something Else

 

Stacked Directive

Angular allows several directives with the same name to co-exists. Well, actually they all exist at the same time, so when you use one of them you use all of them at the same time.

Base Directive:

(function () {
    'use strict';

    angular
        .module('app')
        .directive('requireStack', requireStackDirective);

    function requireStackDirective() {
        return {
            scope: {},
            restrict: 'AE',
            templateUrl: 'requireStack/requireStack.html',
            controller: RequireStackController,
            controllerAs: 'vm'
        };
    }

    function RequireStackController() {
        var vm = this;

        vm.data = [1, 2, 3];
        vm.doSomething = function () {
            console.log('RequireStackController Does Something');
            console.log(vm.data.toString());
        };

        vm.doSomethingElse = function () {
            console.log('RequireStackController does something else (?)')
        }
    }

})();

Directive template HTML:

<h1>Require Stack Directive</h1>
<button ng-click="vm.doSomething()">Do Something</button>
<button ng-click="vm.doSomethingElse()">Do Something Else</button>

Extension directive:

(function () {
    'use strict';

    angular
        .module('app')
        .directive('requireStack', requireStackExtensionDirective);

    function requireStackExtensionDirective() {
        return {
            restrict: 'AE',
            require: 'requireStack',
            link: function (scope, element, attributes, RequireStackController) {
                var originalDoSomething = RequireStackController.doSomething;

                RequireStackController.doSomething = function() {
                    console.log('Extension Does Something');
                    var number = RequireStackController.data.length + 1;
                    RequireStackController.data.push(number);
                    originalDoSomething();
                };

                RequireStackController.doSomethingElse = function () {
                    console.log('ONLY extension does something');
                }
            }
        };
    }

})();

Actual Usage:

<require-stack></require-stack>

 

How it looks:

Screenshot require stack directive

What happens when you click Do Something:

Screenshot require stack do something

What happens when you click Do Something Else:

Screenshot require stack do something else

Works exactly the same.

 

Generic Base Widgets and using delegates for functionality

I recently wrote about using delegate for controller communication.

This is one the use cases where it might make sense. You can wrap your generic base directive inside a more specific directive which implements all the functionality that you need.

Directive for base:

(function () {
    'use strict';

    angular
        .module('app')
        .directive('delegateBase', delegateBaseDirective);

    function delegateBaseDirective() {
        return {
            scope: {
                delegate: '='
            },
            restrict: 'AE',
            templateUrl: 'delegate/delegateBase.html',
            controller: DelegateBaseController,
            controllerAs: 'vm'
        };
    }

    function DelegateBaseController($scope) {
        var vm = this;

        vm.data = [1, 2, 3];
        vm.doSomething = function () {
            if ($scope.delegate && ('doSomething' in $scope.delegate)) {
                $scope.delegate.doSomething(vm.data);
            }
        };

        vm.doSomethingElse = function () {
            if ($scope.delegate && ('doSomethingElse' in $scope.delegate)) {
                $scope.delegate.doSomething();
            } else {
                console.log('No extension here, perhaps do some generic stuff instead?')
            }
        }

    }

})();

Directive template HTML:

<h1>DelegateBase Directive</h1>
<button ng-click="vm.doSomething()">Do Something</button>
<button ng-click="vm.doSomethingElse()">Do Something Else</button>

Extension Directive with Delegate

(function () {
    'use strict';

    angular
        .module('app')
        .directive('delegateExtension', delegateExtensionDirective);

    function delegateExtensionDirective() {
        return {
            restrict: 'AE',
            templateUrl: 'delegate/extension/delegateExtension.html',
            controller: DelegateExtensionController,
            controllerAs: 'vm'
        };
    }

    function DelegateExtensionController() {
        var vm = this;

        vm.delegateObj = {
            doSomething: function(data) {
                console.log('DelegateExtensionController Does Something');
                console.log(data.toString());
            }
        }
    }

})();

Extension Template HTML

<h1>Delegate Extension Directive</h1>
<delegate-base delegate="vm.delegateObj"></delegate-base>

Actual usage:

<delegate-extension></delegate-extension>

How it looks:

Screenshot Delegate Extension

What happens when you click Do Something:

Screenshot delegate do something

What happens when you click Do Something Else:

Screenshot delegate do something else

The reason for this last part is because I didn’t implement the second delegate function. This might be useful if you want fallback functionality.

 

Transclude for extension of HTML

If you need to extend the HTML there are not that many options available to you. You basically have to utilize some sort of wrapping or even more likely transclusion in Angular.

Transclude in Angular directives is basically putting the markup that appears inside a directive somewhere where it makes sense.

<my-directive>
    Some <em>Markup</em>
</my-directive>

 

That can be utilized in my different ways, but in this case I assume you want a generic widget of some sorts that can be extended by putting html in certain places.

Let’s take the example of a header directive. Perhaps you need to use a header widget over several apps. You can create a simple base directive that takes some of the application-specific markup as transclusion.

 

Simple (Single) Transclude

Directive:

(function () {
    'use strict';

    angular
        .module('app')
        .directive('simpleTranslcude', simpleTranslcudeDirective);

    function simpleTranslcudeDirective() {
        return {
            transclude: true,
            restrict: 'AE',
            templateUrl: 'simpleTransclude/simpleTransclude.html'
        }
    }
})();

Directive template HTML:

<h1>Simple Transclude Directive</h1>
<div style="width: 100%; height: 50px; border: 1px solid; line-height: 50px;">
    <img style="max-width:100%;max-height:100%;" src="angularlogo.jpg">
    <div ng-transclude style="float: right"></div>
</div>

Actual Usage:

<simple-translcude>
    <div>
        <button>A Button</button>
        <button>Another Button</button>
    </div>
</simple-translcude>

 

How it looks:

Screenshot header directive transclude

Everything on the left side of the header is now decided with the transcluded content. Nifty, eh?

 

Multiple Transclude

If you need to input HTML several places in a widget there are two options available:

 

Split up the directive

The sane option usually. If it needs a lot of application specific stuff many places it might be better to split the directive up somehow

 

Using multiple transclude

You could probably write some fancy link function that takes all the transcluded content and split it up with string manipulation (or something less error-prone), but luckily there exists a third-party solution to the problem:

ng-multi-transclude (https://github.com/zachsnow/ng-multi-transclude)

You can read about exactly how you can use it on the github page, but for now the code itself explains pretty good how things work:

You need to depend on ng-multi-transclude:

(function () {
    'use strict';

    angular
        .module('app', ['multi-transclude']);
})();

Directive:

(function () {
    'use strict';

    angular
        .module('app')
        .directive('multipleTransclude', multipleTranscludeDirective);

    function multipleTranscludeDirective() {
        return {
            transclude: true,
            restrict: 'AE',
            templateUrl: 'multipleTransclude/multipleTransclude.html'
        }
    }
})();

Directive template HTML:

<h1>Multiple Transclude Directive</h1>
<div ng-multi-transclude-controller style="width: 100%; height: 50px; border: 1px solid; line-height: 50px;">
    <span ng-multi-transclude="left"></span>
    <div ng-multi-transclude="right" style="float: right"></div>
</div>

Actual usage:

<multiple-transclude>
    <img name="left" style="max-width:100%;max-height:100%;" src="angularlogo.jpg">
    <div name="right">
        <button>A Button</button>
        <button>Another Button</button>
    </div>
</multiple-transclude>

How it looks:

Screenshot Multiple Transclude Directive

 

Extra

For some extra tips and tricks for extending directives you can check out the links below that I found while doing research for this topic.

Decorate the directive itself (the directive object with all the information):

http://stackoverflow.com/questions/19409017/angular-decorating-directives

Using JavaScript Prototype for inheritance:

http://blog.mgechev.com/2013/12/18/inheritance-services-controllers-in-angularjs/

 

Sources:


Follow me on Twitter: @gjermundbjaanes