AngularJS: What's the best practice to add ngIf to a directive programmatically?
Solution 1
The first part of your question, "why?", is something I can answer:
The problem you are running into is that you can't dynamically apply directives to elements without calling $compile
on the element.
If you call $compile(element)(element.scope())
after you set the attribute, you run into a stack overflow because you are compiling yourself, which cause you to compile yourself which causes you to compile yourself, etc.
The second part, "how else to achieve", I am having trouble with. I tried a couple of approaches (like transcluding the content with a nested ng-if
) but I can't get exactly the behavior you are looking for.
I think the next step might be to study the code for ng-if and try to implement something similar directly in your directive.
Here is a first pass of getting it working. I expect it needs some cleanup and modification to get it working how you really want it, however.
Solution 2
You can reuse ngIf
in your own directive like this:
/** @const */ var NAME = 'yourCustomIf';
yourApp.directive(NAME, function(ngIfDirective) {
var ngIf = ngIfDirective[0];
return {
transclude: ngIf.transclude,
priority: ngIf.priority,
terminal: ngIf.terminal,
restrict: ngIf.restrict,
link: function($scope, $element, $attr) {
var value = $attr[NAME];
var yourCustomValue = $scope.$eval(value);
$attr.ngIf = function() {
return yourCustomValue;
};
ngIf.link.apply(ngIf, arguments);
}
};
});
and then use it like this
<div your-custom-if="true">This is shown</div>
and it will use all the "features" that come with using ngIf
.
Solution 3
Joscha's answer is pretty good, but actually this won't work if you're using ng-if in addition of it. I took Joscha's code and just added a few lines to combine it with existing ng-if directives :
angular.module('myModule').directive('ifAuthenticated', ['ngIfDirective', 'User', function(ngIfDirective, User) {
var ngIf = ngIfDirective[0];
return {
transclude: ngIf.transclude,
priority: ngIf.priority - 1,
terminal: ngIf.terminal,
restrict: ngIf.restrict,
link: function(scope, element, attributes) {
// find the initial ng-if attribute
var initialNgIf = attributes.ngIf, ifEvaluator;
// if it exists, evaluates ngIf && ifAuthenticated
if (initialNgIf) {
ifEvaluator = function () {
return scope.$eval(initialNgIf) && User.isAuthenticated();
}
} else { // if there's no ng-if, process normally
ifEvaluator = function () {
return User.isAuthenticated();
}
}
attributes.ngIf = ifEvaluator;
ngIf.link.apply(ngIf, arguments);
}
};
}]);
So if can then do things like :
<input type="text" ng-model="test">
<div ng-if="test.length > 0" if-authenticated>Conditional div</div>
And the conditional div
will show only if you're authenticated && the test input is not empty.
Solution 4
There is another way to solve this problem, using a templating function. This requires jquery 1.6+ to function properly.
A working fiddle of the code: http://jsfiddle.net/w72P3/6/
return {
restrict: 'A',
replace: true,
template: function (element, attr) {
var ngIf = attr.ngIf;
var value = attr.addCondition;
/**
* Make sure to combine with existing ngIf!
*/
if (ngIf) {
value += ' && ' + ngIf;
}
var inner = element.get(0);
//we have to clear all the values because angular
//is going to merge the attrs collection
//back into the element after this function finishes
angular.forEach(inner.attributes, function(attr, key){
attr.value = '';
});
attr.$set('ng-if', value);
return inner.outerHTML;
}
}
replace: true prevents embedded elements. Without replace=true the string returned by the template function is put inside the existing html. I.e. <a href="#" addCondition="'true'">Hello</a>
becomes <a href="#" ng-if="'true'"><a href="#" ng-if="'true'">Hello</a></a>
See https://docs.angularjs.org/api/ng/service/$compile for details.
Solution 5
return {
restrict: 'A',
terminal: true,
priority: 50000, // high priority to compile this before directives of lower prio
compile: function compile(element, attrs) {
element.removeAttr("add-condition"); // avoid indefinite loop
element.removeAttr("data-add-condition");
return {
pre: function preLink(scope, iElement, iAttrs, controller) { },
post: function postLink(scope, iElement, iAttrs, controller) {
iElement[0].setAttribute('ng-if', iAttrs.addCondition);
$compile(iElement)(scope);
}
};
}
The combination of high priority and terminal: true
is the basis how this works: The terminal flag tells Angular to skip all directives of lower priority on the same HTML element.
This is fine because we want to modify the element by replacing add-condition
with ng-if
before calling compile
, which then will process ng-if
and any other directives.
Foobar
Updated on July 16, 2022Comments
-
Foobar almost 2 years
I want to create a directive that checks if an element should be present in the dom based on a value coming from a service (e.g. check for a user role).
The corresponding directive looks like this:
angular.module('app', []).directive('addCondition', function($rootScope) { return { restrict: 'A', compile: function (element, attr) { var ngIf = attr.ngIf, value = $rootScope.$eval(attr.addCondition); /** * Make sure to combine with existing ngIf! * I want to modify the expression to be evalued by ngIf here based on a role * check for example */ if (ngIf) { value += ' && ' + ngIf; } attr.$set('ng-if', value); } }; });
At the end the element has the ng-if attribute attached but somehow it doesn't apply to the element and it is still existing in the dom. So this is obviously a wrong approach.
This fiddle shows the problem: http://jsfiddle.net/L37tZ/2/
Who can explain why this happens? Is there any other way a similar behaviour could be achieved? Existing ngIfs should be considered.
SOLUTION:
Usage:
<div rln-require-roles="['ADMIN', 'USER']">I'm hidden when theses role requirements are not satifisfied!</div>
.directive('rlnRequireRoles', function ($animate, Session) { return { transclude: 'element', priority: 600, terminal: true, restrict: 'A', link: function ($scope, $element, $attr, ctrl, $transclude) { var block, childScope, roles; $attr.$observe('rlnRequireRoles', function (value) { roles = $scope.$eval(value); if (Session.hasRoles(roles)) { if (!childScope) { childScope = $scope.$new(); $transclude(childScope, function (clone) { block = { startNode: clone[0], endNode: clone[clone.length++] = document.createComment(' end rlnRequireRoles: ' + $attr.rlnRequireRoles + ' ') }; $animate.enter(clone, $element.parent(), $element); }); } } else { if (childScope) { childScope.$destroy(); childScope = null; } if (block) { $animate.leave(getBlockElements(block)); block = null; } } }); } }; });
It is very important to add the priority in the directive, otherwise other directives attached to that element are not evaluated!
-
Foobar over 10 yearsThanks for the explanation! I'll try to get your approach working!
-
Foobar over 10 yearsIt works like this, I only changed a few things like the observe expression and the priority on which this directive runs. The priority is very important when you need to have other directives on an element as well. Thanks!
-
Ferdinand Torggler over 9 yearsI think that this is a much better approach than the accepted answer since it is very generic and uses the "real" ngIf directive. No directive code is duplicated and if ngIf changes in the future (maybe because of a bugfix/improvement) this will automatically make use of that.
-
André Werlang over 9 yearsIt works, except that a sibling directive won't transclude (e.g. ng-messages).
-
Kumar Sambhav over 9 yearswhat is 'arguments' in last line ?
-
Kumar Sambhav over 9 yearswhat is 'arguments' in last line ?
-
Joscha over 9 years@KumarSambhav the same arguments that were passed to the custom if function - they are just passed through to the original ngIf.
-
hilnius over 9 yearsin JS, arguments is an array-like object containing the arguments of the function. Here, arguments is
[scope, element, attributes, ...]
where ... are the arguments that angular passes to the link function, even if they're not injected in my own link function. you can find an explanation & examples in mozzila docs -
Dabbas over 8 years@Joscha this solution is very good, but if you tried to put console.log in $attr.ngIf = function{ //log here }, you'll see that this method has been called more than once, what is the cause and what is the solution please?.
-
Mark over 8 yearsHow would it work with two custom directives? I mean two directives I wrote?
-
brocksamson over 8 years@Mark if you put this code into 2 directives and place both directives on a single DOM element then angular will execute both directives based on priority order. Because this code ANDS the ng-if checks together, ultimately you'd end up with both conditionals applied to the element. Note that I haven't tested this
-
Karthick Selvam about 8 yearsngIf = ngIfDirective[0]; -> very bad we had to do this? can someone explain why?
-
iberbeu almost 8 yearsI get an error
$transclude is not a function
, which seems right to me because when you callngIf.link.apply
you are not passing any transclude, since it is not in the arguments parameter. Can you explain this? -
Markus Pscheidt over 7 yearsWorks well, only tiny blemish is the use of the deprecated
replace: true
-
Syed Suhail Ahmed over 7 yearsHow do you make this work with normal ng-if directive in the same tag?
-
ryancey about 7 yearsWorks great, thanks. But I'm trying to access the element on which the custom directive is applied, without luck.
element
seems to be the Angular comment,element[0].nextElementSibling
is the right element when logged but when accessed it's the "next-next" element (one element too far). I'd like to add a CSS class to this element. Any idea why it isn't accessible? -
Agat about 7 yearsThat's a great solution indeed! However, what's about the compiling the directing which uses isolated scope? In my case, the condition defined in "parent" ng-if would refer to parent scope, and it won't be presented in the scope.