19

Is there a straight-forward, simple way to do the following -

<div class="my-class" my-custom-directive="{{evaluate expression}}"></div>

So that angular won't add the directive unless the expression is evaluated to true?

Edit:

The directive has to be an attribute so please, no solutions like ng-if with restrict: 'E',
ng-class with restrict: 'C' or ng-attr - which doesn't work with custom directives.

0

3 Answers 3

5

It's possible to do this by creating a directive with a high priority and terminal: true. Then you can fiddle with the element attributes (add or remove them) and then recompile the element to let the directives run.

Here is the example as a plunk: http://plnkr.co/edit/DemVGr?p=info

Change the expression in the "directive-if" attribute to keep/remove the "logger" directive.

If the expression for an attribute evaluates to false then it will be removed.

<div directive-if="{'logger': 'myValue == 1'}"
     logger="testValue">
    <p>"logger" directive exists? <strong>{{logger}}</strong></p>
</div>

Here is the directive implementation.

With some minor tweaking you could swap this around to add directives instead of removing them if that's what you would prefer.

/**
 * The "directiveIf" directive allows other directives
 * to be dynamically removed from this element.
 *
 * Any number of directives can be controlled with the object
 * passed in the "directive-if" attribute on this element:
 *
 *    {'attributeName': expression[, 'attribute': expression]}
 * 
 * If `expression` evaluates to `false` then `attributeName`
 * will be removed from this element.
 *
 * Usage:
 *
 *         <any directive-if="{'myDirective': expression}"
 *                    my-directive>
 *         </any>
 *
 */
directive('directiveIf', ['$compile', function($compile) {
    return {

        // Set a high priority so we run before other directives.
        priority: 100,
        // Set terminal to true to stop other directives from running.
        terminal: true,

        compile: function() {
            
            // Error handling - avoid accidental infinite compile calls
            var compileGuard = 0;
            
            return {
                pre: function(scope, element, attr) {

                    // Error handling.
                    // 
                    // Make sure we don't go into an infinite 
                    // compile loop if something goes wrong.
                    compileGuard++;
                    if (compileGuard >= 10) {
                        console.log('directiveIf: infinite compile loop!');
                        return;
                    }
                    // End of error handling.

                    // Get the set of directives to apply.
                    var directives = scope.$eval(attr.directiveIf);
                    angular.forEach(directives, function(expr, directive) {
                        // Evaluate each directive expression and remove the directive
                        // attribute if the expression evaluates to `false`.
                        var result = scope.$eval(expr);
                        if (result === false) {
                            // Set the attribute to `null` to remove the attribute.
                            // 
                            // See: https://docs.angularjs.org/api/ng/type/$compile.directive.Attributes#$set
                            attr.$set(directive, null)
                        }
                    });

                    /*
                    Recompile the element so the remaining directives can be invoked.
                    
                    Pass our directive name as the fourth "ignoreDirective" argument 
                    to avoid infinite compile loops.
                    */
                    var result = $compile(element, undefined, undefined, 'directiveIf')(scope);


                    // Error handling.
                    // 
                    // Reset the compileGuard after compilation
                    // (otherwise we can't use this directive multiple times).
                    // 
                    // It should be safe to reset here because we will
                    // only reach this code *after* the `$compile()`
                    // call above has returned.
                    compileGuard = 0;

                }
            };

        }
    };
}]);
Sign up to request clarification or add additional context in comments.

1 Comment

Reflecting on this, if you configure the directive with expression values instead of strings like this directive-if="{'logger': myValue == 1}" (note that myValue == 1 is an expression and not a string there) then you don't need the second scope.$eval(expr) inside the loop.
2

@Sly_cardinal is right, used his code, but had to make a few adjustments:

(function () {

angular.module('MyModule').directive('directiveIf', function ($compile) {

    // Error handling.
    var compileGuard = 0;
    // End of error handling.

    return {

        // Set a high priority so we run before other directives.
        priority: 100,
        // Set terminal to true to stop other directives from running.
        terminal: true,

        compile: function() {
            return {
                pre: function(scope, element, attr) {

                    // Error handling.
                    // Make sure we don't go into an infinite
                    // compile loop if something goes wrong.
                    compileGuard++;
                    if (compileGuard >= 10) {
                        console.log('directiveIf: infinite compile loop!');
                        return;
                    }


                    // Get the set of directives to apply.
                    var directives = scope.$eval(attr.directiveIf);

                    for (var key in directives) {
                        if (directives.hasOwnProperty(key)) {

                            // if the direcitve expression is truthy
                            if (directives[key]) {
                                attr.$set(key, true);
                            } else {
                                attr.$set(key, null);
                            }
                        }
                    }

                    // Remove our own directive before compiling
                    // to avoid infinite compile loops.
                    attr.$set('directiveIf', null);

                    // Recompile the element so the remaining directives
                    // can be invoked.
                    var result = $compile(element)(scope);


                    // Error handling.
                    //
                    // Reset the compileGuard after compilation
                    // (otherwise we can't use this directive multiple times).
                    //
                    // It should be safe to reset here because we will
                    // only reach this code *after* the `$compile()`
                    // call above has returned.
                    compileGuard = 0;

                }
            };

        }
    };
});

})();

1 Comment

Thanks, While this code might work (don't know, didn't check), I'd hardly call that "simple" or "straightforward". It looks very risky and error prone (Just look at the amount of Errror handlind comments) and i'm not angular expert enough to know the consequences it will have on my app..
0

Another approach is to create two versions of code - one when directive is needed and another one when it is not. And display using ng-if/ng-show one or another. Duplicate code can be moved to templates and these can be included.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.