Angular.js watching the result of a function call - angularjs

Is there any ostensible issue with the following snippet:
<ul id="entry-name" class="breadcrumb">
<li ng-repeat="dir in pathElements()" class="active">
<span ng-show="!$last">
{{ dir.name }} <span ng-show="!$first" class="dividier">/</span>
</span>
</li>
<li class="active">{{ pathElements()[pathElements().length - 1].name }}</li>
</ul>
with this js:
$scope.pathElements = function() {
var retval = [];
var arr = $scope.currentPath().split('/');
if (arr[0] == '') {
arr[0] = '/';
}
var url = "/";
retval.push({name: arr[0], url: url});
for (var i = 1; i < arr.length; ++i) {
if (arr[i] != '') {
url += arr[i] + '/';
retval.push({name: arr[i], url: url});
}
}
return retval;
};
This seems to be causing a "Error: 10 $digest() iterations reached. Aborting!" error, but I'm not sure why. Is it because pathElements() is returning a new array each time? Is there a way to get around this?

Yes, this happens because you're returning a new array every time, and the $digest cycle loops infinitely (but Angular ceases it). You should declare it outside the function.
$scope.pathArray = [];
$scope.pathElements = function() {
// retval becomes $scope.pathArray
if (weNeedToRecalcAllPathVariables) {
$scope.pathArray.length = 0;
// ...
}
// ...
}
We use $scope.pathArray.length = 0 instead of creating a new one, to avoid it firing continuously.
You should also consider what #Flek suggests. Call this function only once, in the time you need it to recalculate. And all you binds should be directly over $scope.pathArray.
If you do need a function to test its clenaning state before using it, then at least I suggest you to create two separate functions, just to keep each function with it own attribution.

For some nice reference on how to implement breadcrumbs in Angular check out the angular-app project (breadcrumbs service, how to use it).
Here's a demo plunker.

Related

Infinite Digest Loop in AngularJS filter

