Andy
Andy

Reputation: 2749

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

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

Upvotes: 7

Views: 12888

Answers (4)

phazei
phazei

Reputation: 5437

So what Tiddo said about Namesv is true, but also overtly complex.

This'll do it:

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

Containing showIt in an object on the parent scope will ensure it's the same object when using it in children scopes

Upvotes: -1

Tiddo
Tiddo

Reputation: 6554

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.

Upvotes: 18

IgorCh
IgorCh

Reputation: 2661

Use function or expression in the both cases, so this variant is working.

<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="showIt = false">Hide it</a>
  </div>
</div>

And this variant also

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

with the code

$scope.changeState= function (state) {
  $scope.showIt = state;
};

Upvotes: 1

nemesv
nemesv

Reputation: 139778

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.

Upvotes: 13

Related Questions