Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Issue with isolated scope and "replace: true" in angular directive

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>
like image 918
Joe Avatar asked Mar 27 '16 10:03

Joe


People also ask

Which directive definition option is used to replace the current?

As the documentation states, 'replace' determines whether the current element is replaced by the directive. The other option is whether it is just added to as a child basically.

What is replace in AngularJS directive?

AngularJS Directive's replace option can be used to replace the container element itself by directive content. By default, the directive content inserted as the child of the element directive is applied on. But using replace, that container element altogether can be replaced by directive's actual content HTML.

What is the default scope in an angular directive?

By default, directives do not create their own scope; instead they use the scope of their parent, generally a controller (within the scope of which the directive is defined). We can change the default scope of the directive using the scope field of the DDO (Data Definition Object).

What is isolated scope in AngularJS?

Isolated scope directive is a scope that does not inherit from the parent and exist on its own. Scenario: Lets create a very simple directive which will show the object from the parent controller.


2 Answers

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.

like image 76
Estus Flask Avatar answered Sep 28 '22 03:09

Estus Flask


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

like image 21
georgeawg Avatar answered Sep 28 '22 04:09

georgeawg