Angular ng-show not working if parent has ng-if - 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">
Show It
</div>
<div ng-show="showIt">
This is a dynamically-shown div.
Hide it
</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">
Show It
</div>
<div ng-show="showIt">
This is a dynamically-shown div.
Hide it
</div>
</div>
The JavaScript:
scope.hideIt = function () {
scope.showIt = false;
};
Thanks,
Andy

Nemesv mentioned above that you should use $parent, but although working, this is not the right solution. The problem with this solution is:
It creates a high coupling between the scope from ng-if and the controller scope.
Because of 1, changing ng-if to ng-show will break your code.
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.

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">
Show It
</div>
<div ng-show="showIt">
This is a dynamically-shown div.
Hide it
</div>
</div>
Demo Plunker.

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">
Show It
</div>
<div ng-show="showIt">
This is a dynamically-shown div.
Hide it
</div>
</div>
And this variant also
<div ng-if="true">
<div>
visibility variable: {{showIt}}
</div>
<div ng-show="!showIt">
Show It
</div>
<div ng-show="showIt">
This is a dynamically-shown div.
Hide it
</div>
</div>
with the code
$scope.changeState= function (state) {
$scope.showIt = state;
};

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

Related

How do I reference and set all scope variables created from ng-click within ng-repeat?

I want to be able to hide the item.value when I click on item.label or just hide everything with I click on the button. I'm not too sure how the "hide" variables are related here. From what I understand, they are created separately in an isolated scope so my button should not work. What is the solution here?
<button ng-click="hide=false">HideAll</button>
<div ng-repeat="item in items">
<div ng-click="hide=!hide">item.label</div>
<div ng-hide="hide">item.value</div>
</div>
You should be using the dot syntax and within each item model, you can use your toggle button to toggle the hide property of each individual item. This way you can ng-hide each item via item.hide expression.
Angular's ng-hide directive creates a watch on the expression. When the value of the evaluated expression changes, it triggers a change to the DOM to show (if the value is falsey) or hide (if the value is not falsey). Initially, the items array does not have the hide property set, so they are all undefined, which evaluates to a falsey value, thus the items do not hide by default on initial display. Once an item's hide property is set (either by using the ng-click expression to execute the controller's toggleHide method or the controller's hideAll method).
These are some of the basics of angular expressions and core ng directives.
angular.module('myApp', [])
.controller('MainController', function () {
this.$onInit = function $onInit() {
this.items = [
{value: 'Item 1'},
{value: 'Item 2'},
{value: 'Item 3'}
];
};
this.toggleHide = function toggleHide(item) {
item.hide = !item.hide;
};
this.hideAll = function hideAll() {
var items = this.items;
for (var i = 0; i < items.length; ++i) {
items[i].hide = true;
}
};
});
<script src="//code.angularjs.org/1.6.5/angular.js"></script>
<div ng-app="myApp" ng-controller="MainController as mc">
<button ng-click="mc.hideAll()">Hide All</button>
<div ng-repeat="item in mc.items">
<div><button ng-click="mc.toggleHide(item)">Toggle</button></div>
<div ng-hide="item.hide">{{::item.value}}</div>
</div>
</div>
This answer is leveraging some features of the more recent versions of angular 1.x (controller lifecycle methods and one time binding -- none of which are shipped with the stackoverflow snippet of Angular 1.2).

How can we get the child scope value in function and toggle it (Angular Js)?

Can we change the dom level child scope value using javascript function?
<article data-ng-init="show=true" data-ng-repeat="a in obj track by $index">
<div class="holder">
<div class="submit_btn" data-ng-bind="a.name" data-ng-click="ajaxCall(a,$event,show);"></div>
</div>
<ahref ="javascript:void(0)" data-ng-click="show=true></a>
</article>
####Controller
$scope.ajaxCall = function (obj,event,show){
//after ajax success togggle show
show = !show; //nothing is happening
};
I think you did just forgot a $scope...:
$scope.ajaxCall = function(obj, event, show) {
// on ajax call success, toggle $scope.show
$scope.show = !show; // something should happen... :-)
};
Right now show property shared by all article.So, whatever you do changes in it, will affect to all.
You can define html as follows.
Assign show property to each a object.
So,it will affect only respected article.
<article data-ng-init="a.show=true" data-ng-repeat="a in obj track by $index">
<div class="holder">
<div class="submit_btn" data-ng-bind="a.name" data-ng-click="ajaxCall(a,$event,a.show);"></div>
</div>
<ahref="javascript:void(0)" data-ng-click="sa.how=true">
</a>
</article>
And call ajaxCall method with a.show
This could help
$scope.show = [];
$scope.ajaxCall = function(obj, event, index) {
// after ajax success toggles show
$scope.show[index] = !$scope.show[index];
};
<div class="submit_btn" data-ng-bind="a.name"
data-ng-click="ajaxCall(a,$event,$index);"></div>

