This is weird as it should be pretty straightforward. I will post my code first and then ask the question:
html -
<div ng-controller="myController" ng-switch on="addressCards">
<div>
{{addCustom}} // does not get changed
<div ng-if="addCustom === false">
{{addCustom}} // does get changed
<button type="button" class="btn btn-primary btn-icon-text" ng-click="addCustom = true">
<span class="icon icon-plus"></span>
click here
</button>
</div>
</div>
</div>
controller -
(function(){
'use strict';
angular.module('myApp')
.controller('myController',['$scope',myController]);
function myController($scope){
$scope.addCustom = false;
}
})();
So I simply introduced a scope variable - addCustom - in my controller and set it to false as default. This variable controls if a div is shown or not. I am also outputting the value of the scope on the html at 2 different locations. Please see above.
But when I change its value in an ng-click within this divs, its value is changing at the second location(within the div) but not the first one(outside the div). Because of this the div does not change state as well.
I am not able to figure what might be possibly wrong here. Can someone please help?
The thing happening is when you have ng-repeat,ng-switch and ng-if directive, angular creates child scope for those element wherever they are placed. Those newly created scope are prototypically inherited from there parent scope.
On contrast Prototypal Inheritance means?
If you have scope hierarchy, then parent scope property are accessible inside child scope, only if those property are object (originally object referenced is passed to child scope without creating its new reference). But primitive datatypes are not accessible inside child scope and if you looked at your code addCustom scope variable is of primitive dataType.
Lets discuss more about it.
Here you have myController controller which has addCustom scope variable of primitive type & as I said above ng-switch & ng-if directive are compiled they do create new child scope on that element. So in your current markup you have ng-switch on ng-controller="myController" div itself. For inner html it had created a child scope. If you wanted to access parent scope inside child(primitive type) you could use $parent notation before scope variable name. Now you can access the addCustom value by $parent.addCustom.
Here its not over when angular compiler comes to ng-if div, it does create new child scope again. Now inner container of ng-if will again have child scope which is prototypically inherited from parent. Unfortunately in your case you had primitive dataType variable so you need to use $parent notation again. So inside ng-if div you could access addCustom by doing $parent.$parent.addCustom. This $parent thing will solve your problem, but having it on HTML will make unreadable and tightly couple to its parent scope(suppose on UI you would have 5 child scope then it will look so horrible like $parent.$parent.$parent.$parent). So rather you should go for below approach.
Follow Dot rule while defining ng-model
So I'd say that you need to create some object like $scope.model = {} and add addCustom property to it. So that it will follow the prototypal inheritance principle and child scope will use same object which have been created by parent.
angular.module('myApp')
.controller('myController',['$scope',myController]);
function myController($scope){
$scope.model = { addCustom : false };
}
And on HTML you will use model.addCustom instead of addCustom
Markup
<div ng-controller="myController" ng-switch on="addressCards">
<div>
{{model.addCustom}} // does not get changed
<div ng-if="model.addCustom === false">
{{model.addCustom}} // does get changed
<button type="button" class="btn btn-primary btn-icon-text" ng-click="model.addCustom = true">
<span class="icon icon-plus"></span>
click here
</button>
</div>
</div>
</div>
Other best way to deal with such kind of issue is, use controllerAs pattern while using controller on HTML.
Markup
<div ng-controller="myController as myCtrl" ng-switch on="addressCards">
<div>
{{myCtrl.addCustom}} // does not get changed
<div ng-if="myCtrl.addCustom === false">
{{myCtrl.addCustom}} // does get changed
<button type="button" class="btn btn-primary btn-icon-text" ng-click="myCtrl.addCustom = true">
<span class="icon icon-plus"></span>
click here
</button>
</div>
</div>
</div>
From the Docs:
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.
-- AngularJS ng-if directive API Reference
The rule of thumb is don't bind to a primitive, instead bind to an object.
Scope inheritance is normally straightforward, and you often don't even need to know it is happening... until you try 2-way data binding (i.e., form elements, ng-model) to a primitive (e.g., number, string, boolean) defined on the parent scope from inside the child scope. It doesn't work the way most people expect it should work. What happens is that the child scope gets its own property that hides/shadows the parent property of the same name. This is not something AngularJS is doing – this is how JavaScript prototypal inheritance works. New AngularJS developers often do not realize that ng-repeat, ng-if, ng-switch, ng-view and ng-include all create new child scopes, so the problem often shows up when these directives are involved. (See this example for a quick illustration of the problem.)1
This issue with primitives can be easily avoided by following the "best practice" of always have a '.' in your ng-models – watch 3 minutes worth. Misko demonstrates the primitive binding issue with ng-switch.1
Ng-if introduces a different scope. Try this as an attribute of your button:
ng-click="$parent.addCustom = false"
This will assure that you're accessing the same scope.
It's because of this that it's always good practice to use the ControllerAs syntax. All attributes are bound to the controller object and namespaced accordingly, meaning you never run in to this problem. I've updated your example using the ControllerAs syntax to demonstrate its use.
HTML
<div ng-controller="myController as vm" ng-switch on="addressCards">
<div>
{{vm.addCustom}}
<div ng-if="vm.addCustom === false">
{{vm.addCustom}}
<button type="button" class="btn btn-primary btn-icon-text" ng-click="vm.addCustom = true">
<span class="icon icon-plus"></span>
click here
</button>
</div>
</div>
</div>
Controller
(function(){
'use strict';
angular.module('myApp')
.controller('myController', [ myController ]);
function myController () {
var vm = this;
vm.addCustom = false;
}
})();
Here is an excellent article providing more detail about ControllerAs and it's advantages.
Both Classic Controller and Controller As have $scope. That's super important to understand. You are not giving up any goodness with either approach. Really. Both have their uses.
Related
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.
I have a label with an input box like so:
<label class="item item-input">
<input type="text" data-ng-model="description"
placeholder="Add a Description!">
</label>
As you can see this input box is binded to the scope "description"
In my controller I have something along the lines of this:
$scope.description = "";
$scope.submit = function() {
console.log($scope.description);
};
In my HTML I also have this line:
<div ng-show="description.length <= maxChars">{{description}}</div>
The submit function is called when the user hits the submit button after typing inside the input box. The description displays correctly. As the user types into the input box the description gets updated in the HTML but NOT in the controller.
I have a feeling it has something to do with setting description to an empty string but it is clearly getting updated as far as the HTML is concerned. But it keeps console.logging and empty string regardless.
In order to get this to work I had to pass description directly into the submit function (And also update the function accordingly):
<button class="button button-positive" data-ng-click="submit(description)">Submit</button>
This is completely unnecessary since Angular should be doing Two-way data binding automatically but it's not.
Anyone have any ideas?
Any help would be appreciated.
EDIT:
Here is the full HTML due to popular demand.
<ion-view title="Moments" id="page2">
<ion-content padding="true" class="has-header">
<img src="{{picture}}" height="300px" width="100%"> </img>
<div class="row">
<div ng-show="description.length > maxChars" style= "color: red">{{description}}</div>
<div ng-show="description.length <= maxChars">{{description}}</div>
</div>
<div class="row" ng-if="description">
<div ng-show="description.length > maxChars" style= "color: red">{{description.length}} / {{maxChars}}</div>
<div ng-show="description.length <= maxChars" style= "color: green">{{description.length}} / {{maxChars}}</div>
</div>
<div class="row">
<div class="col">
<label class="item item-input">
<input type="text" data-ng-model="description" placeholder="Add a Description!">
</label>
</div>
<button style="margin-right: 5px" class="col col-10 button button-positive" data-ng-click="checkDescription(description)">OK</button>
</div>
<div class="row">
<div class="col"><ion-checkbox data-ng-change="changeLocation()" data-ng-model="location">Show Location</ion-checkbox></div>
</div>
<div class="row">
<div class="button-bar">
<button class="button button-assertive" data-ng-click="cancel()">Cancel</button>
<button class="button button-positive" data-ng-click="submit(description)">Submit</button>
</div>
</div>
</ion-content>
</ion-view>
I am also aware that in this question the submit function takes no parameters. That's how I would like it to be. Currently my submit button takes one parameter(the description). This should not be nessesary. I am having the same problem with the function 'checkDescription' as well. That function should also have no parameters but again, I am forced to pass the description directly into the function. Something I prefer not to do.
Executive Summary:
In AngularJS, a child scope normally prototypically inherits from its parent scope. One exception to this rule is a directive that uses scope: { ... } -- this creates an "isolate" scope that does not prototypically inherit.(and directive with transclusion) This construct is often used when creating a "reusable component" directive. In directives, the parent scope is used directly by default, which means that whatever you change in your directive that comes from the parent scope will also change in the parent scope. If you set scope:true (instead of scope: { ... }), then prototypical inheritance will be used for that directive.
Scope inheritance is normally straightforward, and you often don't even need to know it is happening... until you try 2-way data binding (i.e., form elements, ng-model) to a primitive (e.g., number, string, boolean) defined on the parent scope from inside the child scope. It doesn't work the way most people expect it should work. What happens is that the child scope gets its own property that hides/shadows the parent property of the same name. This is not something AngularJS is doing – this is how JavaScript prototypal inheritance works. New AngularJS developers often do not realize that ng-repeat, ng-switch, ng-view, ng-include and ng-if all create new child scopes, so the problem often shows up when these directives are involved. (See this example for a quick illustration of the problem.)
This issue with primitives can be easily avoided by following the "best practice" of always have a dot '.' in your ng-models – watch 3 minutes worth. Misko demonstrates the primitive binding issue with ng-switch.
--AngularJS Wiki -- The Nuances of Scope Prototypal Inheritance
I tried to do the '.' notation in my scope but whenever I try to reference it like this:
$scope.moment.description = "";
It says "Cannot read property 'description' of undefined.
The code needs to create the object before assigning a value to a property:
$scope.moment = {};
$scope.moment.description = "";
//OR
$scope.moment = { description: "" };
<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.
ng-show is not working in following code
<button ng-click="itemIndex=0;showHome=true;" class="btn btn-link">Home</button>
<div ng-if="itemIndex==0">
<div ng-show="showHome">{{showHome}}
<h3>Home Section</h3>
<img ng-click="showHomeItems=showHomeItems?false:true;showHome=false;" ng-src="images/home.png"/>
</div>
<div ng-show="showHomeItems">
Home Items{{showHome}}
</div>
</div>
Without ng-if it is working fine but with ng-if its not working.
Read this article.
There are some directives in Angular that create a child scope, like ng-repeat, ng-if.
Inside those scopes, the booleans (such as showHome) are searched only within that new scope, and not in the parent scope.
In order to avoid such bugs, it's considered best practice to place the logic in the controller (or service, just not in the HTML) inside an object, which is not a primitive variable and will be looked up in the scope prototypical chain.
Try declaring a variable like this:
$scope.switch = {showHome : true}
and then use it in the html like this:
<div ng-show = "switch.showHome" >
Here's Plunker
I have an external template within in a controller with ng-include. It is shown and hidden based on click event of Button.It is working as required but with $parent in ng-include Template.Is there any other better way of doing this ?
Html
<body ng-controller="MainCtrl">
<div data-ng-include="'terms.html'" data-ng-show="otherContent"></div>
<div ng-show="mainPage">
<p>Hello {{name}}!</p>
<button data-ng-click="mainPage=false; otherContent=true">Link to some Content</button>
</div>
</body>
JS
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.mainPage=true;
});
External Template
<p>Some content here </p>
<button data-ng-click="$parent.mainPage=true; $parent.otherContent=false">Back</button>
Option1 - Set property on an object in the scope
In the main controller add an object on the scope.
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
$scope.page={mainPage:true};
});
and in the ng-click do:-
<div data-ng-include="'terms.html'" data-ng-show="page.otherContent"></div>
<div ng-show="page.mainPage">
<button data-ng-click="page.mainPage=true; page.otherContent=false">Back</button>
<!-- -->
<button data-ng-click="page.mainPage=true; page.otherContent=false">Back</button>
Demo - setting property on an object in the scope
Option2 - Use function
Instead of setting properties on the view (Which is anyways a good idea to abstract out too much logic from the view), Do your set operations in the controller exposed as a function that can be invoked from the view, which also gives extensibility when you need to add more logic for that particular action. And in your case you could even use the same function and call it from both the button clicks and flipped based on a boolean argument.
Demo - with function
Option3 - Use Controller Alias
Using an alias for the controller, which is nothing but instance of the controller is set as a property on the scope with the property name same as the alias provided. This will make sure you are enforce to use dot notation in your bindings and makes sure the properties you access on the child scopes with the controller alias are inherited as object reference from its parent and changes made are reflected both ways. With angular 1.3, it is also possibly to set the isolate scoped directive properties are bound to the controller instance automatically by setting bindToController property in the directive configuration.
Demo - With Controller alias
ControllerAs is the recommend way of avoiding this problem.
Using controller as makes it obvious which controller you are accessing in the template when multiple controllers apply to an element.
If you are writing your controllers as classes you have easier access to the properties and methods, which will appear on the scope, from inside the controller code.
Since there is always a . in the bindings, you don't have to worry about prototypal inheritance masking primitives.
<body ng-controller="MainCtrl as main">
<div data-ng-include="'terms.html'" data-ng-show="main.otherContent"></div>
<div ng-show="mainPage">
<p>Hello {{main.name}}!</p>
<button data-ng-click="main.mainPage=false; main.otherContent=true">Link to some Content</button>
</div>
</body>
Here are some resources for controller as:
http://www.johnpapa.net/angularjss-controller-as-and-the-vm-variable/
http://toddmotto.com/digging-into-angulars-controller-as-syntax/
https://docs.angularjs.org/api/ng/directive/ngController#example