Angular ngModel binding on first level method of $scope - javascript

This is not exactly a question with some code, but more for my understandings. So please forgive me if this isnt appropriate here..
I was playing with a simple checkbox with an ngModel assigned:
<input type="checkbox" ng-model="someFlag"/>
I expect this to bind the boolean of the checkbox to $scope.someFlag (if controller and everything else configured properly).
And yes, it does work. But there are times where I found that this is not working. Case: When I try to do something when the someFlag changes (for example in a $watch) the value is not really binded.
But then I came accross something a collegua at work mentioned once:
Use a wrapper object
Doing it like that works now without any problems:
<input type="checkbox" ng-model="wrapperObject.someFlag"/>
Watching $scope.wrapperObject.someFlag works as expected.
Now the question: Why??

When ngModel directive is executed, it reads attribute's ng-model value (in this case, "someFlag") and saves it in it's local function scope (not to confuse to angulars $scope). But since a boolean in javascript is a primitive, you cannot pass it by reference, only by value. That means only the $scope.someFlag's value (true or false) get's copied to ngModel, and not the way of accessing and modifying $scope.someFlag from ngModel.
When you use wrapper object, it is passed by reference, meaning, it is the same thing both in $scope.wrapperObject and in ngModel's local function scope, because it points to the same memory address behind the scenes.
I'm not sure if this explanatory enough, but when working with primitive values in angular, you must keep in mind the difference between passing by reference and by value and that when primitive's change, other parts of application might not know this.

In my understanding you want to pass Boolean value if check box checked to controller
try this:
In your html:
<input type="checkbox" ng-model="value"
ng-true-value="YES" ng-false-value="NO"> <br/>
in your controller:
$scope.value is gives Boolean value//

This is because scope inheritance. A nice article about angularjs scope and inheritance
I guess the $watch logic you mentioned might have been triggered from inside a ng-repeat or a ng-if. Remember angularjs creates a new scope variable for each object inside ng-repeat and ng-if.
<input type="checkbox" ng-model="someFlag"/> //This is a primitive $scope.someFlag
<input type="checkbox" ng-model="obj.someFlag"/> //This is an object $scope.obj.someFlag
<div ng-repeat="opt in options">
Here a new child scope - $scope.somechild is created for the opt variable.
So if primitive variable someFlag is referenced here it will be taken as $scope.child.someFlag. so any update here will not update the parent $scope.someFlag.
But if an object obj.someFlag is referenced here by object inheritance the compiler will try to find obj.someFlag in child - $scope.child as it is not present it will search in parent $scope as it is present this will now refer to $scope.obj.someFlag and any modification done here is done in the actual parent scope.

This is part of the mantra: "there should always be a dot in your model" by Misko Hevery, one of the fathers of Angular. You can, and probably should, watch this video: https://www.youtube.com/watch?v=ZhfUv0spHCY&feature=youtu.be&t=32m51s
ngModel is a directive that does two-way-data-binding. Binding to a primitive (Boolean in this case) will make the setter to set it on the current scope, rather than on the scope that's defined which could interfere with other scopes.
In your scenario, in a parent scope we have your $scope.someFlag = true. In a child scope we then have your input:
<input type="checkbox" ng-model="someFlag"/>
That will work initially but once the user changes the value, the someFlag will be created on the child scope and the bindings will read and write from that value from now on.
I hope this is, somehow, clear.
Note that this only happens with two-way-data-binding directives and not regular ones such as ngDisabled or ngHide

Related

AngularJS - Create a directive that uses ng-model and md-autocomplete