Capture hide event in $modal of angular strap

I am using angular Strap to create a modal like :
$modal({
template : "/templ/alert-with-title.html",
content : content,
title : title,
show : true,
backdrop : true,
placement : 'center'
});
I have the written the following :
$scope.$on("modal.hide.before",function() {
console.log("Closing1");
});
$scope.$on("modal.hide",function() {
console.log("Closin2");
});
My /templ/alert-with-title.html is like this :
<div aria-hidden="true" aria-labelledby="windowTitleLabel" role="dialog"
tabindex="-1" class="modal hide fade in modal" id="">
<div class="modal-header">
<a class="fui-cross pull-right" ng-click="$hide()"></a>
<h3 ng-bind="title"></h3>
</div>
<div class="modal-body">
<div class="divDialogElements" >
<span ng-bind="content"></span>
</div>
</div>
<div class="modal-footer">
<div>
<button type="button" ng-click="$hide()"
class="btn btn-default btn-gray-l gray pull-left mar_t-4">OK</button>
</div>
</div>
</div>
However even after all this, I get no console logs when i click Ok. Why is this?
so the solution is very simple, I had to provide the scope to the $modal.
$modal({
template : "/templ/alert-with-title.html",
content : content,
title : title,
show : true,
backdrop : true,
placement : 'center',
scope : $scope
});
But what I do not understand that why for an event that is "$emit" , $on of the outside scope would not work
$emit and $broadcast are angular event handling mechanisms are distinct from events that are found in pure JavaScript. The latter traverse the DOM of your web page. the $event in angular traverses the scope hierarchy present in your module. With that being said here is an excerpt from the source code of angular-strap modal :
function ModalFactory(config) {
var $modal = {};
// Common vars
var options = $modal.$options = angular.extend({}, defaults, config);
var promise = $modal.$promise = $bsCompiler.compile(options);
var scope = $modal.$scope = options.scope && options.scope.$new() || $rootScope.$new();
the parameters you pass as the argument for your $modal service is the config object. the default object contains the default values for the parameters. The line of interest is the last line .
There it checks wether you have provided a scope object as one of the parameters. If so then a child of that scope is created via scope.$new. Else it creates a scope which is the child of the top most scope in the heirarchy.
Therfore any events which are bubbled up via $emit, from this particular scope can only be caught by the $rootScope.
In the code you posted in the question you did not provide any scope object in the parameters. Hence a child of the $rootScope is created, not of the current $scope you were working in. In the second code you posted , a child scope of your current $scope is created. That is the reason why you are able to handle the 'model.hide' and other events from your current $scope
Hope this helps :)

ng-init fetches value only once, even if the model value changes

I have a status filed deep nested to 3 levels in my model, I want to apply different classes to a div based on the value of the status. In order the save the typing and keep the html tidy, I wanted to apply short name to this status so I used ng-init to copy the values and use ng-class on ng-init variable. But ng-init gets its value only once, when I change the status again in controller it doesnt change but holds the old value. Is there a way to give short names to response.obj1.obj2.status at least inside a div?
<div ng-app="app">
<div ng-controller="statusCtrl">
<button ng-click="toggle()">toggle</button>
<div id="section1" ng-init="s1Status=response.section1.status">
<div id="statusIndicator" ng-class="{'success':s1Status==0,'error':s1Status==1}">Status</div>
</div>
<span>{{response.section1.status}}</span>
</div>
var app = angular.module('app', [])
app.controller('statusCtrl', function ($scope) {
$scope.response = {
section1: {
status: 1
}
};
$scope.toggle = function () {
if ($scope.response.section1.status === 0) $scope.response.section1.status = 1;
else $scope.response.section1.status = 0;
}
});
Demo: http://plnkr.co/edit/ES7rgf97BPFAWzkfIEhw?p=preview
EDIT:
Knockout JS has nice way to do this, where you can bind a model at any level to a div and any models referred inside that div will work with the parnet model as context. Thats what I'm looking for.
<div id="section1" data-bind="response.section1">
<div id="statusIndicator" data-bind=" css:{success:status==0, error:status=1 }">Status</div>
</div>

