Why can't I overwrite the value of a variable like this? - javascript

I'm trying to figure out why I'm having trouble overwriting a value passed to an angularJS directive via an isolate scope (#). I try to overwrite the value of vm.index with the following:
vm.index = parseInt(vm.index, 10)
However, it doesn't work for some reason.
If I change it to:
vm.newIndex = parseInt(vm.index, 10)
It works. Also, assigning the value on the $scope works.
Why doesn't the first method work?
I've created this example plunker for reference.

As you used # here which need value from an attribute with {{}} interpolation directive. And seems like directive is getting loaded first & then the vm.index value is getting evaluated. So the changes are not occurring in current digest cycle. If you want those to be reflected you need to run digest cycle in safer way using $timeout.
$timeout(function(){
vm.index = parseInt(vm.index, 10)
})
Above thing is ensuring that value is converted to decimal value. The addition will occur on the on the directive html <h2>Item {{ vm.index + 1 }}</h2>
Working Demo
The possible reason behind this
As per #dsfq & my discussion we went through the angular $compile API, & found that their is one method call initializeDirectiveBindings which gets call only when we use controllerAs in directive with an isolated scope. In this function there are switch cases for the various binding #,= and & , so as you are using # which means one way binding following switch case code gets called.
Code
case '#':
if (!optional && !hasOwnProperty.call(attrs, attrName)) {
destination[scopeName] = attrs[attrName] = void 0;
}
attrs.$observe(attrName, function(value) {
if (isString(value)) {
destination[scopeName] = value;
}
});
attrs.$$observers[attrName].$$scope = scope;
if (isString(attrs[attrName])) {
// If the attribute has been provided then we trigger an interpolation to ensure
// the value is there for use in the link fn
destination[scopeName] = $interpolate(attrs[attrName])(scope);
}
break;
In above code you can clear see that there they placed attrs.$observe which is one sort of watcher which is generally used when value is with interpolation like in our case it is the same {{index}}, this means that this $observe gets evaluated when digest cycle run, That's why you need to put $timeout while making index value as decimal.
The reason #dsfq answer works because he use = provides two way binding which code is not putting watcher directly fetching value from the isolated scope, here is the code. So without digest cycle that value is getting updated.

Apparently it has something to do with one-way binding of the scope index value. So Angular won't update scope.index (or this.index in case of bindToController: true) because scope is configured as
scope: {
index: '#'
},
If you change it to two-way binding like:
scope: {
index: '='
},
It will work:
<some-directive index="$index"></some-directive>
Demo: http://plnkr.co/edit/kq16cpk7gyw8IE7HiaQL?p=preview
UPD. #pankajparkar made a good point that updating value in the next digest fixed the issue. This approach for the problem then is closer then what I did in this answer.

Related

Angular radio ng-change works only first time

I can't understand why angular ng-change function are called only in first click. In this example
http://jsfiddle.net/ZPcSe/5/
function is called every time when i change current radio selection, but in my code something is wrong
http://jsfiddle.net/4jL3u8ko/1/
Can someone change my code to work like first example, and explain why it isn't working?
Great question.
This is not an answer but one may call this explanations to answers and any further problems that may occur. And this happens quite often, so its important to understand. The answer given by #Victor is ok and the comment of #Atias is also helpful.
Explanation -
Simple explanation : ng-repeat forms child scope.
Little detailed :
So your application has 3 scopes (or may increase depending on the number of values in $scope.radioButtons). Let me name them - parentScope(main scope), childScope1(scope of first element of ng-repeat) and childScope2(scope of second element of ng-repeat).
Parent scope variables : calledFunctions(array), radioButtons(object) and newValue(function)
ChildScope1 variables : name(assigned a literal at the start), val(assigned an object at start) and value(it has no value, or undefined)
ChildScope2 variables : name(assigned a literal at the start), val(assigned an object at start) and value(it has no value, or undefined)
What happens when you click on the radio button of childScope1(for first time):
ChildScope1.value = ChildScope1.name or ChildScope1.value= "Radio1"; the ng-change directive checks if this scope's model has been changed? Yes, as it was undefined and now it has a literal("Radio1")!! So call ChildScope1.newValue(), which obviously is not present. Therefore now look for parent- call parentScope.newValue(). This is present, so execute it.
keep in mind that ChildScope1.value= "Radio1";
After clicking on other radio buttons......
Now lets click 2nd time on ChildScope1's radio button. After clicking it- ChildScope1.value = ChildScope1.name or ChildScope1.value= "Radio1"; the ng-change directive checks if this scope's model has been changed??? No!! Because it still contains the same literal vaule as before, so do not even look for ChildScope1.newValue() or ParentScope.newValue() function.
Now same thing happens with ChildScope2.
Hope this explains why your fiddle works like a one time binding, or how #Victor or #Atias are correct.
Simply the solution is - The ng-model of ChildScope1 or ChildScope2 or any other child scopes, should point to a variable of parent scope. So you can use $parent or make an object in parent scope(main scope) with a property( eg.- ParentVal={ChildVal = ""};). And in ng-model inside ng-repeat- write ng-model=ParentVal.ChildVal.
Sorry for my poor english and please pardon my spelling mistakes if there is any.
Thanks
ng-model is tricky. You should always, always have a dot in your models, or you get this kind of problem where you are creating several models and you think you only have one.
As a quick fix, use $parent.value instead of just value in your ng-model and ng-change.
As a good fix, have a proper model object in the scope, instead of storing values directly in the scope:
$scope.model = {};
ng-model="model.radiosValue"
As an even better fix, use the controllerAs pattern and use that object as the view model.

function called multiple times from template

In my template I have something like this:
{{formatMyDate(date)}}
but date is a scope variable that is not immediately available so this expression will call the function formatMyDate() many times (returning undefined) before returning the correct value.
I could check if the date is not null within the function but I guess it would be more clean NOT to call the function at all if the date is null.
Any way to achieve this?
Would a custom filter help me out here?
EDIT:
It was suggested that this behaviour could be normal, depending on the $digest cycle.
I've then put a scope.$watch to verify how many times the value of date is changing.
Note that I'm defining these in a directive.
scope.$watch('date', function(value){
console.log('watched_date: ' + value)
})
and I've introduced a console.log() on my formatMyDate function as well
scope.formatMyDate = function(date){
console.log("called_date: " + date)
return dateService.format(date, 'YYYY-MM-DD')
}
Inspecting the console I get (pseudo code)
called_date: undefined
watched_date: undefined
called_date: undefined // many many times (around 20/30)
called_date: correctValue //2 or 3 times
watched_date: correctValue
called_date: correctValue //other 3/4 times
I'm wondering if this is still due to the $digest cycle or it is a bug in my code
I would recommend you to do things differently:
Either use the date $filter or if you are doing something VERY unique and the date $filter is not good enough for you, then you could create your own $filter, like this:
app.filter('formatMyDate', function () {
return function (date) {
if (!date) return "";
var result;
//your code here
return result;
};
});
And use it like this in your template:
{{date | formatMyDate}}
UPDATE:
I guess that I didn't quite answer your question, I just gave you advice on how to improve your code. This time I will try to answer your question:
The $digest cycle is the stage in which Angular ensures the changes of the model have settled,
so that it can render the view with the updated changes. In order to do that,
Angular starts a loop in which each iteration evaluates all the template expressions
of the view, as well as the $watcher functions of the $scope.
If in the current iteration the result is the same as the previous one,
then Angular will exit the loop. Otherwise, it will try again.
If after 10 attempts things haven't settled, Angular will exit
with an error: The "Infite $digest Loop Error" (infdig).
That's why the first time that the $digest cycle runs all the expressions are evaluated (at least) twice. And then Every time that you make a change to the $scope or that one of the $watchers of the $scope gets triggered, the $digest cycle will run again in order to make sure that things have settled, so your expressions will be evaluated again. This is how Angular makes "data-binding" happen, it's a normal behaviour.
So in your case, when in your template you do this: {{formatMyDate(date)}} or this {{date | formatMyDate}} you're defining Angular expressions that will be evaluated every time that the $digest cycle runs, which as you can imagine is very often. That's why is very important to make sure that the $filters (or functions) that you use in your view are efficient and stateless.
You can do this:
{{date && formatMyDate(date)}}
will only execute the second case if the first condition exists and is different from null and undefined.
Check this fiddle: http://jsfiddle.net/HB7LU/7512/

Why does one AngularJS service bind work but the other doesn't

If you take the following Plunkr you will see a simple service - increments a count and it gets reported to the user.
What I am trying to understand is why this works (the increment is reported to the user on a click) - binding to the function in the view:
From the HTML
<p> This is my countService variable : {{countService()}}</p>
From the controller
$scope.countService = testService.getCount
And why this Doesn't work - binding the function to the scope:
From the HTML
<p> This is my countService variable : {{countService}}</p>
From the controller
$scope.countService = testService.getCount()
Ultimately we're binding to a function in the service, though the second one doesn't bind the new value.
A clear, easy to understand, explanation would be great :)
In the first version you bind directly to the function so angular checks if the functions output has changed. In the second version you only call the function once when the scope is created and set countService to that value. Since countService now is a variable that has nothing to do with the counting function its value wont reflec the value returned from that function.