I am trying to create a directive for md-autocomplete. I tried using the answer provide by AngularJS - Create a directive that uses ng-model, but it does not work for me.
My CodePen is here: http://codepen.io/colbroth/pen/QyMaQX/?editors=101.
The code is based on the Angular material demo for autocomplete at https://material.angularjs.org/latest/demo/autocomplete. I have multiple pages, which need an autocomplete field to select a state. I do not want to repeat the code for each web page component.
state-directive takes an md-autocomplete input, I need demoCtrl.selected state to reflect the same value in both cases. But when I update the input element, the state-directive does not reflect this and vice versa.
<input ng-model="demoCtrl.selectedState">
<state-directive ng-model="demoCtrl.selectedState">...</state-directive>
You are on the right track. Your problem is that your model is a string - a primitive in javascript but a ngModel always needs to be an object if you want to avoid these kind of problems.
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 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.)
This issue with primitives can be easily avoided by following the "best practice" of always have a '.' in your ng-models.
Taken from Understanding-Scopes
It also links to this Youtube video - 3 minutes of well invested time
function DemoCtrl() {
var self = this;
self.state = {
selected: "Maine"
};
}
Fixed codepen

Data binding in directive isolated scope

I want to create a directive with isolated scope, but I'm not able to get it working.
jsFiddle
I want to isolate age model in a directive scope. I want to perform some business logic on that model and then set that model to parent binding. I hope the fiddle is explanatory.
I am also adding a button to the template which when clicked should invoke a submit function:
<button ng-click="submit()">click me</button>
It seems the button is working fine, but why is $scope.$watch() is not begin triggered? In a normal situation, if I change the view value it will automatically update the model value. But now it isn't.
$watch requires a dollar sign, and you pass either a function or a string that is evaluated on your scope, i.e.:
$scope.$watch('age', function(value) {
There are many more errors in your code, for instance you don't have a declared variable called 'age' so this line will reference window.age and give you an error because it is undefined, you need to say $scope.age I think:
age = age+10;
It just looks like your updated fiddle is a playground, hope these point you in the right direction. I'd recommend going through the egghead.io angular videos.

Angular multiple instances of same directive, scope is not isolated

I have a problem when creating multiple directives with isolated scope: when I change something in 1st directive it also makes changes in all other directives.
Here is a working example: http://plnkr.co/edit/drBghqHHx2qz20fT91mi?p=preview
(try to add more of Type1 'Available notifications' - a change in 1st will reflect in all other directives of Type1)
I found some solutions to similar problems here but they don't work in my case. Also found a working solution with mapping 'subscription' data to local scope variables in directive (app.js, line 76) but I think there should be a more general way to do this right?
In your directive 'notificationitem' you have the following code, keep it in mind as i explan:
// if all variables are mapped in this way than works
//$scope.enabled = $scope.subscription.enabled;
The reason why all of the 'isolated' scopes are updating is because of this code in your scope declaration in the same directive (notificationitem):
scope: {
subscription: '=',
index: '#'
},
The equal sign on subscription is angular's way of saying "Whenever the current scope updates, go to the parent and update that value as well." This means whenever you update your 'isolated' scope, it updates the parent scope as well. Since all of these isolated scopes are binding to the parent, they will change as well.
Since you want the subscription.value to be the default value of that text field, you will need to do exactly what your commented code is doing:
scope.value = scope.subscription.value;
This will create an isolated value inside of the isolated scope. When scope.value changes, scope.subscription.value will not. All of the text fields now have their own 'value' to keep track of.
Check out this article for information on directive bindings: http://www.ng-newsletter.com/posts/directives.html
Also, another way to get the default value would be to inject your service into the directive, if you don't like the above solution. Hope this all helps.

Is 'this' truly a reference to "$scope" in AngularJS's context?

I'm quite new to the superheroic framework AngularJS, so please excuse my noobness! So, while I was trying to develop a simple SPA that allowed admins to change permissions of other users in my website through a table filled with selection checkboxes, I came by an issue. I defined $scope.checked so I could know how many users the administrator chose and also $scope.mainCheckBox which would hold the correct CSS class for the major checkbox (like the one from Gmail, which you use to select all/none) in each of the three situations: all users selected, no users selected and partial selection. This simple logic goes as follows:
$scope.updateMainCheckBox = function(){
if(this.checked==this.users.length) this.mainCheckBox = "qm-icon-checked";
else if(!this.checked) this.mainCheckBox = "";
else this.mainCheckBox = "qm-icon-minus";
};
Running this at the end of the ng-click event callback of the other checkboxes, this code was capable of choosing the class correctly but it wouldn't assign the class to the mainCheckBox. When I changed every this.mainCheckBoxto $scope.mainCheckBox it all went well and the page would behave as expected.
So, I know in vanilla Js this is a reference to the window object but since AngularJS has its own event handling loop, I believe that's not the case now. I expected it to be a ref to $scope, since checked and users were found, but apparently it's not. So, what's happening here? And also, is there a more "angular" way to implement this?
You're mostly right--in the case of your function, this did reference a scope. But, it did not reference $scope; instead, it referenced one of $scope's children.
Try this: add some console.log statements to log out the ID of the $scope variable, and then the ID of the this scope in your callback:
$scope.updateMainCheckBox = function(){
console.log('$scope:', $scope.$id);
console.log('this:', this.$id);
// ...
};
You'll find the IDs are different. This is because the ngRepeat directive creates a child scope, which is a scope that inherits prototypally from its parent. This means that when you assign directly to a value on that scope, it's not reaching the parent scope, which is $scope.
I highly recommend a read through the excellent (and lengthy) "The Nuances of Scope Prototypal Inheritance" by Mark Rajcok; it esplains why you see this seemingly strange behavior on child scopes. (You can also find a copy on the AngularJS wiki.)

In AngularJS, how to make an isolated scope inherit from ng-repeat's scope

I'm trying to create a custom component that receives arguments in a ng-repeat loop.
So for example, say I have a component named "mycomp" that receives a custom argument "name" in a ng-repeat:
<mycomp name="{obj.name}" ng-repeat="obj in list" />
And in my directive the isolated scope is defined like this:
scope:{name:"#"}
That won't work because ng-repeat creates an isolated scope for each element it iterates. So I ended up having two levels of scopes.
How do I get around this issue? Am I doing something wrong?
Thanks.
As I stated in my comment of your original question, this has already been answered. Anyway, here it is, summed up:
In your template, state the model you want to have inherited, without {{}} (as using brackets results in the value being passed, and not the reference to the model itself):
<mycomp name="obj.name" ng-repeat="obj in list" />
And in your directive, establish a 2-way binding, like so:
scope:{name:"="}
EDIT:
I realize now (after your comment) that while this solves your problem, it doesn't fully answer the question. Here goes:
When you create a directive you have the choice of creating a scope that inherits from its parent (controller, typically, though not necessarily) ou an "isolated" scope, by specifying scope: true or scope: {...}, respectively.
So, by creating an unisolated scope, all the parent's models are available (you can access scope.obj - created via ng-repeat - but also scope.list). This is convenient, but also dangerous, of course (and doesn't really create reusable code).
If you create an isolated scope, you can specify the scope's models using '#', '=' or '&'.
'#' and '&' both produce a isolated, unbinded value, (that, if you change, changes only on the isolated scope - in your case, the object in the original list suffers no change at all), the only difference being that '#' reads a string value, and '&' reads an expression.
THIS IS IMPORTANT: the reason why I believe your code didn't work was (only) because you passed name="{obj.name}" and not name="{{obj.name}}", for with '#' the string value is read, and that string value can be the name of obj, but you must include it in {{}}!
If you use '=', you are declaring that you want that variable to be binded with the specified outside variable. So, if (in a fit of crazy, crazy rage!) you want to have 2 models in your directive that start up with the same value, but on is binded (i.e. changes are propagated to the outside scope), you could do something like this:
<mycomp binded-name="obj.name" unbinded-name="{{obj.name}}" ng-repeat="obj in list" />
and in your directive:
scope:{
bindedName: "=",
unbindedName: "#"
}

Categories