Why does adding a show/hide feature break my AngularJS code?

Starting with the following working fiddle:
http://jsfiddle.net/77vXu/14/
I added a few changes to add a show/hide button
http://jsfiddle.net/77vXu/27/
var myApp = angular.module('myApp', []);
myApp.controller('test', function($scope) {
$scope.show = false;
$scope.cancelMessage = '';
$scope.clickTest = function(){
alert($scope.cancelMessage);
};
$scope.toggleShow = function(){
$scope.show = !$scope.show;
}
});
But this completely breaks the character counter. What have I done wrong?
From angularjs :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. An important implication of this is if ngModel is used within ngIf to bind to a javascript primitive defined in the parent scope. In this case any modifications made to the variable within the child scope will override (hide) the value in the parent scope.
Solution 1.
Please remove ng-if from textarea see here : http://jsfiddle.net/Tex3P/
<div ng-app="myApp">
<div ng-controller="test">
<button ng-if="!show" ng-click="toggleShow()">show me</button>
<div ng-if="show">
<textarea ng-model="cancelMessage" ></textarea>
<span > {{100 - cancelMessage.length}} characters remaining</span>
<button ng-click="clickTest()" ng-if="show">clickTest</button>
</div>
</div>
</div>
Solution 2.
Define cancelMessage as a object. http://jsfiddle.net/cnre6/
<div ng-app="myApp">
<div ng-controller="test">
<p>f{{cancelMessage}}</p>
<button ng-if="!show" ng-click="toggleShow()">show me</button>
<textarea ng-model="cancelMessage" ng-if="show"></textarea>
<span ng-if="show"> {{100 - cancelMessage.length}} characters remaining</span>
<button ng-click="clickTest()" ng-if="show">clickTest</button>
</div>
</div>
var myApp = angular.module('myApp', []);
myApp.controller('test', function ($scope) {
$scope.show = false;
$scope.cancelMessage = {};
$scope.clickTest = function () {
alert($scope.cancelMessage);
};
$scope.toggleShow = function () {
$scope.show = !$scope.show;
}
});
The reason it does not work is because of the way scope variables behave when they're assigned within a child scope and your model does not have a '.' in it. ng-if creates a child scope and since your ng-model does not have a '.' in it it will assign a scope variable named 'cancelMessage' in the child scope that shadows the scope variable in the 'test' controller's scope with the same name - effectively breaking two-way model binding as soon as text is entered in the textarea.
To fix this, you should have a '.' in your ng-model:
<textarea ng-model="cancelMessage.test" ng-if="show"></textarea>
By having a '.', angular will resolve what's left of the dot first, and will find the reference defined in the 'test' controller. It then binds the 'test' property of the 'cancelMessage' model.
The important point is, binding is resolving to the same model (the model which is defined on the 'test' controller's scope.
Infamous Dot in ng-Model (by Design)
Demo Plunker
If you refer to AngularJS documentation on ng-if, it says
"The ngIf directive removes or recreates a portion of the DOM tree based on an {expression}. If the expression assigned to ngIf evaluates to a false value then the element is removed from the DOM, otherwise a clone of the element is reinserted into the DOM." (https://docs.angularjs.org/api/ng/directive/ngIf)
One thing you can do is hide/show it instead of deleting it from DOM using ng-show or ng-hide
I demonstrate this in this fiddle : http://jsfiddle.net/lookman/0rfz6d1v/
<div ng-app="myApp">
<div ng-controller="test">
<button ng-if="!show" ng-click="toggleShow()">show me</button>
<div ng-show="show">
<textarea ng-model="cancelMessage" ></textarea>
<span > {{100 - cancelMessage.length}} characters remaining</span>
<button ng-click="clickTest()">clickTest</button>
</div>
</div>
</div>

Resources