AngularJS: What is the best practice for adding ngIf to the directive programmatically?

I want to create a directive that checks whether an element should be present in dom based on the value received from the service (for example, check the user role).

The relevant directive is as follows:

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 an attached ng-if attribute, but somehow it does not apply to the element and it still exists in dom. So this is obviously the wrong approach.

This script shows the problem: http://jsfiddle.net/L37tZ/2/

Who can explain why this is happening? Is there any other way of similar behavior? Existing ngIfs should be considered.

DECISION:

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 priority to the directive, otherwise other directives attached to this element will not be evaluated!

+45
javascript angularjs angularjs-directive
Dec 02 '13 at 10:01
source share
5 answers

The first part of your question is "why?" - this is what I can answer:

The problem you are facing is that you cannot dynamically apply directives to elements without calling the $compile element.

If you call $compile(element)(element.scope()) after setting the attribute, you start the stack overflow because you are compiling yourself, which forces you to compile yourself, which forces you to compile yourself, etc.

The second part, "how else to achieve," I have problems. I tried several approaches (e.g. switching content with ng-if nested), but I cannot get exactly the behavior you are looking for.

I think the next step could be to study the ng-if code and try to implement something similar directly in your directive.

Here is the first pass of getting a job . I expect this to require some cleaning and modification to make it work the way you really want it.

+7
Dec 02 '13 at
source share

You can reuse ngIf in your own directive as follows:

 /** @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 he will use all the "functions" that are supplied using ngIf .

+74
Jun 05 '14 at 16:23
source share

Joscha's answer is pretty good, but it really won't work if you use ng-if in addition to it. I took the Joscha code and added some 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 no ng-if, process normally ifEvaluator = function () { return User.isAuthenticated(); } } attributes.ngIf = ifEvaluator; ngIf.link.apply(ngIf, arguments); } }; }]); 

So, if you can 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 only be displayed if you are authenticated && & the test input is not empty.

+39
Mar 12 '15 at 13:19
source share

There is another way to solve this problem using the templating function. This requires the jquery 1.6+ function to function properly.

Working code script: 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 nested elements. Without replace = true, the string returned by the template function is placed inside the existing html. That is, <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/ $ for more details.

+2
May 01 '14 at 23:31
source share
 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 foundation of how this works: The terminal flag tells Angular to skip all lower priority directives in the same HTML element.

This is normal because we want to change the element by replacing add-condition with ng-if before calling compile , which then processes ng-if and any other directives.

+1
Dec 14 '16 at 8:18
source share



All Articles