I have written this custom filter for AngularJS, but when it runs, I get the infinite digest loop error. Why does this occur and how can I correct this?
angular.module("app", []).
filter('department', function(filterFilter) {
return function(items, args) {
var productMatches;
var output = [];
var count = 0;
if (args.selectedDepartment.Id !== undefined && args.option) {
for (let i = 0; i < items.length; i++) {
productMatches = items[i].products.filter(function(el) {
return el.Order__r.Department__r.Id === args.selectedDepartment.Id;
});
if (productMatches.length !== 0) {
output[count] = {};
output[count].products = productMatches;
output[count].firstProduct = items[i].firstProduct;
count++;
}
}
}
return output;
};
}).
This is the relevant HTML:
<tr class='destination' ng-repeat-start='pickupAccount in pickupAccounts | department : {"selectedDepartment": selectedDepartment, "option": displayExclusive }'>
<!-- td here -->
</tr>
displayExclusive is boolean.
I have written this custom filter for AngularJS, but when it runs, I get the infinite digest loop error.
Keep in mind that filter should return array of the same object structure. When we activate filter, it fires digest cycle that will run over our filter again. If something changed in output list - fires new digest cycle and so on. after 10 attempts it will throw us Infinite Digest Loop Exception
Testing
This empty filter will works (100%). Actually we do nothing here but return the same object that filter receives.
filter('department', function(filterFilter) {
return function(items, args) {
var output = items;
return output;
};
})
Now the main idea is: write some condition to push to output objects from input list a.e. items based on some if statement, a.e.
var output = [];
if (args.selectedDepartment.Id !== undefined && args.option) {
angular.forEach(items, function(item) {
if(<SOME CONDITION>) {
output.push(item);
}
});
}
By this way it will work too.
our case:
we have this logic:
productMatches = items[i].products.filter(function(el) {
return el.Order__r.Department__r.Id === args.selectedDepartment.Id;
});
if (productMatches.length !== 0) {
output[count] = {};
output[count].products = productMatches;
output[count].firstProduct = items[i].firstProduct;
count++;
}
Here we completely modified object that has been stored in output.
So next digest cycle our items will change again and again.
Conclusion
The main purpose of filter is to filter list and not modify list object content.
Above mentioned logic you wrote is related to data manipulation and not filter. The department filter returns the same length of items.
To achieve your goal, you can use lodash map or underscorejs map for example.
This happens when you manipulate the returned array in a way that it does not match the original array. See for example:
.filter("department", function() {
return function(items, args) {
var output = [];
for (var i = 0; i < items.length; i++) {
output[i] = {};
output[i] = items[i]; // if you don't do this, the next filter will fail
output[i].product = items[i];
}
return output;
}
}
You can see it happening in the following simplified jsfiddle: https://jsfiddle.net/u873kevp/1/
If the returned array does have the same 'structure' as the input array, it will cause these errors.
It should work in your case by just assigning the original item to the returned item:
if (productMatches.length !== 0) {
output[count] = items[i]; // do this
output[count].products = productMatches;
output[count].firstProduct = items[i].firstProduct;
count++;
}
output[count] = {};
Above line is the main problem. You create a new instance, and ng-repeat will detect that the model is constantly changed indefinitely. (while you think that nothing is changed from the UI perspective)
To avoid the issue, basically you need to ensure that each element in the model remains the 'same', i.e.
firstCallOutput[0] == secondCallOutput[0]
&& firstCallOutput[1] == secondCallOutput[1]
&& firstCallOutput[2] == secondCallOutput[2]
...
This equality should be maintained as long as you don't change the model, thus ng-repeat will not 'wrongly' think that the model has been changed.
Please note that two new instances is not equal, i.e. {} != {}

How to get selected value from ng-option inside an ng-repeat

In my controller I loop through the questions in a questionnaire:
for (var j = 0; j < s.Questions.length; j++) {
var q = s.Questions[j];
var aq = datacontext.getAnsweredQuestion(propertySurvey.ID, q.ID);
if (q.QuestionType === 'SingleAnswer') {
q.dropdown = true;
q.selectedOption = aq.ActualAnswers.length > 0 ?
aq.ActualAnswers[0].AnswerID : null;
q.optionChanged = function () {
var debug = q.selectedOption; //ERROR - this is undefined
aq.toggleAnswer(q.selectedOption);
}
}
//etc
}
In my view I also loop through the questions:
<div class="card" ng-repeat="q in s.Questions">
<div class="item item-divider">{{q.Text}}</div>
<div class="item" ng-if="q.dropdown">
<select ng-model="q.selectedOption"
ng-options="va.AnswerID as va.Text for va in q.ValidAnswers"
ng-change="q.optionChanged()">
<option>--Select--</option>
</select>
</div>
</div>
When viewed in the browser, the correct selectedOption is displayed, and the q.optionChanged() function is called. But inside that function, q.selectedOption is undefined
EDIT
This function appears to work. But I can't explain why!
q.optionChanged = function () {
var debug = q.selectedOption;
var answerid = this.selectedOption;
aq.toggleAnswer(answerid);
}
The reason is because you are creating a function with closure inside a loop, which causes this unintuitive behavior. Read: Creating closures in loops: A common mistake
What ends up happening is that the inner function captures the last q in the loop, for which (for some reason, perhaps not shown in code here), q.optionChanged is undefined.
In contrast, this is bound to the right q as supplied by the expression in ng-change="q.optionChanged()".
To fix, use a function generator to create a function for each loop iteration, like so:
for (var j = 0; j < s.Questions.length; j++) {
var q = s.Questions[j];
var aq = datacontext.getAnsweredQuestion(...);
// ...
q.optionChanged = makeOptionChangeFn(q, aq)
// ...
}
function makeOptionChangeFn(q, aq){
return function(){
aq.toggleAnswer(q.selectedOption);
}
}
Using this would have worked if you only needed to reference the right q, but you also need aq, so a function generator is necessary.
In order to auto select the option from the controller, the ng-model must hold a reference to the array item. Assign q.selectedOption the whole item from the aq.ActualAnswers. You will need to refactor your comprehension expression.
// in the controller
q.selectedOption = aq.ActualAnswers.length > 0 ?
aq.ActualAnswers[0] : null;
// in the view
ng-options="va as va.Text for va in q.ValidAnswers"
This has caused me pain in the past. Make sure you read the documentation carefully:
https://docs.angularjs.org/api/ng/directive/ngOptions

Converting string to array using filter and using it in ng-repeat

I have a string which is in "1200:2,1300:3,1400:2" format. I need this to be printed like
<p>1200</p><p>2</p>
<p>1300</p><p>3</p>
<p>1400</p><p>2</p>
I tried using filter,
return function (input) {
//Validate the input
if (!input) {
return '';
}
var hoaArray = [];
var inputArray = input.split(',');
for (var i = 0; i < inputArray.length; i++) {
var adminTimeArray = inputArray[i].split(':');
hoaArray.push({ 'adminTime': adminTimeArray[0], 'dose': adminTimeArray[1]?adminTimeArray[1]:'' });
}
return hoaArray;
};
and inside html like
<p ng-repeat="timing in timing_list | formatter">{{timing.}}</p>{{timing .adminTime}}</div>
I am getting the following error,
Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [[{"msg":"fn: regularInterceptedExpression","newVal":36,"oldVal":34}],[{"msg":"fn: regularInterceptedExpression","newVal":38,"oldVal":36}],[{"msg":"fn: regularInterceptedExpression","newVal":40,"oldVal":38}],[{"msg":"fn: regularInterceptedExpression","newVal":42,"oldVal":40}],[{"msg":"fn: regularInterceptedExpression","newVal":44,"oldVal":42}]]
Could anyone please help me understand what am I doing wrong?
Regards,
Raaj
In the IndexController.js file:
var inputString = "1200:2,1330:3,1400:4,1500:3";
var formatInputString = function (input) {
//Validate the input
if (!input) {
return '';
}
var hoaArray = [];
var inputArray = input.split(',');
for (var i = 0; i < inputArray.length; i++) {
var adminTimeArray = inputArray[i].split(':');
hoaArray.push({ 'adminTime': adminTimeArray[0], 'dose': adminTimeArray[1] ? adminTimeArray[1] : '' });
}
return hoaArray;
};
$scope.inputString = inputString;
$scope.formattedString = formatInputString(inputString);
In the HTML file:
<div ng-repeat="timing in formattedString" >
{{timing.adminTime}}
{{timing.dose}}
</div>
The issue here - possibly a limitation or a bug in Angular - is that your filter creates new array objects every time it runs. ng-repeat uses under the covers $scope.$watchCollection to watch for the expression "timing_list | formatter" - this watcher always trips up because, in trying to detect a change in a values in the collection, it compares objects with a simple "!==" - and the objects are always new and different objects.
In short, this is another way to reproduce:
$scope.items = [1, 2, 3];
$scope.$watchCollection("items | foo", function(){
});
where foo is a filter that operates on each element in the array creating a new object:
.filter("foo", function(){
return function(inputArray){
return inputArray.map(function(item){
return {a: item};
});
};
});
So, to answer your question - you cannot with Angular v1.3.15 use a filter that returns an array with objects (without some funky object caching) with $watchCollection, and by extension, with ng-repeat.
The best way is to create the array first (with ng-init or in the controller), and then use it in the ng-repeat.

angular function in ng-show with a promise getting error

I'm using restangular for my app. Say I have this in my view:
<div ng-repeat="resource in resources">
<i ng-show="isCheckedLabel(resource)" class="fa fa-checked"> Checked </i>
</div>
and in my controller I have my api call to the service
$scope.getResources = function(){
$scope.getResourcePromise = ResourceService.getResourceById($stateParams.id).then(function(response){
$scope.resources = response;
});
};
and here is my check function which will return true or false
$scope.isCheckedLabel = function(resource){
$scope.getResourcePromise.then(function(){
for(var group in resource.groups){
for(i = 0; i < resource.groups.length; i++){
if (resource.groups[group].isChecked === true){
return true;
} else {
return false;
}
}
}
});
};
What i'm trying to do: loop through each group and if 1 or more is 'checked' I want my label in the front end to show checked.
My function is returning true when one of them is checked and false when none are checked but it's not displaying the i element in the view because I'm getting this error in the console:
Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: []
and it just keeps firing over and over. What's going on here?
First and foremost thing is a value returned inside async then callback will not be the return value of isCheckedLabel.
Looking at your code, you do not need $scope.getResourcePromise.then in isCheckedLabel as the you already have the resource object passed as parameter. Also for loop code seems to wrong. Change it to
$scope.isCheckedLabel = function(resource){
for(var group in resource.groups){
if (group.isChecked === true){
return true;
} else {
return false;
}
}
};
This error occurs only when you trying to update the dom when the $digest is running. In your case you can try to set the checked variable as another property in the $scope variable resources.
$scope.getResources = function () {
$scope.getResourcePromise = ResourceService.getResourceById($stateParams.id).then(function (response) {
$scope.resources = response;
$scope.resources.forEach(function (resource) {
$scope.getResourcePromise.then(function () {
for (var group in resource.groups) {
for (i = 0; i < resource.groups.length; i++) {
if (resource.groups[group].isChecked === true) {
//Here setting another property on the resource object
resource.checked = true;
} else {
resource.checked = false;
}
}
}
});
});
});
};
Now in HTML you can do something like this simply:
<div ng-repeat="resource in resources">
<i ng-show="resource.checked" class="fa fa-checked"> Checked </i>
</div>

Angular filter returning an array of objects causing infinite $digest loop

I have a custom filter which returns an array of matches to search field input and it works, but only after causing an infinite $digest loop. This also apparently only began happening after upgrading from Angular 1.0.6. This is the filter code:
angular.module("Directory.searches.filters", [])
.filter('highlightMatches', function() {
var ary = [];
return function (obj, matcher) {
if (matcher && matcher.length) {
var regex = new RegExp("(\\w*" + matcher + "\\w*)", 'ig');
ary.length = 0;
angular.forEach(obj, function (object) {
if (object.text.match(regex)) {
ary.push(angular.copy(object));
ary[ary.length-1].text = object.text.replace(regex, "<em>$1</em>");
}
});
return ary;
} else {
return obj;
}
}
});
I've seen elsewhere that this could be caused by having the filter inside of an ng-show, or that it's because the array being returned is interpreted as a new array every time it's checked, but I'm not sure how I could fix either problem. You can see a production example of this issue at https://www.popuparchive.com/collections/514/items/4859 and the open source project is available at https://github.com/PRX/pop-up-archive. Thank you!
This is happening because of angular.copy(object). Each time the digest cycle runs, the filter returns an array of new objects that angular has never seen before, so the the digest loop goes on forever.
One solution is return an array containing the original items that match the filter, with a highlightedText property added to each item...
angular.module("Directory.searches.filters", [])
.filter('highlightMatches', function() {
return function (items, matcher) {
if (matcher && matcher.length) {
var filteredItems = [];
var regex = new RegExp("(\\w*" + matcher + "\\w*)", 'ig');
angular.forEach(items, function (item) {
if (item.text.match(regex)) {
item.highlightedText = item.text.replace(regex, "<em>$1</em>");
filteredItems.push(item);
}
});
return filteredItems;
} else {
angular.forEach(items, function (item) {
item.highlightedText = item.text;
});
return items;
}
}
});
You can bind to the highlightedText property, something like...
<div>
Results
<ul>
<li ng-repeat="item in items | highlightMatches : matcher" ng-bind-html="item.highlightedText"></li>
</ul>
</div>

Resources