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

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>

Related

ng-keyup firing but not reading properly when used with an ng-if inside a directive template

I have a directive and it works fine in a way such that when I type something the search() scope function inside my directive fires and sets $scope.query with the input text.
here is the directive template
<div class="container">
<div class="system-filter-header">
<div class="no-gutter">
<div class="system-search-wrapper search-wrapper-width">
<i ng-click="search($evt)" class="fa fa-search"></i>
<input type="text" ng-keyup=search($evt) class="search pull-left suggesstions-styles"
ng-model="query" ng-attr-placeholder="Search...">
</div>
</div>
</div>
</div>
here is the scope function which gets triggered
$scope.search = function() {
console.log($scope.query.length)
}
But when I used an ng-if="true" in first line of template (true used for generalizing only, I want to do a different conditional check inside ng-if) such that,
<div class="container" ng-if="true">
still the search gets triggered but the console.log gives always 0 and it doesn't seem to update the $scope.query value as it stays as $scope.query = ''
throughout the typing.
EDIT
Here is a an example codepen with almost similar behaviour. The problem is with the searchBox directive and I have added ng-if=true to the template but searching doesn't work. When I remove the ng-if searching works fine.
Any reason for this?
Rule of thumb in AngularJS: your ng-model should always include a dot. Otherwise AngularJS directives that create child scopes (like ng-if or ng-repeat) will create a duplicate property on that child scope instead of the parent scope. Following the controllerAs convention completely mitigates this behavior.

Why do I need to specify $parent inside ngRepeat but not on the same element as ngRepeat

