6

I have been struggling with a scoping issue when making an error message directive using AngularJS.

I have an ng-if and ng-class directive as part of the directive template, but the expression in the ng-class directive always seemed to return a blank string, unless:

  1. I removed the ng-if directive.
  2. or, I removed the 'replace' key in the directive definition object.

Looking at the compiled output for my directive, it looks like an isolated scope is being created if the ng-if or the replace key is removed, but if they are both left in, then there are no ng-isolate-scope classes in the html output, just ng-scope.

I would really like to understand exactly what is going on here and would be grateful for any explanations.

Directive Definition

angular.module('myMessages')
.directive('pageMessages', function() {

    return {
        restrict: 'E',
        replace: true,
        scope: {
            messages: '='
        },
        controller: function($scope) {
            $scope.severity = 'alert-success';
        },
        template: '<div ng-if="messages.length > 0">' +
                    '<div class="alert" ng-class="severity">' + 
                        '<ul>' + 
                            '<li ng-repeat="m in messages">{{::m.message}}</li>' +
                        '</ul>' +
                    '</div>' +
                  '</div>'
    };
});

Output (note no alert-danger class is added)

<!-- ngIf: messages.length > 0 -->
<div ng-if="messages.length > 0" messages="messages" class="ng-scope">
    <div class="alert" ng-class="severity">
        <ul>
        <!-- ngRepeat: m in messages -->
            <li ng-repeat="m in messages" class="ng-binding ng-scope">test error</li>
        <!-- end ngRepeat: m in messages --></ul>
    </div>
</div>
<!-- end ngIf: messages.length > 0 --></div>

alert-danger class is added after removing replace (removing ng-if would work as well)

<page-messages messages="messages" class="ng-isolate-scope">
    <!-- ngIf: messages.length > 0 -->
    <div ng-if="messages.length > 0" class="ng-scope">
        <div class="alert alert-danger" ng-class="severity">
            <ul>
            <!-- ngRepeat: m in messages -->
                <li ng-repeat="m in messages" class="ng-binding ng-scope">test error</li>
            <!-- end ngRepeat: m in messages -->
            </ul>
        </div>
    </div>
    <!-- end ngIf: messages.length > 0 -->
</page-messages>
georgeawg
  • 48,608
  • 13
  • 72
  • 95
Joe
  • 4,852
  • 10
  • 63
  • 82
  • If anyone switched from templateUrl to template and it suddenly started having weird scope rending issues, check this out. Somehow in our case having templateUrl hid this issue, so we never saw it. – Zach Leighton Oct 27 '19 at 10:35

2 Answers2

5

The job of truthy ng-if comes to cloning an original element and giving it inherited scope. It uses transclusion for that, this allows ng-if to get inherited scope on an element with isolated scope, avoiding $compile:multidir error with Multiple directives requesting new/isolated scope verdict.

The good thing is that it won't throw an error if it is used on an element with isolated scope. The bad thing is when used on a directive with higher priority (ng-if priority is 600) it will just replace it, ignoring its scope. And another bad thing is that when used on on root template element of a directive with isolated scope (like this one) it will just replace an element with cloned one that inherits its scope from parent scope (belonging to directive's parent element, because its own element was already replaced with replace).

So it just gets severity value from pageMessages parent scope and evaluates ng-class expression to empty string if it doesn't exist.

The solution is to not use ng-if on root element of a directive with replace flag. replace flag has got deprecation status, which means that issues won't be fixed. When directive's template gets an extra <div> wrapper (though it may serve against the purpose of replace), everything should work as intended.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
4

By using replace=true and ng-if and isolate scope together, the code is attempting to directives with different scopes on the same element.

From the Docs:

In general it's possible to apply more than one directive to one element, but there might be limitations depending on the type of scope required by the directives. The following points will help explain these limitations. For simplicity only two directives are taken into account, but it is also applicable for several directives:

  • no scope + no scope => Two directives which don't require their own scope will use their parent's scope
  • child scope + no scope => Both directives will share one single child scope
  • child scope + child scope => Both directives will share one single child scope
  • isolated scope + no scope => The isolated directive will use it's own created isolated scope. The other directive will use its parent's scope isolated scope + child scope => Won't work! Only one scope can be related to one element. Therefore these directives cannot be applied to the same element.
  • isolated scope + isolated scope => Won't work! Only one scope can be related to one element. Therefore these directives cannot be applied to the same element.

-- AngularJS Comprehensive Directive API - scope


replace:true is Deprecated1

From the Docs:

replace ([DEPRECATED!], will be removed in next major release - i.e. v2.0)

specify what the template should replace. Defaults to false.

  • true - the template will replace the directive's element.
  • false - the template will replace the contents of the directive's element.

-- AngularJS Comprehensive Directive API

From GitHub:

Caitp-- It's deprecated because there are known, very silly problems with replace: true, a number of which can't really be fixed in a reasonable fashion. If you're careful and avoid these problems, then more power to you, but for the benefit of new users, it's easier to just tell them "this will give you a headache, don't do it".

-- AngularJS Issue #7636

Community
  • 1
  • 1
georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • The thing that should be noticed is that the code not only 'is attempting to directives with different scopes on the same element' but is succeeding at that. No error is thrown, and ngIf expression can use the properties of isolated scope. The manual applies to multiple scopes via 'scope' property, while ngIf gets a scope via transclusion. – Estus Flask Mar 27 '16 at 18:21
  • What I have seen is that the isolate scope gets replaced by inherited scope. This causes subtle problems. As [Caitp](https://github.com/caitp) says, ""this will give you a headache, don't do it (use replace: true)". – georgeawg Mar 27 '16 at 18:50
  • Thanks for your answer @georgeawg. V. helpful. – Joe Apr 02 '16 at 07:37