How to Update a Parent Scope Variable from within ng-repeat Expression?

I've run into a situation where I had to update a scope variable from within an ng-repeat expression (ng-class call within ng-repeat in fact).
<div id="page" ng-controller="MainCtrl">
.
<section id="body" ng-init="countMis = {num:0}">
.
<tbody>
<tr ng-repeat="upperCaseLetter in alphabet.specialsUpper" ng-controller="letterController">
<td>{{upperCaseLetter}}</td>
<td>{{filteredLetter=(upperCaseLetter | lowercase)}}</td>
<td>{{alphabet.specialsLower[$index]}}</td>
<td><span ng-class="lowercaseEqual($index,filteredLetter)"></span></td>
</tr>
</tbody>
and in app.js:
function letterController($scope)
{
$scope.lowercaseEqual = function(indx,letter)
{
var returnStr="";
if(letter == $scope.alphabet.specialsLower[indx])
{
returnStr = "glyphicon glyphicon-ok";
}
else
{
$scope.countMis.num = $scope.countMis.num + 1;
returnStr = "glyphicon glyphicon-remove";
}
return returnStr;
};
}
Ng-repeat is in a child controller and data I want to update is in its parent controller. I know you've read same question many times, please keep reading and see JsFiddle example.
So expression function checks filtered data of that particular ng-repeat iteration, if it doesn't match with some other corresponding parent scope variable it returns a class but also it should update parent scope counter.
As those expressions don't evaluate just once, but at least once (because of dirty check in digest) it results counter being incremented more than once for each case.
I've solved my problem by counting classes applied to that particular data after ng-repeat (you'll see in the JsFiddle example).
However I think I may run into same problem in the future, so I want to learn how to update parent scope within an expression of ng-repeat iteration.
I've put a second JsFiddle, I've tried using a factory on both parent controller and child, hoping to update a common variable for both controllers. When I log (consol.log) each iteration and factory method variable, it shows iteration times + 1 (still false value), but it's not reflected on page expression...doesn't make sense at all.
I'd appreciate any help. Thanks.
JsFiddle 1 JsFiddle 2
Since watched expressions (like the one implicitely set up by the ng-class directive) "can execute multiple times per $digest() and should be idempotent".
You should understand what the $watch() and $digest() functions do and look for resources regarding Angular's digest cycle.
So, incrementing a counter inside a watchExpression is not a good idea.
Counting classes seems ok though.
BTW, the factory-approach will help you share data across scopes, but it won't help with the "multiple executions per digest cycle" problem. Furthermore, it might be redundant in your situation, since the child- and parent-scopes can share data directly.
In any case, for the results to be displayed you have to "bind" the factory's getMismatchCount() function with the getMismatchCount() function that you use in your HTML (in Mismatches Count from Factory: {{getMismatchCount()}}):
function mainCtrl($scope, $window, repeatFactory)
...
$scope.getMismatchCount = repeatFactory.getMismatchCount;

access scope variable from directive by name, avoiding using eval

I'm new to angularJs and the currently working example(fiddle) doesn't feel right. When you try to blur the field it should increment event.times, currently I doing it by $apply on line 10, so I'm asking is there a better way to achieve this ? Maybe something like this var times = scope.$get(attrs.timesField); and then scope.$apply(function(){ times += 1; });
Whenever a directive does not use an isolate scope and you specify a scope property using an attribute, and you want to change the value of that property, use $parse:
<input custom-field times-field="event.times" ng-model="event.title" type="text">
link: function (scope, element, attrs){
var model = $parse(attrs.timesField);
element.bind('blur', function(){
model.assign(scope, model(scope) + 1);
scope.$apply();
});
}
fiddle
You're right, that's not the correct way to do something like that.
Find out the $watch() function, especially when using it in directives.
There is no need to use eval here, you can just do this:
scope.$apply(function(){
scope.event.times++;
});
However you must ensure that an event object exists on scope before using this directive and I don't think that it is good for a directive to rely on data being set elsewhere.
What are you actually trying to achieve?

Categories

Resources