<div class="btn-group" role="group" aria-label="...">
<label ng-repeat="year in years" ng-class="{'btn-primary': year === selectedYear}" class="btn btn-default">
<input type="radio" ng-model="$parent.selectedYear" name="year" ng-value="year" />
{{year}}
</label>
</div>
In this code, I understand that I need to use $parent to bind to $scope.selectedYear because ngRepeat creates its own scope and selectedYear is a primitive which belongs to the parent scope.
What I don't understand is how ng-class="{'btn-primary': year === selectedYear} works.
Is the ngClass inside the ngRepeat's scope? If so, why does selectedYear not need $parent and if not, how can it use year which is inside the ngRepeat's scope?
angular.module("app", [])
.controller("controller", function($scope) {
$scope.currentYear = new Date().getFullYear();
$scope.selectedYear = $scope.currentYear;
$scope.years = [$scope.currentYear - 1, $scope.currentYear, $scope.currentYear + 1];
});
body > div {
padding: 15px;
}
.btn-group > label > input[type="radio"] {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" />
<div ng-app="app" ng-controller="controller">
<div class="btn-group" role="group" aria-label="...">
<label ng-repeat="year in years" class="btn btn-default" ng-class="{'btn-primary': year === selectedYear}">
<input type="radio" ng-model="$parent.selectedYear" name="year" ng-value="year" />
{{year}}
</label>
</div>
<br/>
<br/>
Selected year: {{selectedYear}}
</div>
You could do it also without $parent reference. The problem is in scope inheritance. When you create child scope, all variables - both objects and primitive variables are in child scope with the same names as they were in parent. But when you change primitive variables inside child scope, they are changed only inside child scope.
There are a lot of information about scope inheritance in internet. For example there is good explanation: https://github.com/angular/angular.js/wiki/Understanding-Scopes
What you could do to avoid such cases - always put your primitive variables, which you want to bind, inside objects (like in my code snippet below)
About your question -
Is the ngClass inside the ngRepeat's scope
I think, ngClass creates their own scope. This scope inherits parents scope's variables (ngRepeat's scope and your controller's scope). So, primitive variable selectedYear in ng-repeat's scope and in ng-class scope are not the same. Not correct, see comments
angular.module("app", [])
.controller("controller", function($scope) {
$scope.settings = {currentYear: new Date().getFullYear()};
$scope.settings.selectedYear = $scope.settings.currentYear;
$scope.settings.years = [$scope.settings.currentYear - 1, $scope.settings.currentYear, $scope.settings.currentYear + 1];
});
body > div {
padding: 15px;
}
.btn-group > label > input[type="radio"] {
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" />
<div ng-app="app" ng-controller="controller">
<div class="btn-group" role="group" aria-label="...">
<label ng-repeat="year in settings.years" class="btn btn-default" ng-class="{'btn-primary': year === settings.selectedYear}">
<input type="radio" ng-model="settings.selectedYear" name="year" ng-value="year" />
{{year}}
</label>
</div>
<br/>
<br/>
Selected year: {{settings.selectedYear}}
</div>
To answer the first question
Is the ngClass inside the ngRepeat's scope?
Since ng-repeat and ng-class are present on the same element, the order of execution is determined by the priority of each directive. The priority of ng-repeat is 1000 and that of ng-class is 0 which means the order of execution should ideally be
1) Compile of ng-repeat
2) compile of ng-class
3) link of ng-repeat
4) link of ng-class
according to my understanding, since ng-class gets linked first (where scope is attached), you don't have to use $parent.selectedYear.
I could be wrong but this is my understanding. Hope it might be of some help.
EDIT
After reading the code for ng-class and ng-repeat. Had a few corrections/changes to be added
Firstly, neither ng-class nor ng-repeat creates isolated scope.
Secondly, the order of execution that i mentioned was not accurate. The order should be
1) compile of ng-repeat
2) link of ng-repeat
3) link of ng-class (ng-class doesn't have compile)
This is because ng-repeat uses $watchCollection in its link function which updates the scope async using a setTimeout.
Now during the link phase of ng-repeat, it loops through the array and uses the transclude function to attach scope to the template (which is the whole div with ng-repeat in this case.)
This would mean, all repeated elements will have a new scope which follows prototypical hierarchy. This would mean that you could access the properties attached to the scope (in this case, properties attached to scope in the controller) inside child scope (scope of ng-repeat).
Since ng-class again doesn't have isolated scope and doesn't create a new scope internally, the scope in ng-class should be the same as used in ng-repeat.
Any suggestions/modifications are much appreciated as i am no pro.
It would be great if you could attach a working plunker.
To answer How do i use year in ng-class?. If by year you mean the value entered in the text-box by the user, you could use it just the way you are using it and it should work.
You can read the value from the parent scope without $parent due to the scope prototypical inheritance.
For ng-model you have to use $parent to refer the parent scope directly because ng-model uses two-way binding - otherwise the new value would be set to the child scope instead of parent because selectedYear is primitive.

Directive doesn't work when I which the version of Angular to 1.0.1 to 1.2.27

The following could be run in demo here.
this is html:
<div ng-controller="MyCtrl">
<h2>Parent Scope</h2>
<input ng-model="foo"> <i>// Update to see how parent scope interacts with component scope</i>
<br><br>
<!-- attribute-foo binds to a DOM attribute which is always
a string. That is why we are wrapping it in curly braces so
that it can be interpolated.
-->
<my-component attribute-foo="{{foo}}" binding-foo="foo"
isolated-expression-foo="updateFoo(newFoo)" >
<h2>Attribute</h2>
<div>
<strong>get:</strong> {{isolatedAttributeFoo}}
</div>
<div>
<strong>set:</strong> <input ng-model="isolatedAttributeFoo">
<i>// This does not update the parent scope.</i>
</div>
<h2>Binding</h2>
<div>
<strong>get:</strong> {{isolatedBindingFoo}}
</div>
<div>
<strong>set:</strong> <input ng-model="isolatedBindingFoo">
<i>// This does update the parent scope.</i>
</div>
<h2>Expression</h2>
<div>
<input ng-model="isolatedFoo">
<button class="btn" ng-click="isolatedExpressionFoo({newFoo:isolatedFoo})">Submit</button>
<i>// And this calls a function on the parent scope.</i>
</div>
</my-component>
</div>
And this is js:
var myModule = angular.module('myModule', [])
.directive('myComponent', function () {
return {
restrict:'E',
scope:{
/* NOTE: Normally I would set my attributes and bindings
to be the same name but I wanted to delineate between
parent and isolated scope. */
isolatedAttributeFoo:'#attributeFoo',
isolatedBindingFoo:'=bindingFoo',
isolatedExpressionFoo:'&'
}
};
})
.controller('MyCtrl', ['$scope', function ($scope) {
$scope.foo = 'Hello!';
$scope.updateFoo = function (newFoo) {
$scope.foo = newFoo;
}
}]);
This should be a good example for three kinds of scope binding in directives.However, it just doesn't work when I try to switch a higher angular version - (1.2.27). I suspect the shadow of the inherited scope within the directive, but I'm not sure of it.
This isn't going to work the way you expect. Isolated Scopes are created and provided to the Link, Compile, and Template portions of a Directive. However, the HTML within the Element itself is not actually part of the Directive. Those HTML portions are still bound to the parent $scope. If you have a tendancy to name your isolated scope objects the same, you may have just been working against the $scope unintentionally and not noticed any ill effect. If your HTML was in a Template rather than inside the Element, it would access the isolate scope.
As an example, in the HTML that is inline in the Element, you can call updateFoo(), but that would not be possible from inside a Template

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">
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

angularjs: directive breaks expression and data-ng-show?

My html:
<div ng-app="myApp">
<div ng-controller="testCtrl">
<div in-tags text="{{ tags }}"></div>
<div data-ng-show="tags.length" in-tags text="{{ tags }}"></div>
<p data-ng-show="tags.length">another text</p>
</div>
</div>
And js:
.controller('testCtrl', function($scope){
$scope.tags = 'one two three';
})
.directive( 'inTags',function() {
return {
scope: {
text: '#'
},
template: '<span ng-repeat="item in text | splitByWords"> {{ item }} </span>'
};
})
.filter( 'splitByWords', function() {
return function( text ) {
return text.split( /\s+/ );
};
});
How it works: http://jsfiddle.net/3HT2F/12/
Question is: Why tags.length interpreted like false with directive?
extra question: How can i hide div?
For your primary question, the scope attribute on your directive (inTags) sets a new isolated scope with only one member (the connected text attribute). It's one of the stumbling blocks of Angular with nested scopes and isolated scopes. When you set a literal object for the scope and specify a mapping (such as this case with at the dom attribute binding using '#'), it creates an isolated scope that doesn't inherit any other values from its parent. So tags is no longer a member of the local scope on that element.
See the scope rules for directives
Second question, why wouldn't ngShow or ngHide work? If you're on a new enough Angular (1.2+), you can also use ngIf to complete remove elements vs just hiding them.
Edit: Here's your fiddle updated

Resources