I'm trying to implement an animdated version of ng-show and ng-hide; I originally tried to use jQueryUI.toggle('slide', …), but since $watch fires multiple times, my elements were toggling in and then immediately toggling out (sometimes more than once). But I saw in AngularJS's github issues that that is $watch's intended behaviour (dirty checking).
So I thought, okay this little eff-you be damned, I'll explicitly show or hide instead of the simple toggle: I broke it down to check the value of $watch's newValue like so:
scope.$watch(condition, function myShowHideAction(newValue,oldValue) {
if ( newValue !== oldValue ) {
if (newValue) {
elm.show("slide", { direction: direction }, "slow");
} else {
elm.hide("slide", { direction: direction }, "slow");
}
}
}
Fire it up, and what do I see? Toggling!
After putting in a few console logs, I discover somehow my condition's value is being changed during $watch's loop (which iterates like 6 times instead of just once, but that should be neither here nor there). However, the actual scope parameter's value does not change (as it shouldn't) half way thru like newValue's does.
What the heck is going on?
Plunker
The reason this happens is because all your infoboxes share the same scope. If you put a console.log(scope) statement in your mySlide directives linking function, you'll see that it's created several times with the same scope. So you have multiple watches for the same condition in the same scope.
Your code isn't the easiest to follow I'm afraid, but the issue seems to be inside my-map.js on the line 87. Instead of doing $compile(elm.contents())(scope);, you should probably do $compile(elm.contents())(scope.$new()); to create an isolated scope for that info box.
Related
I am using angular-meteor and would like to perform a function on each object. I tried running this function within an ng-repeat in the view, but I am getting massive amounts of function calls and can't figure out why. I tried to make it as simple as possible to demonstrate what is going on.
constructor($scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
this.loaderCount = 0;
this.helpers({
loaders() {
return Loaders.find( {isloader:true}, {sort: { name : 1 } })
}
});
That gives me 26 Loaders. My function just adds 1 to the count every time the function is called:
displayLoaderCount()
{
return ++this.loaderCount;
}
Now in my view, I am looping through each loader, and calling the function. This should in my mind give me 26, but instead I am getting 3836.
<tr ng-repeat="loader in loaderExhaustion.loaders">
<td>{{loaderExhaustion.displayLoaderCount()}}</td>
Can anyone help explain this to me? Ideally I would like to loop over the contents in my module but as the collection is async, when the loop starts the length of the collection is 0, hence why I made the call in the view.
THANKS!
Every time angular enters a change detection cycle, it evaluates loaderExhaustion.displayLoaderCount(), to know if the result of this expression has changed, and update the DOM if it has. This function changes the state of the controller (since it increments this.loaderCount), which thus triggers an additional change detection loop, which reevaluates the expression, which changes the state of the controller, etc. etc.
You MAY NOT change the state in an expression like that. For a given state, angular should be able to call this function twice, and get the same result twice. Expressions like these must NOT have side effects.
I can't understand what you want to achieve by doing so, so it's hard to tell what you should do instead.
I'm really happy with the "new" $onChanges method you can implement in a component's controller. However it only seems to be triggered when the bound variable is overwritten from outside my component, not (for instance) when an item is added to an existing array
It this intended behaviour or a bug? Is there another way of listening to updates to my input bindings, besides doing a $scope.$watch on it?
I'm using Angular 1.5.3
First TL;DR
For an array that is bounded via one-way binding, a watch expression is added that does not check for object equality but uses reference checking. This means that adding an element to the array will never fire the '$onChanges' method, since the watcher will never be 'dirty'.
I've created a plnkr that demonstrates this:
http://plnkr.co/edit/25pdLE?p=preview
Click the 'add vegetable in outer' and 'change array reference in outer' and look at the 'Number of $onChanges invocation'. It will only change with the latter button.
Complete explanation
To fully grasp what is going on, we should check the angular code base. When a '<' binding is found, the following code is used to set up a watch expression.
case '<':
if (!hasOwnProperty.call(attrs, attrName)) {
if (optional) break;
attrs[attrName] = void 0;
}
if (optional && !attrs[attrName]) break;
parentGet = $parse(attrs[attrName]);
destination[scopeName] = parentGet(scope);
// IMPORTANT PART //
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
var oldValue = destination[scopeName];
recordChanges(scopeName, newParentValue, oldValue);
destination[scopeName] = newParentValue;
}, parentGet.literal);
// ------------- //
removeWatchCollection.push(removeWatch);
break;
The important part here is how the 'scope.$watch' expression is set up. The only parameters passed are the parsed expression and the listener function. The listener function is fired once the '$watch' is found dirty in the digest cycle. If it is fired, the listener will execute the 'recordChanges' method. This records an '$onChanges' callback task that will be executed in the '$postDigest' phase and notify all components that are listening for the '$onChanges' lifecycle hook to tell them if the value has changed.
What's important to keep in mind here, if the '$watcher' is never dirty, the '$onChanges' callback is not triggered. But even more importantly, by the way the '$watch' expression is created, it will NEVER be dirty, UNLESS the reference changes. If you wanted to check for equality between objects instead of reference, you should pass an extra third parameter that asks for this:
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression)
As this is not the case here with the way the one way binding is set up, it will ALWAYS check for reference.
This means, if you add an element to an array, the reference is not changed. Meaning the '$watcher' will never be dirty, meaning the '$onChanges' method will not be called for changes to the array.
To demonstrate this, I've created a plnkr:
http://plnkr.co/edit/25pdLE?p=preview
It contains two components, outer and inner.
Outer has primitive string value that can be changed through an input box and an array that can be extended by adding an element or have its reference changed.
Inner has two one-way bounded variables, the value and the array. It listens for all changes.
this.$onChanges = setType;
function setType() {
console.log("called");
vm.callCounter++;
}
If you type into the input field, the '$onChanges' callback is fired every time. This is logical and expected, since a string is primitive so it cannot be compared by reference, meaning the '$watcher' will be dirty, and the '$onChanges' lifecycle hook fired.
If you click the 'Add vegetable in outer', it will execute the following code:
this.changeValueArray = function() {
vm.valueArray.push("tomato");
};
Here we just add a value to the existing bounded array. We're working by reference here, so the '$watcher' is not fired and there is no callback. You will not see the counter increment or the 'called' statement in your console.
Note: If you click the 'Add something to the array' inside the inner component, the array in outer component also changes. This is logical, since we are updating the exact same array by reference. So even though it is a one-way binding, the array can be updated from inside the inner component.
If you change the reference in the outer component by clicking 'Change array reference in outer', the '$onChanges' callback is fired as expected.
As to answer your question: Is this intended behaviour or a bug? I guess this is intended behaviour. Otherwise they would have given you the option to define your '<' binding in a way that it would check for object equality. You can always create an issue on github and just ask the question if you'd like.
I just want to understand why in the following jsFiddle 'here is a lo' is printed three times.
http://jsfiddle.net/wg385a1h/5/
$scope.getLog = function () {
console.log('here is a log');
}
Can someone explain me why ? What should I change to have only one log "here is a log" (that's what I would like this fiddle do). Thanks a lot.
Angular uses digest cycles/iterations to determine when state has changed and needs to update the UI. If it finds any change on one of it's cycles, it keeps rerunning cycles until the data stabilizes itself. If it's done 10 cycles and the data is still changing, you'll see a rather know message: "angularjs 10 iterations reached. aborting".
Therefor, The fact that you are seeing the message displayed 3 times is because you have a simple interface. In fact, you can get up to many more such messages in the log, due to the fact that your directive uses {{getLog()}}. Angular keeps evaluating the expression to see if it changed.
To avoid such problems, under normal circumstances, you should store the value returned by the function you want called only once in the $scope object inside the controller and use that variable (not the function call) in the UI.
So in the controller you'd have $scope.log = getLog() [assuming it returns something, and not just writing to the console] and in the directive use the template {{log}}. This way, you'll get the value only once, per controller instance.
Hope I was clear enough.
I'm trying here to get enough info to go fix this problem, just wanting some help understanding what is going on inside angular.
ng-grid has issues, lots of them, but I've found a "fix" to this one that I don't understand.
I have a grid with enough rows that it fills the visible area. If I click on the different rows, the afterSelectionChange method is called. If after clicking in the grid I move the focus with the arrow keys, it only calls that callback if the grid scrolls.
So I put in a $timeout to print out the selected row every half second to see if it was changing the selected row and just not calling the callback, and THAT fixed the problem. Now every time I move the cursor with the keyboard, the callback fires, even though the only thing happening in the callback is $log.debug().
Is this because $timeout is causing something to happen within the framework like a $apply or a $digest?
If that's the case, why isn't the keyboard causing that to happen?
Edit: Options for #tasseKATT
$scope.callGridOptions = {
data: 'callRecords',
multiSelect: false,
sortInfo: {fields:['startOn'], directions:['asc']},
columnDefs: [ ...
],
afterSelectionChange: $scope.onCallChange,
selectedItems: $scope.selectedCalls
};
In the end, I could reduce the timeout code to this:
function ngGridFixer() {
// Presence of this timer causes the ngGrid to correctly react to up/down arrow and call the
// afterSelectionChange callback like it is supposed to.
$timeout(ngGridFixer, 500);
}
ngGridFixer();
I put this in the rootscope because the problem happens on all the pages of the app.
$log is part of the Angular framework, anything processed by it is might execute watches laid down earlier. In other words by calling $log.debug() to print out the structure, you might be basically running scope.$digest every half second, which cause the callback(s) to fire. If you take out everything inside the $timeout function, or use console.log instead, the callback(s) probably won't fire
A way to do this semi-properly would be to use something like ngKeydown.
EDIT:
$timeout execute the function in scope.$apply by default. https://docs.angularjs.org/api/ng/service/$timeout (invokeApply). I was not aware of this. So essentially your code is calling scope.$apply every half second.
I'm creating a directive which position children div justify and with a margin = 0 changing the height of the children div. (like http://brunjo.github.io/rowGrid.js/)
All is working well. Except when the children are changing.
I have added a watch for the number of children so that if I add a child, it is still working.
But how to watch at the size of each child to update the layout when they are changing? Put a watch on each one of them?
Here is a plunker: http://plnkr.co/edit/OYZ1snIjmLS4HRB6J5vy?p=preview
I'm new to angular, it may be an easy answer.
Thanks in advance.
NOTICE:
Watch expressions (e.g. the first argument of a $watch() function call) are executed several times per $digest cycle. Therefore, they should be kept as quick as possible. If your element has many children, it might become expensive to calculate all their widths.
If that is the case, it might be better to capture the events that are associated with the resizing of the children and check the sizes only when there is indeed the possibility that something got resized.
That said, you could do something like this:
$scope.$watchCollection(function() {
return [].map.call(el[0].children, function (child) {
return child.offsetWidth;
});
}, function(newValue, oldValue) { // Note: newValue comes before oldValue
if (oldValue !== newValue) {
console.log('eoke');
this.layout(el, 'blue');
}
}.bind(this));
See, also, this short demo.