Daniel
Daniel

Reputation: 6481

Conditionally add angular directive attribute to an element

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.

Upvotes: 19

Views: 9507

Answers (3)

Sly_cardinal
Sly_cardinal

Reputation: 13003

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;

                }
            };

        }
    };
}]);

Upvotes: 5

John David Five
John David Five

Reputation: 831

@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;

                }
            };

        }
    };
});

})();

Upvotes: 2

michal.jakubeczy
michal.jakubeczy

Reputation: 9469

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.

Upvotes: 0

Related Questions