Cleanly Extend 3rd Party AngularJS Directives
There have been a number of times that a directive written by some other brilliant mind gets me almost where I need to go, but not quite.
In the past I handled this in one of two ways:
- Fork the project and retrofit my requirements atop it.
- Write a solution from scratch to solve my problem.
While these options are okay, they're hardly flexible, and the 2nd of them can needlessly pollute the community and waste valuable time.
Enter Decorators
Angular gives us the ability to actually modify existing directives in or own app config, the problem is it doesn't seem to be super well documented...
Example:
Recently I wanted the ability to dynamically set an active tab for AngularUI Bootstrap's tabset
directive.
It provides an active
attribute but its not writable making it fail to be set by something like, say, ui-router
My Config
angular.module('myApp')
.config(function($provide) {
/**
* Hijacking the AngularUI Tabs directive
* We need a way to set an active tab based on current router state
* By default this is only supported when using tabs via an `ngRepeat` loop.
*
* @example
<tab isactive="vm.test(isTrue)">tab contents</tab>
*/
$provide.decorator('tabDirective', function($delegate) {
var directive, link;
directive = $delegate[0];
link = directive.link;
directive.compile = function() {
return function Link(scope, element, attrs, ctrls) {
scope.active = scope.$parent.$eval(attrs.isactive);
return link.apply(this, arguments);
};
};
return $delegate;
});
}));
Basically what's happening here is I'm grabbing the directive before my app loads, pulling up the tabDirective
and then extending the link
method via compile
(the return of compile
is always Link
).
Link allows us to add to the scope
, so inside of my custom Link
method i'm evaluating a new attribute I've applied to my tabs called isactive
.
scope.$parent.$eval
will actually fire the method with the existing scope.
I wrap it all up by returning the original link
method with my updates by simply applying my link and all arguments to it: return link.apply(this, arguments);
All done!
Using this same style you can essentially overwrite/extend any aspect of a directive (or factory, provider, service). I've also used this to change directives templates to that of my own design.
directive.templateUrl = 'app/path/to/directive/template.html';
Happy Hacking!
Written by Josey Morton
Related protips
1 Response
nice simply explanation.. wondered if you had encountered 3rd party code where you needed to override a function nested in the link function..
(function () {
'use strict';
function UmbTabsNavDirective($timeout) {
function link(scope, el, attr) {
function activate() {
$timeout(function () {
//use bootstrap tabs API to show the first one
el.find('a:first').tab('show');
//enable the tab drop
el.tabdrop();
});
}
var unbindModelWatch = scope.$watch('model', function (newValue, oldValue) {
activate();
});
scope.$on('$destroy', function () {
//ensure to destroy tabdrop (unbinds window resize listeners)
el.tabdrop('destroy');
unbindModelWatch();
});
}
var directive = {
restrict: 'E',
replace: true,
templateUrl: 'views/components/tabs/umb-tabs-nav.html',
scope: {
model: '=',
tabdrop: '=',
idSuffix: '@'
},
link: link
};
return directive;
}
angular.module('umbraco.directives').directive('umbTabsNav', UmbTabsNavDirective);
}());
I want to override fnct active to change to el.find('a:not(:hidden):first').tab('show');
but can only seem to do it by having to completely override the link function.. eg don't return link.apply(this, arguments);
?????