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

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>

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. {} != {}

AngularJs - Filter an object only by certain fields in a custom filter

I'm working on this codepen. The data comes from an array of objects, and I need to make a filter only by name and amount.
I have this code, but if you type a character in the search box, it only search by amount, and not by name too. In other words, if the you type 'warren' or '37.47' it has to return the same result, but doesn't works.
var filterFilter = $filter('filter');
$scope.filter = {
condition: ""
};
$scope.$watch('filter.condition',function(condition){
$scope.filteredlist = filterFilter($scope.expenses,{name:condition} && {amount:condition});
$scope.setPage();
});
You want to create a custom filter for your app.
directiveApp.filter("myFilter", function () {
return function (input, searchText) {
var filteredList = [];
angular.forEach(input, function (val) {
// Match exact name
if (val.name == searchText) {
filteredList.push(val);
}
// Match exact amount
else if (val.amount == searchText) {
filteredList.push(val);
}
});
input = filteredList;
return input;
};
});
You can write your logic in this filter and now use this filter to filter your list.
Update
You can just implement this filter to your custom filter pagination.
Here is the new version of your code. Codepen
List of updates on your code
Added new filter parameter to your ng-repeat attribute
ng-repeat="expense in filteredlist | pagination: pagination.currentPage : numPerPage : filter.condition"
...
Well, finally (based in the idea of Abhilash P A and reading the docs), I solved my question in this way:
var filterFilter = $filter('filter');
$scope.filter = {
condition: ""
};
$scope.$watch('filter.condition',function(condition){
$scope.filteredlist = filterFilter($scope.expenses,function(value, index, array){
if (value.name.toLowerCase().indexOf(condition.toLowerCase()) >= 0 ) {
return array;
}
else if (value.amount.indexOf(condition) >= 0 ) {
return array;
}
});
$scope.setPage();
});
The final codepen ! (awsome)

AngularJS, Add Rows

Morning,
We are trying to implement this add row Plunkr, it seems to work however our input data seems to repeat. Does anyone know of a solution to add a unique id to preview duplicated fields ?
Here is our current Plunkr and LIVE example.
$scope.addRow = function(){
var row = {};
$scope.productdata.push(row);
};
$scope.removeRow = function(index){
$scope.productdata.splice(index, 1);
};
$scope.formData you have is not an array, but just one object. All your rows are bound to that object and hence all of them reference the same data.
The reason you get a new row added is because your ng-repeat is bound to $scope.productData and you add extra record in it. You should bind your form elements to the properties in the row object that you create
a simple example is :
In your template
<div ng-repeat="product in products">
<input type="text" ng-model="product.title">
</div>
In your controller
$scope.addProduct = function(){
var product = {};
$scope.productData.add(product);
}
You'd then always only work with the productData array and bind your model to them.
Even in your backend calls, you'd use productData instead of your formData.
Hope this helps.
U can use a filter : This will return Unique rows only
app.filter('unique', function () {
return function (items, filterOn) {
if (filterOn === false) {
return items;
}
if ((filterOn || angular.isUndefined(filterOn)) && angular.isArray(items)) {
var hashCheck = {}, newItems = [];
var extractValueToCompare = function (item) {
if (angular.isObject(item) && angular.isString(filterOn)) {
return item[filterOn];
} else {
return item;
}
};
angular.forEach(items, function (item) {
var valueToCheck, isDuplicate = false;
for (var i = 0; i < newItems.length; i++) {
if (angular.equals(extractValueToCompare(newItems[i]), extractValueToCompare(item))) {
isDuplicate = true;
break;
}
}
if (!isDuplicate) {
newItems.push(item);
}
});
items = newItems;
}
return items;
};
});
I think the reason why this is happening is that the addRow() function is just pushing an empty son object into the $scope.productdata array, whereas all input fields are bound to $scope.formData[product.WarrantyTestDescription]. I think you mean to bind the input fields to the properties of the product object.

Knockout tree - get all selected items in tree

