I have an expensive function call that gets called once per loop, but I would like to set the return value of that to be a temporary variable that can be used directly within the loop.
This works by setting {{ val=fn(...) }}, but it also prints to the screen. Is it possible to do this without printing the value?
I've tried using one set of curly braces {} but it doesn't work.
Any ideas on how to do this?
Have you looked into "filters" or possibly doing the work in your JS code instead of the view?
Instead of a temporary variable, call a method when you need the value, and in this method, use a cached version of the result, or call your expensive method if the cached version is null/undefined. It will only call your expensive function once per iteration, and it won't call it if it's not needed. It won't print it unless you want to. Because each iteration of a ng-repeat spawns a new scope, each iteration will start with an empty cache.
e.g.:
$scope.cache = null
$scope.getValue = function() {
if (!this.cache) { // use typeof if 0 or false or empty string is valid!
this.cache = myExpensiveFunc()
}
return this.cache
}
Not 100% sure $scope.cache = null will be initialized for each scope of the iteration, you may need to check if 'this' hasOwnProperty 'cache' in getValue. So in your view, you only reference getValue() when you need to.
HTH!
edit: cache on scope, not in controller.
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 am updating a long array, that has complex calculation and can take a second or two.
While the array is changing ( i.e. in different algorithm steps ), angular keep updating the ui for each intermediate steps.
How do I ask angular to stop looking for change for this variable for a second or two and start watching it again after I complete ?
How do I ask angular to stop looking for change for this variable for
a second or two and start watching it again after I complete ?
Assuming you are referring to the 2 way data binding between a controller scope variable and the view, one solution would be to do the calculations off the scope in a controller variable and then assign that variable to the scope afterward. This way during the "second or two" that the calculations are taking place, the digest cycle wouldn't be triggered on variable:
// making copy so we are not passing by reference (array or object)
var localCopyOfVar = angular.copy( $scope.complexArray );
// do complex stuff with localCopyOfVar
// ...
// assign finished work back onto scope variable
$scope.complexArray = localCopyofVar;
If you are using {{ }} in the HTML markup you cannot stop the watches. If you created the $watch yourself you can stop it and restart is later like this:
var watchFunction = ... blahBlah ...
var stopWatch = scope.$watch('foo', watchFunction)
stopWatch()
You can minimize / eliminate the screen flicker by doing all of the updates within a single digest by not using $timeouts.
In angularJS, when trying to assign a scope variable from the value of another scope variable, the value of the derived variable is empty. In the example, I'd like to have a scope reference for the car, and also for a specific car part (which may change later in the application).
Example:
$scope.car = Car.get(); //async http service that returns a JSON car object
$scope.selectedCarPart = $scope.car.brakes;
HTML:
<div>{{car.engine}} - {{selectedCarPart}}</div>
Output:
v8 -
Why is selectedCarPart empty?
I assume that you get call is async, so when you assign the selectedCarPart, your $scope.car is currently null and doesn't have yet some brakes.
You have to wait the end of your get call and assign the value of the resulting JSON car object in the success callback of your http service.
Accepted answer from Apercu is correct. You can also use more general solution which is using $watch. In that case you write this:
$scope.$watch('car',function(newValue) {
$scope.selectedCarPart = newValue['brakes'];
}, true);
More information about $watch can be found here: https://docs.angularjs.org/api/ng/type/$rootScope.Scope
Even if that would work, I think there's a better way.
<div>{{car.engine}} - {{car[selectedPartKey]}}</div>
That way, you can just change the value of selectedPartKey to 'brakes'. Your way, if car's values change, it won't be reflected...
Trying to understand the nature of Angular filters. So I have this:
<p>RandomCase: {{ aString | randomCase }}</p>
and this:
.filter 'randomCase', () ->
(input) ->
input.replace /./g, (c) ->
if Math.random() > 0.5 then c.toUpperCase() else c
Coffeescript makes for a cleaner code here, JS version is found in JSFiddle along with the complete example:
http://jsfiddle.net/nmakarov/5LdKV/
The point is to decorate a string by having random letters capitalized.
it works, but throws "10 $digest() iterations reached. Aborting!" most of the time. I figured that for some reason Angular would re-run the filter at least twice to see that outputs are the same. And if not, will run it again until the last two matches. Indeed, since the filter's code produces a random string, it is quite unlikely it will repeat itself twice in a row.
Now to the question: is it possible to tell Angular not to re-run this filter more than once? I do not need to observe the value of this filtered output in the code, so no need for Angular to watch the changes - even if a hardcoded "string" be used in place of an aString variable, the code behaves the same - 10 iterations reached...
And I know that I can put the randomizing logic in a controller and bind the result to a $scope.aString and it would just work - I'm trying to understand the Angular way of filters.
Cheers.
There is no way to use an non-idempotent filter in a watched expression without a hack. This the simplest one that I can think of, which will make the filter idempotent...
Use a memoizing function to ensure that subsequent calls to the filter passing the same arguments return the same result.
Example using Underscore:
myApp.filter('randomCase', function() {
return _.memoize(function (input) {
console.log("random");
return input.replace(/./g, function(c) {
if (Math.random() > 0.5) {
return c.toUpperCase();
} else {
return c;
}
});
});
});
Updated Fiddle
The filter itself will only run when an expression with the | operator (e.g. someVar | someFilter) is evaluated. It is Anuglar's dirty checking that causes the expression to be evaluated multiple times.
In short, Angular runs the expression aString | randomCase over and over until it doesn't change. At that point it knows what to put into the DOM. To prevent infinite looping when that value doesn't stop changing, it throws the infinite $digest error.
For this reason filters always run at least twice. Once to get the initial value, and then a second time to compare it against that first value.
By putting the randomizing logic in the controller, you would then have something like {{randomizedString}} in your HTML. The value of randomizedString wouldn't change from the first time it was evaluated, and thus would accomplish your end goal without hitting the infinite $digest error.
I receive data from my back end server structured like this:
{
name : "Mc Feast",
owner : "Mc Donalds"
},
{
name : "Royale with cheese",
owner : "Mc Donalds"
},
{
name : "Whopper",
owner : "Burger King"
}
For my view I would like to "invert" the list. I.e. I want to list each owner, and for that owner list all hamburgers. I can achieve this by using the underscorejs function groupBy in a filter which I then use in with the ng-repeat directive:
JS:
app.filter("ownerGrouping", function() {
return function(collection) {
return _.groupBy(collection, function(item) {
return item.owner;
});
}
});
HTML:
<li ng-repeat="(owner, hamburgerList) in hamburgers | ownerGrouping">
{{owner}}:
<ul>
<li ng-repeat="burger in hamburgerList | orderBy : 'name'">{{burger.name}}</li>
</ul>
</li>
This works as expected but I get an enormous error stack trace when the list is rendered with the error message "10 $digest iterations reached". I have a hard time seeing how my code creates an infinite loop which is implied by this message. Does any one know why?
Here is a link to a plunk with the code: http://plnkr.co/edit/8kbVuWhOMlMojp0E5Qbs?p=preview
This happens because _.groupBy returns a collection of new objects every time it runs. Angular's ngRepeat doesn't realize that those objects are equal because ngRepeat tracks them by identity. New object leads to new identity. This makes Angular think that something has changed since the last check, which means that Angular should run another check (aka digest). The next digest ends up getting yet another new set of objects, and so another digest is triggered. The repeats until Angular gives up.
One easy way to get rid of the error is to make sure your filter returns the same collection of objects every time (unless of course it has changed). You can do this very easily with underscore by using _.memoize. Just wrap the filter function in memoize:
app.filter("ownerGrouping", function() {
return _.memoize(function(collection, field) {
return _.groupBy(collection, function(item) {
return item.owner;
});
}, function resolver(collection, field) {
return collection.length + field;
})
});
A resolver function is required if you plan to use different field values for your filters. In the example above, the length of the array is used. A better be to reduce the collection to a unique md5 hash string.
See plunker fork here. Memoize will remember the result of a specific input and return the same object if the input is the same as before. If the values change frequently though then you should check if _.memoize discards old results to avoid a memory leak over time.
Investigating a bit further I see that ngRepeat supports an extended syntax ... track by EXPRESSION, which might be helpful somehow by allowing you to tell Angular to look at the owner of the restaurants instead of the identity of the objects. This would be an alternative to the memoization trick above, though I couldn't manage to test it in the plunker (possibly old version of Angular from before track by was implemented?).
Okay, I think I figured it out. Start by taking a look at the source code for ngRepeat. Notice line 199: This is where we set up watches on the array/object we are repeating over, so that if it or its elements change a digest cycle will be triggered:
$scope.$watchCollection(rhs, function ngRepeatAction(collection){
Now we need to find the definition of $watchCollection, which begins on line 360 of rootScope.js. This function is passed in our array or object expression, which in our case is hamburgers | ownerGrouping. On line 365 that string expression is turned into a function using the $parse service, a function which will be invoked later, and every time this watcher runs:
var objGetter = $parse(obj);
That new function, which will evaluate our filter and get the resulting array, is invoked just a few lines down:
newValue = objGetter(self);
So newValue holds the result of our filtered data, after groupBy has been applied.
Next scroll down to line 408 and take a look at this code:
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
if (oldValue[i] !== newValue[i]) {
changeDetected++;
oldValue[i] = newValue[i];
}
}
The first time running, oldValue is just an empty array (set up above as "internalArray"), so a change will be detected. However, each of its elements will be set to the corresponding element of newValue, so that we expect the next time it runs everything should match and no change will be detected. So when everything is working normally this code will be run twice. Once for the setup, which detects a change from the initial null state, and then once again, because the detected change forces a new digest cycle to run. In the normal case no changes will be detected during this 2nd run, because at that point (oldValue[i] !== newValue[i]) will be false for all i. This is why you were seeing 2 console.log outputs in your working example.
But in your failing case, your filter code is generating a new array with new elments every time it's run. While this new array's elments have the same value as the old array's elements (it's a perfect copy), they are not the same actual elements. That is, they refer to different objects in memory that simply happen to have the same properties and values. Hence in your case oldValue[i] !== newValue[i] will always be true, for the same reason that, eg, {x: 1} !== {x: 1} is always true. And a change will always be detected.
So the essential problem is that your filter is creating a new copy of the array every time it's run, consisting of new elements that are copies of the original array's elments. So the watcher setup by ngRepeat just gets stuck in what is essentially an infinite recursive loop, always detecting a change and triggering a new digest cycle.
Here's a simpler version of your code that recreates the same problem: http://plnkr.co/edit/KiU4v4V0iXmdOKesgy7t?p=preview
The problem vanishes if the filter stops creating a new array every time it's run.
New to AngularJS 1.2 is a "track-by" option for the ng-repeat directive. You can use it to help Angular recognize that different object instances should really be considered the same object.
ng-repeat="student in students track by student.id"
This will help unconfuse Angular in cases like yours where you're using Underscore to do heavyweight slicing and dicing, producing new objects instead of merely filtering them.
Thanks for the memoize solution, it works fine.
However, _.memoize uses the first passed parameter as the default key for its cache. This could not be handy, especially if the first parameter will always be the same reference. Hopefully, this behavior is configurable via the resolver parameter.
In the example below, the first parameter will always be the same array, and the second one a string representing on which field it should be grouped by:
return _.memoize(function(collection, field) {
return _.groupBy(collection, field);
}, function resolver(collection, field) {
return collection.length + field;
});
Pardon the brevity, but try ng-init="thing = (array | fn:arg)" and use thing in your ng-repeat. Works for me but this is a broad issue.
I am not sure why this error is coming but, logically the filter function gets called for each element for the array.
In your case the filter function that you have created returns a function which should only be called when the array is updated, not for each element of the array. The result returned by the function can then be bounded to html.
I have forked the plunker and have created my own implementation of it here http://plnkr.co/edit/KTlTfFyVUhWVCtX6igsn
It does not use any filter. The basic idea is to call the groupBy at the start and whenever an element is added
$scope.ownerHamburgers=_.groupBy(hamburgers, function(item) {
return item.owner;
});
$scope.addBurger = function() {
hamburgers.push({
name : "Mc Fish",
owner :"Mc Donalds"
});
$scope.ownerHamburgers=_.groupBy(hamburgers, function(item) {
return item.owner;
});
}
For what it's worth, to add one more example and solution, I had a simple filter like this:
.filter('paragraphs', function () {
return function (text) {
return text.split(/\n\n/g);
}
})
with:
<p ng-repeat="p in (description | paragraphs)">{{ p }}</p>
which caused the described infinite recursion in $digest. Was easily fixed with:
<p ng-repeat="(i, p) in (description | paragraphs) track by i">{{ p }}</p>
This is also necessary since ngRepeat paradoxically doesn't like repeaters, i.e. "foo\n\nfoo" would cause an error because of two identical paragraphs. This solution may not be appropriate if the contents of the paragraphs are actually changing and it's important that they keep getting digested, but in my case this isn't an issue.