I'm trying to build a directive that takes care of adding more directives to the element it is declared on.
For example, I want to build a directive that takes care of adding datepicker
, datepicker-language
and ng-required="true"
.
If I try to add those attributes and then use $compile
I obviously generate an infinite loop, so I am checking if I have already added the needed attributes:
angular.module('app')
.directive('superDirective', function ($compile, $injector) {
return {
restrict: 'A',
replace: true,
link: function compile(scope, element, attrs) {
if (element.attr('datepicker')) { // check
return;
}
element.attr('datepicker', 'someValue');
element.attr('datepicker-language', 'en');
// some more
$compile(element)(scope);
}
};
});
Of course, if I don't $compile
the element, the attributes will be set but the directive won't be bootstrapped.
Is this approach correct or am I doing it wrong? Is there a better way to achieve the same behavior?
UDPATE: given the fact that $compile
is the only way to achieve this, is there a way to skip the first compilation pass (the element may contain several children)? Maybe by setting terminal:true
?
UPDATE 2: I have tried putting the directive into a select
element and, as expected, the compilation runs twice, which means there is twice the number of expected option
s.
You can actually handle all of this with just a simple template tag. See http://jsfiddle.net/m4ve9/ for an example. Note that I actually didn't need a compile or link property on the super-directive definition.
During the compilation process, Angular pulls in the template values before compiling, so you can attach any further directives there and Angular will take care of it for you.
If this is a super directive that needs to preserve the original internal content, you can use
transclude : true
and replace the inside with<ng-transclude></ng-transclude>
Hope that helps, let me know if anything is unclear
Alex
There was a change from 1.3.x to 1.4.x.
In Angular 1.3.x this worked:
Now in Angular 1.4.x we have to do this:
(From the accepted answer: https://stackoverflow.com/a/19228302/605586 from Khanh TO).
A simple solution that could work in some cases is to create and $compile a wrapper and then append your original element to it.
Something like...
This solution has the advantage that it keeps things simple by not recompiling the original element.
This wouldn't work if any of the added directive's
require
any of the original element's directives or if the original element has absolute positioning.In cases where you have multiple directives on a single DOM element and where the order in which they’re applied matters, you can use the
priority
property to order their application. Higher numbers run first. The default priority is 0 if you don’t specify one.EDIT: after the discussion, here's the complete working solution. The key was to remove the attribute:
element.removeAttr("common-things");
, and alsoelement.removeAttr("data-common-things");
(in case users specifydata-common-things
in the html)Working plunker is available at: http://plnkr.co/edit/Q13bUt?p=preview
Or:
DEMO
Explanation why we have to set
terminal: true
andpriority: 1000
(a high number):When the DOM is ready, angular walks the DOM to identify all registered directives and compile the directives one by one based on
priority
if these directives are on the same element. We set our custom directive's priority to a high number to ensure that it will be compiled first and withterminal: true
, the other directives will be skipped after this directive is compiled.When our custom directive is compiled, it will modify the element by adding directives and removing itself and use $compile service to compile all the directives (including those that were skipped).
If we don't set
terminal:true
andpriority: 1000
, there is a chance that some directives are compiled before our custom directive. And when our custom directive uses $compile to compile the element => compile again the already compiled directives. This will cause unpredictable behavior especially if the directives compiled before our custom directive have already transformed the DOM.For more information about priority and terminal, check out How to understand the `terminal` of directive?
An example of a directive that also modifies the template is
ng-repeat
(priority = 1000), whenng-repeat
is compiled,ng-repeat
make copies of the template element before other directives get applied.Thanks to @Izhaki's comment, here is the reference to
ngRepeat
source code: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.jsHere's a solution that moves the directives that need to be added dynamically, into the view and also adds some optional (basic) conditional-logic. This keeps the directive clean with no hard-coded logic.
The directive takes an array of objects, each object contains the name of the directive to be added and the value to pass to it (if any).
I was struggling to think of a use-case for a directive like this until I thought that it might be useful to add some conditional logic that only adds a directive based on some condition (though the answer below is still contrived). I added an optional
if
property that should contain a bool value, expression or function (e.g. defined in your controller) that determines if the directive should be added or not.I'm also using
attrs.$attr.dynamicDirectives
to get the exact attribute declaration used to add the directive (e.g.data-dynamic-directive
,dynamic-directive
) without hard-coding string values to check for.Plunker Demo
Try storing the state in a attribute on the element itself, such as
superDirectiveStatus="true"
For example:
I hope this helps you.