Here's the fiddle
I have a tree structure of clients that I'm binding to an unordered list, and each client may or may not have a SubClient. I've added the ability to select an item in the list but now I cannot figure out how to loop through the tree and get an array of all the selected items.
In particular, this beast is where I'm having problems:
cp.GetSelectedClientsArray = function (clients) {
var selected = [];
ko.utils.arrayForEach(clients, function (item) {
if (item.IsSelected()) {
selected.push(item.ClientName());
}
ko.utils.arrayForEach(item.SubClient(), function (subItem) {
if (subItem.IsSelected()) {
selected.push(subItem.ClientName());
}
cp.GetSelectedClientsArray(subItem);
});
});
console.log(selected);
return selected;
};
After I toggle the IsSelected() observable I'd like to loop through the list and get an array with only the selected items.
I've written and re-written this more than a few times, and could really use some help. I'm not even sure how to write a recursive function that would work, because every time I call the function from within, it wipes out my "selected" array and setting it as a global variable keeps any item that has ever been selected in the array.
Any help is appreciated
Here's recursive version
cp.GetSelectedClientsArray = function (clients) {
var result = [];
function GetSelected(clients){
for (var i in clients){
if(clients[i].IsSelected()){
result.push(clients[i].ClientName());
}
GetSelected(clients[i].SubClient());
}
}
GetSelected(clients);
console.log(result);
return result;
};
See jsfiddle
If I understand correctly what you are trying to do, why not try something like this?
_self.SelectedClient = ko.observableArray();
_self.ToggleSelectedUser = function (data, event) {
var toggle = !data.IsSelected();
data.IsSelected(toggle);
if(toggle === true)
{
_self.SelectedClient.push(data.ClientName());
}
else
{
_self.SelectedClient.remove(data.ClientName());
}
Why walk on the clients list recursively when you can simply create a SelectedClients field on the View-Model, and remove/add to it upon toggling?
For example:
_self.SelectedClients = ko.observableArray([]);
_self.ToggleSelectedUser = function (data, event) {
var toggle = !data.IsSelected();
data.IsSelected(toggle);
if (toggle)
_self.SelectedClients.push(data.ClientName());
else
_self.SelectedClients.remove(data.ClientName());
};
See Fiddle.
Update:
As per your comment, when you do need to walk recursively on the tree, you can try something like this:
function AggregateSelectedClients(clients, results)
{
results = results || [];
if (!clients || !clients.length) return results;
ko.unwrap(clients).forEach(function(v, i)
{
var selected = ko.unwrap(v.IsSelected);
var subClients = ko.unwrap(v.SubClient);
if (selected)
results.push(ko.unwrap(v.ClientName));
if (subClients && subClients.length)
AggregateSelectedClients(subClients, results);
});
return results;
}
The selected children have to be added to the parent selection.
ko.utils.arrayForEach(item.SubClient(), function (subItem) {
if (subItem.IsSelected()) {
selected.push(subItem.ClientName());
}
//cp.GetSelectedClientsArray(subItem);
selected.push.apply(selected, cp.GetSelectedClientsArray(subItem));
});
See fiddle
I hope it helps.

Checking if object is empty, works with ng-show but not from controller?

I have a JS object declared like so
$scope.items = {};
I also have a $http request that fills this object with items. I would like to detect if this item is empty, it appears that ng-show supports this... I enter
ng-show="items"
and magically it works,I would also like to do the same from a controller but i can't seem to get it to work, it appears I may have to iterate over the object to see if it has any properties or use lodash or underscore.
Is there an alternative?
I did try
alert($scope.items == true);
but it always returns false , when the object is created and when populated with $http, so its not working that way.
Or you could keep it simple by doing something like this:
alert(angular.equals({}, $scope.items));
In a private project a wrote this filter
angular.module('myApp')
.filter('isEmpty', function () {
var bar;
return function (obj) {
for (bar in obj) {
if (obj.hasOwnProperty(bar)) {
return false;
}
}
return true;
};
});
usage:
<p ng-hide="items | isEmpty">Some Content</p>
testing:
describe('Filter: isEmpty', function () {
// load the filter's module
beforeEach(module('myApp'));
// initialize a new instance of the filter before each test
var isEmpty;
beforeEach(inject(function ($filter) {
isEmpty = $filter('isEmpty');
}));
it('should return the input prefixed with "isEmpty filter:"', function () {
expect(isEmpty({})).toBe(true);
expect(isEmpty({foo: "bar"})).toBe(false);
});
});
regards.
Use an empty object literal isn't necessary here, you can use null or undefined:
$scope.items = null;
In this way, ng-show should keep working, and in your controller you can just do:
if ($scope.items) {
// items have value
} else {
// items is still null
}
And in your $http callbacks, you do the following:
$http.get(..., function(data) {
$scope.items = {
data: data,
// other stuff
};
});
another simple one-liner:
var ob = {};
Object.keys(ob).length // 0
If you couldn't have the items OBJ equal to null, you can do this:
$scope.isEmpty = function (obj) {
for (var i in obj) if (obj.hasOwnProperty(i)) return false;
return true;
};
and in the view you can do:
<div ng-show="isEmpty(items)"></div>
You can do
var ob = {};
Object.keys(ob).length
Only if your browser supports ECMAScript 5. For Example, IE 8 doesn't support this feature.
See http://kangax.github.io/compat-table/es5/ for more infos
if( obj[0] )
a cleaner version of this might be:
if( typeof Object.keys(obj)[0] === 'undefined' )
where the result will be undefined if no object property is set.
Or, if using lo-dash: _.empty(value).
"Checks if value is empty. Arrays, strings, or arguments objects with a length of 0 and objects with no own enumerable properties are considered "empty"."
Check Empty object
$scope.isValid = function(value) {
return !value
}
you can check length of items
ng-show="items.length"

Resources