Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular ng-show not working if parent has ng-if

Tags:

angularjs

I have a view where a parent div has ng-if on it, and some child element has ng-show on it. It seems that the ng-show isn't working correctly when nested under an element with ng-if on it. Is this an Angular bug or am I doing something wrong? See this plunker.

The HTML:

<!-- with ng-if on the parent div, the toggle doesn't work -->
    <div ng-if="true">
      <div>
          visibility variable: {{showIt}}
      </div>
      <div ng-show="!showIt">
          <a href="" ng-click="showIt = true">Show It</a>
      </div>
      <div ng-show="showIt">
        This is a dynamically-shown div.
        <a href="" ng-click="hideIt()">Hide it</a>
      </div>
    </div>

    <br/><br/>

    <!-- with ng-show on the parent div, it works -->
    <div ng-show="true">
      <div>
          visibility variable: {{showIt}}
      </div>
      <div ng-show="!showIt">
          <a href="" ng-click="showIt = true">Show It</a>
      </div>
      <div ng-show="showIt">
        This is a dynamically-shown div.
        <a href="" ng-click="hideIt()">Hide it</a>
      </div>
    </div>

The JavaScript:

scope.hideIt = function () {
  scope.showIt = false;
};

Thanks,

Andy

like image 346
Andy Avatar asked Jan 02 '14 14:01

Andy


People also ask

Can we use ngIf and Ng-show together?

ng-if is better in this regard. Using it in place of ng-show will prevent the heavy content from being rendered in the first place if the expression is false. However, its strength is also its weakness, because if the user hides the chart and then shows it again, the content is rendered from scratch each time.

What is the difference between Ng-if and ng-show ng-hide?

Basic Difference between ng-if, ng-show and ng-hideng-if can only render data whenever the condition is true. It doesn't have any rendered data until the condition is true. ng-show can show and hide the rendered data, that is, it always kept the rendered data and show or hide on the basis of that directives.

Does ng-If create a new scope?

Note that when an element is removed using ngIf its scope is destroyed and a new scope is created when the element is restored. The scope created within ngIf inherits from its parent scope using prototypal inheritance.

Is ng-show a directive?

AngularJS ng-show DirectiveThe ng-show directive shows the specified HTML element if the expression evaluates to true, otherwise the HTML element is hidden.


2 Answers

Nemesv mentioned above that you should use $parent, but although working, this is not the right solution. The problem with this solution is:

  1. It creates a high coupling between the scope from ng-if and the controller scope.
  2. Because of 1, changing ng-if to ng-show will break your code.
  3. As soon as your going to nest more scopes it becomes a mess ($parent.$parent.$parent....)

Solution:

The quick correct solution is to not define showIt directly on your scope, but instead place it in an object (e.g. component.isVisible).

Explanation:

To understand why this seemingly counter-intuitive solution works and is indeed the correct one you first need to know a little more about how inheritance works with angular:

Scopes inherit from each other using prototypal inheritance, which is the form of inheritance build in into Javascript. This looks as followed:

var myScope = {
    showIt : false
}
var ngIfScope = {};
nfIfScope.__proto__ = myScope;

When you now get a property on the ngIfScope object that is not present there it will look in it's prototype to find it there. So if you request ngIfScope.showIt the browser does something like this:

if (ngIfScope.hasOwnProperty("showIt")) {
    return ngIfScope.getOwnProperty("showIt"); // getOwnProperty does not actually exist in javascript
} else {
    return ngIfScope.__proto__.showIt;
}

(in reality this happens recursively, but that's unimportant for this example).

Setting a property is much more straightforward though:

ngIfScope.setOwnProperty("showIt", newValue);

Now we have this information we can see what actually went wrong with your original implementation.

We started with the following scopes:

var myScope = {
    showIt : false
}
var ngIfScope = {};
ngIfScope.__proto__ = myScope;

When the user clicks the show button the following code is executed:

ngIfScope.showIt = true;

and the resulting scopes are:

var myScope = {
    showIt : false
}
var ngIfScope = {
    showIt : true
}
ngIfScope.__proto__ = myScope;

As you can see the new value is written in ngIfScope, and not in myScope as you probably expected. The result is that ngIfScope.showIt overshadows the variable from myScope and myScope.showIt isn't actually changed at all.

Now lets see what happens if we place the visibility trigger in an object.

We begin with the new scopes:

var myScope = {
    component : {
        isVisible : false
    }
};
var nfIfScope = {};
ngIfScope.__proto__ = myScope;

Not much changed so far. But now lets see what happens when the user clicks the button:

ngIfScope.component.isVisible = true;

With some helper variables we can see how this is executed in the browser:

var tempObject = ngIfScope.component; 
tempObject.isVisible = true;

The first line here is a get operation. Since component is not defined on ngIfScope the Javascript engine will look at the prototype of ngIfScope (myScope) to find it there, as I explained above. Thus:

tempObject === ngIfScope.__proto__.component === myScope.component 

We are now changing values directly on myScope.component, and hence the variables are not overshadowed this time. Resulting scopes are:

var myScope = {
    component : {
        isVisible : true
    }
};
var ngIfScope = {};
var ngIfScope.__proto__ = myScope;

We now have a working implementation without explicitly binding to a $parent scope, and thus there is no (or little) coupling between the scopes. Prototypal inheritance does the work for us, and nested scopes work right out of the box as well.

like image 82
Tiddo Avatar answered Nov 11 '22 17:11

Tiddo


Unlike ng-show the ng-if directive creates a new scope.

So when you write showIt = true inside the ng-if you are setting the showIt property on your child scope and not on your main scope.

To fix it use the $parent to access your property on your parent scope:

<div ng-if="true">
   <div>
       visibility variable: {{showIt}}
   </div>
   <div ng-show="!showIt">
       <a href="" ng-click="$parent.showIt = true">Show It</a>
   </div>
   <div ng-show="showIt">
     This is a dynamically-shown div.
     <a href="" ng-click="hideIt()">Hide it</a>
   </div>
 </div>

Demo Plunker.

like image 39
nemesv Avatar answered Nov 11 '22 19:11

nemesv