Why is a function in ng-repeat called several times? - javascript

I want to supply a ng-repeat element by a controller function as follows:
<div ng-repeat="picture in allPictures(data.pictures)"></div>
$scope.allPictures = function(pictures) {
alert("function called");
//return... extract all pictures and return as array
}
Result: my allPictures function is called several times, even though I'd expect it to be called only once and then iterate over the results.
Why? And moreover: how can I prevent this and really call the method only once for picture supply?

I would really avoid calling a function insides a ngRepeat attribute, since it will give errors and unexpected behaviour.
But to be honest I dont think that you would need to call a function inside a ngRepeat. I would suggest to do the following:
<div ng-repeat="picture in allPictures"></div>
$scope.getPictures = function(pictures) {
alert("function called");
//return... extract all pictures and return as array
};
$scope.allPictures = $scope.getPictures();
This way the $scope.getPictures function will get called and the $scope.allPictures will be created. ngRepeat can call that collection instead of a function.
See also my Fiddle: https://jsfiddle.net/ABr/w6kc8qyh/1/

a little bit about the digest cycle:
we need to check every time something in the application changes - what was effected by that change, and re-evaluate all the places that might depend on that change,
so that is why the function in the ng-repeat was called multiple times - it had to check wheter the repeated list is the same after some changes happend in the application
read more about the digest cycle and two-way data binding:
http://blog.bguiz.com/post/60397801810/digest-cycles-in-single-page-apps/

Related

How to call two methods #click on a button but execute one method after the other

I want to be able to call two different methods on a single button click, but I want the methods to run one after the other.
Indeed, I have my first method which creates multiple instances of a component and the second one is going to move these instances on my screen.
I didn't find a solution to create the instances and move them in a single method so I want to do it that way.
I tried by calling the two methods like this : #click="method1(); method2();" but the two methods are executed at the same time.
So these are the methods I am dealing with:
modifyLayer(){
console.log(this.selectedLayer)
this.modifyingLayer = this.selectedLayer
var donnéesCouche = this.couche.find(element => element.id == this.modifyingLayer)
this.emplacement=[]
this.emplacement = donnéesCouche.listePositions
},
moveLayer(){
for (var element in this.emplacement){
var toMove=document.getElementById(this.emplacement[element].id)
for (var element in this.emplacement){
toMove.top=this.emplacement[element].x
toMove.left=this.emplacement[element].y
}
}
},
this.emplacement corresponds to a list of objects like this: {id: ....; x: "...px"; y: "...px"} and in my HTML, I create instances of my component with a v-for each element in this.emplacement
Could you help me find a solution, either to call the two methods one after the other or to be able to create instances and move them within the same method?
Thank you for your help!
I want to be able to call two different methods on a single button click, but I want the methods to run one after the other
Well, the handler as #click="method1(); method2();" indeed runs the methods one after another (1st returns before 2nd starts)
I have my first method which creates multiple instances of a component and the second one is going to move these instances on my screen.
This is little bit unfortunate wording. Your fist method does not create any components directly. It just assigns some data into emplacement data member and this emplacement property is used in the template to render some components...
So your problem is not that those methods are not executed one after another (they are), problem is that Vue is using async update queue to update the DOM
So when the modifyLayer() finishes executing, DOM elements for the emplacement does not exist yet. The solution is to postpone the moveLayer() execution using the $nextTick() method
So instead of #click="method1(); method2();" handler, create a method:
#click="handleClick"
methods: {
handleClick() {
this.modifyLayer()
this.$nextTick(this.moveLayer)
}
}
Note: even tho I do not understand why you rendering 1st and moving later instead of rendering everything in one go at correct possition. Something like document.getElementById is not needed in Vue in most cases....
You can simply call the second method from the first one, and call the first method on button click. #click="modifyLayer()"
modifyLayer(){
console.log(this.selectedLayer)
this.modifyingLayer = this.selectedLayer
var donnéesCouche = this.couche.find(element => element.id == this.modifyingLayer)
this.emplacement=[]
this.emplacement = donnéesCouche.listePositions
this.moveLayer(); // calling the second method from the first
},
moveLayer(){
for (var element in this.emplacement){
var toMove=document.getElementById(this.emplacement[element].id)
for (var element in this.emplacement){
toMove.top=this.emplacement[element].x
toMove.left=this.emplacement[element].y
}
}
Can't you do something like that : #click="methods()"
And then declare :
async methods{
await this.methods1()
this.methods2()
}

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;

AngularJS Directive - re-run link function on scope parameter change

I have a directive that builds a set of nested <ul> elements representing a folder structure. I used the link function to create the new DOM elements and append them to the directive instance element:
function link(scope, iElement, iAttr) {
var rootElement = buildChildElement(scope.tree);
iElement.append(rootElement);
}
Elements within the <ul> tree are wired with jQueryUI's drag/drop interactions that call a function on the Controller housing the directive to update the scope parameter based on the drag & drop events.
I would like the <ul> tree to automatically update when there is a change to the scope parameter. I have tried a watch function within my link function:
scope.$watch('tree', function(newTree, oldTree) {
var newRoot = buildChildElement(newTree);
iElement.contents().remove();
iElement.append(newRoot);
}
This works to a certain extent, but the call to remove() fires off the $watch() method a second time which ends up reverting my Controller changes. If I comment out the remove(), I can see that a new <ul> tree is written that properly reflects the changes to the parameter made in the Controller.
The double firing $watch() makes me think I'm going about this wrong. Without it, my parameter is properly updating but my <ul> doesn't update (the dropped element stays where it was dropped).
What's the correct way to make sure your directive is refreshed on a change in one of the scope parameters?
Should I be using the compile function and building the <ul> tree based on the attributes array instead of using the link function?
Your approach is very jQuery-style. I think you'll find that you're working against Angular in this case. sh0ber is right with his/her question; you should post a demo or something, or at least some sample code so you can have an effective answer.
I think you want to make a recursive tree directive. Check out this SO answer for some interesting approaches to this. The main idea is that watch is unnecessary. Simply change the object and Angular will take care of the rest. The most efficient thing is to change the specific node objects directly rather than replacing the whole object, but that will work too.
scope.$watch('tree', function(newTree, oldTree) {
var newRoot = buildChildElement(newTree);
iElement.contents().remove();
iElement.append(newRoot);
},**true**)
I think you can have a try and reference the watch API for more information
Here is another artical
http://www.bennadel.com/blog/2566-scope-watch-vs-watchcollection-in-angularjs.htm

Categories

Resources