I have a filter that is on ng-repeat and compares strings of all objects (including nested ones) to a search string. If the search string is found in the object, it returns true.
I'm looking for a way to extend this functionality so that when the search string matches with a string in the object, the filter will return true for that object and will return true for all nested objects in the matching object (this is a tree view, I'm searching for a node and want to show all children nodes when matched).
How would I do that?
My filter looks like this:
.filter('deepFilter', function ($filter) {
return function(text) {
return function (value) {
if(text && text.length > 0) {
var searchTerm = text;
if (angular.isObject(value)) {
var found = false;
angular.forEach(value, function(v) {
found = found || $filter('deepFilter')(searchTerm)(v);
});
return found;
} else if (angular.isString(value)) {
if (value.indexOf(searchTerm) !== -1) {
return true;
} else {
return false;
}
}
} else {
return true;
}
};
};
});
The solution I found is by using a function in the isString part of the filter, and iterating over the collection. If I find the object, I look for it's children using a recursive function and set a visibleAsAChild property for these. Then, I've added a condition in the isObject evaluation to return true for these object that have visibleAsAChild prop.
I'm not sure if this is the most efficient way to do it, but it certainly works.
.filter('deepFilter', function ($filter) {
var currentObject;
var setChildrenToVisible = function(node) {
angular.forEach(node.nodes, function(node) {
if(node.nodes) {
setChildrenToVisible(node);
}
node.visibleAsAChild = true;
});
};
var lookupChildren = function(o, value) {
// console.log(o);
angular.forEach(o.nodes, function(node) {
if (node.name === value) {
setChildrenToVisible(node);
}
});
};
return function(text) {
return function (value) {
if(text && text.length > 0) {
var searchTerm = text;
if (angular.isObject(value)) {
var found = false;
angular.forEach(value, function(v) {
found = found || $filter('deepFilter')(searchTerm)(v);
});
if(found && value.hasOwnProperty('id')) {
currentObject = value;
}
if(value.hasOwnProperty('id') && value.visibleAsAChild) {
return true;
}
return found;
} else if (angular.isString(value)) {
if (value.indexOf(searchTerm) !== -1) {
if(currentObject){
lookupChildren(currentObject, value);
}
return true;
} else {
return false;
}
}
} else {
return true;
}
};
};
I have here a filter for AngularJS. Is there any better way for this?
return function (data, selected) {
var result = [];
if (selected[0] == 'All Types') {
result = data;
}
else {
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < selected.length; j++) {
if (data[i].Type == selected[j]) {
result.push(data[i]);
}
}
}
}
return result;
};
return function (data, selected) {
var result = [];
if (selected[0] == 'All Types') {
result = data;
}
else {
result=data.filter(x=>selected.indexOf(x.Type)>-1);
}
return result;
};
Obviously this is using lodash, but native methods can be substituted:
return (data, selected) => {
return
_.first(selected) === 'All Types' ? data :
_.reduce(data, (sum, value, key) => {
if (_.includes(selected, value.Type)) {
sum.push(value);
}
return sum;
}, []);
}
Use data for selected value. I am assuming selected value is in data[i].selected.
return function (data) {
var result = [];
if (data[0].selected == 'All Types') {
result = data;
}
else {
for (var i = 0; i < data.length; i++) {
if (data[i].Type == data[i].selected) {
result.push(data[i]);
}
}
}
return result;
};
We can use filters
return function (data) {
if (data[0].selected == 'All Types') {
return data;
}
else {
return data.filter(function (item) {
return item.Type == item.selected;
});
}
};
I'm having a bit of trouble getting my function to return a single true or false
Basically I have an array like below;
orderItem contains menu_modifier_groups which contains menu_modifier_items
Now each menu_modifier_groups has an attribute max_selection_points.
The menu_modifier_items are displayed using ng-repeat with checkboxes and have an ng-model = item.selected
What I want to do is to be able to loop through all menu_modifier_groups and determine if the required menu_modifier_items have been selected by comparing them to max_selection_points of each menu_modifier_groups
This is what I have so far
$scope.isValid = function (orderItem) {
var count = 0;
angular.forEach(orderItem.menu_modifier_groups, function(group) {
angular.forEach(group.menu_modifier_items, function (item) {
count += item.selected ? 1 : 0;
});
if (count != group.max_selection_points) {
return false;
} else if (count == group.max_selection_points) {
return true;
}
});
}
Any help/advice appreciated
Your code should be
$scope.isValid = function(orderItem) {
//By default make it false
var IsAllSelected = false;
angular.forEach(orderItem.menu_modifier_groups, function(group) {
var count = 0;
angular.forEach(group.menu_modifier_items, function(item) {
count += item.selected ? 1 : 0;
});
if (count == group.max_selection_points) {
IsAllSelected = true;
} else {
//if one item failed All select do return false
IsAllSelected = false;
}
});
return IsAllSelected;
}
Is there a formal property bag for Angular? I need a way to temporarily store variables and optionally save them between apps.
To save time, I whipped one up. It's here if anyone needs it, however I would love to find one in a framework that is supported by a team:
/**
* Created by Fred Lackey on 4/24/15.
*/
(function (module) {
var propertyBag = function (localStorage) {
var items = [];
var getIndex = function (key) {
if (!key || !key.trim || key.trim().length < 1) { return -1; }
if (items.length < 1) { return -1; }
for (var i = 0; i < items.length; i += 1) {
if (items[i].key.toLowerCase() === key.toLowerCase()) { return i; }
}
return -1;
};
var exists = function (key) {
return (getIndex(key) >= 0);
};
var getItem = function (key) {
var index = getIndex(key);
return (index >= 0) ? items[index] : null;
};
var getValue = function (key) {
var index = getIndex(key);
return (index >= 0) ? items[index].value : null;
};
var putItem = function (key, value) {
if (!key || !key.trim || key.trim().length < 1) { return; }
var index = getIndex(key);
if (index >= 0) {
items[index].value = value;
} else {
items.push({ key: key, value: value });
}
};
var removeItem = function (key) {
var index = getIndex(key);
if (index >= 0) { items.splice(index, 1); }
};
var count = function () {
return items.length;
};
var saveBag = function (key) {
if (!key || !key.trim || key.trim().length < 1) { return; }
localStorage.add(key, items);
};
var loadBag = function (key) {
if (!key || !key.trim || key.trim().length < 1) { return; }
var bag = localStorage.get(key);
if (!bag || !bag.length) { return; }
for (var i = 0; i < bag.length; b += 1) {
if (!bag[i].key || !bag[i].key.trim || bag[i].key.trim().length < 1) { continue; }
putItem(bag[i].key, bag[i].value);
}
localStorage.remove(key);
};
return {
getItem: getItem,
exists: exists,
getValue: getValue,
putItem: putItem,
removeItem: removeItem,
count: count,
save: saveBag,
load: loadBag
};
};
module.factory('propertyBag', propertyBag);
})(angular.module('common'));
Here's the local storage code if you don't already have one (referenced in the code above)...
(function (module) {
var localStorage = function ($window) {
var store = $window.localStorage;
var add = function (key, value) {
value = angular.toJson(value);
store.setItem(key, value);
};
var get = function (key) {
var value = store.getItem(key);
if (value) {
value = angular.fromJson(value);
}
return value;
};
var remove = function (key) {
store.removeItem(key);
};
return {
add: add,
get: get,
remove: remove
}
};
module.factory('localStorage', localStorage);
})(angular.module('common'));
In an Angular scope, I have a collection of objects that carry some data, plus x and y coordinates. Some other scope variables must be recomputed based on the x and y values. What is the best way to do it efficiently?
If I use $scope.$watch(collection, handler) or $scope.$watchCollection(collection, handler), I don't get notified about changes to the objects it contains.
If I use $scope.$watch(collection, handler, true), I do get notified, but when anything changes, not only x and y. Plus, I don't know which element of the collection was changed (and I imagine that this deep comparison is rather costly).
Ideally, I would like to write something like $scope.$watchObjects(collection, ["x", "y"], handler), where my handler would be called with the changed object and possibly its index. Is there an easy way to do that?
Could you do:
angular.forEach(colletion, function(object) {
$scope.$watch(object, function() {
... I'm not sure what would you like to do with object here...
})
}, true)
I am pretty sure it was in this video: https://www.youtube.com/watch?v=zyYpHIOrk_Y
but somewhere I saw Angular devs talking about mapping the data you are watching to a smaller subset, something like this maybe:
$scope.$watchCollection(function() {
return yourList.map(function(listItem) {
return { 'x': listItem.x, 'y': listItem.y };
};
}, function(newVal, oldVal) {
// perform calculations
});
That would leave you $watching just an array of objects having x and y properties.
$scope.$watch('collection', function() {
...
}, true);
Keep in mind that the collection must be declared on the $scope.
Based on Slaven Tomac’s answer, here's what I came up with. Basically: this uses a $watchCollection to detect when items are inserted or added on the collection. For each added item, it starts monitoring it. For each removed item, it stops monitoring it. It then informs a listener each time an object changes.
This further allows to refine what should be considered as a change in the object itself or a change in the collection only. The sameId function is used to test whether two objects a and b should be considered to be the same (it could just a === b, but it could be something more sophisticated — in particular, if you pass in a field name as the sameId argument [e.g., "id"], then two objects will be considered to be “the same.”)
The createArrayDiffs is adapted from a similar change-detection method in the Eclipse Modeling Framework and is interesting in its own right: it returns a list of changes that happened between an array and another array. Those changes are insertions, removals, and object changes (according to the passed fields).
Sample usage:
watchObjectsIn($rootScope, "activities", "id", ["x", "y"], function (oldValue, newValue) {
console.log("Value of an object changed: from ", oldValue, " to ", newValue);
});
Of course, I'm interested in any simpler and/or more efficient solution!
Implementation (compiled TypeScript):
function watchObjectsIn(scope, expr, idField, watchedFields, listener) {
var fieldCompareFunction = makeFieldCompareFunction(watchedFields);
var unbindFunctions = [];
function doWatch(elem, i) {
var unbindFunction = scope.$watch(function () {
return elem;
}, function (newValue, oldValue) {
if (newValue === oldValue)
return;
if (!fieldCompareFunction(oldValue, newValue))
listener(oldValue, newValue);
}, true);
unbindFunctions.push(unbindFunction);
}
function unwatch(elem, i) {
unbindFunctions[i]();
unbindFunctions.splice(i, 1);
}
scope.$watchCollection(expr, function (newArray, oldArray) {
if (isUndef(newArray))
return;
var diffs = createArrayDiffs(oldArray, newArray, idField, fieldCompareFunction);
if (diffs.length === 0 && newArray.length !== unbindFunctions.length) {
for (var i = unbindFunctions.length - 1; i >= 0; i--) {
unwatch(null, 0);
}
diffs = createArrayDiffs([], newArray, idField);
}
_.forEach(diffs, function (diff) {
switch (diff.changeType()) {
case 0 /* Addition */:
doWatch(diff.newValue, diff.position);
break;
case 1 /* Removal */:
unwatch(diff.oldValue, diff.position);
break;
case 2 /* Change */:
listener(diff.oldValue, diff.newValue);
break;
}
});
});
}
function isUndef(v) {
return typeof v === "undefined";
}
function isDef(v) {
return typeof v !== "undefined";
}
function parseIntWithDefault(str, deflt) {
if (typeof deflt === "undefined") { deflt = 0; }
var res = parseInt(str, 10);
return isNaN(res) ? deflt : res;
}
function cssIntOr0(query, cssProp) {
return parseIntWithDefault(query.css(cssProp));
}
function randomStringId() {
return Math.random().toString(36).substr(2, 9);
}
var ArrayDiffChangeType;
(function (ArrayDiffChangeType) {
ArrayDiffChangeType[ArrayDiffChangeType["Addition"] = 0] = "Addition";
ArrayDiffChangeType[ArrayDiffChangeType["Removal"] = 1] = "Removal";
ArrayDiffChangeType[ArrayDiffChangeType["Change"] = 2] = "Change";
})(ArrayDiffChangeType || (ArrayDiffChangeType = {}));
var ArrayDiffEntry = (function () {
function ArrayDiffEntry(position, oldValue, newValue) {
this.position = position;
this.oldValue = oldValue;
this.newValue = newValue;
}
ArrayDiffEntry.prototype.changeType = function () {
if (isUndef(this.oldValue))
return 0 /* Addition */;
if (isUndef(this.newValue))
return 1 /* Removal */;
return 2 /* Change */;
};
return ArrayDiffEntry;
})();
function makeFieldCompareFunction(fields) {
return function (o1, o2) {
for (var i = 0; i < fields.length; i++) {
var fieldName = fields[i];
if (o1[fieldName] !== o2[fieldName])
return false;
}
return true;
};
}
function createArrayDiffs(oldArray, newArray, sameId, sameData, undefined) {
if (isUndef(sameId)) {
sameId = angular.equals;
} else if (_.isString(sameId)) {
var idFieldName = sameId;
sameId = function (o1, o2) {
return o1[idFieldName] === o2[idFieldName];
};
}
var doDataChangedCheck = isDef(sameData);
if (doDataChangedCheck && !_.isFunction(sameData)) {
if (_.isString(sameData))
sameData = [sameData];
var fieldsToCheck = sameData;
sameData = makeFieldCompareFunction(fieldsToCheck);
}
var arrayDiffs = [];
function arrayIndexOf(array, element, index) {
for (var i = index; i < array.length; i++) {
if (sameId(array[i], element))
return i;
}
return -1;
}
var oldArrayCopy = oldArray ? oldArray.slice() : [];
var index = 0;
var i;
for (i = 0; i < newArray.length; i++) {
var newValue = newArray[i];
if (oldArrayCopy.length <= index) {
arrayDiffs.push(new ArrayDiffEntry(index, undefined, newValue));
} else {
var done;
do {
done = true;
var oldValue = oldArrayCopy[index];
if (!sameId(oldValue, newValue)) {
var oldIndexOfNewValue = arrayIndexOf(oldArrayCopy, newValue, index);
if (oldIndexOfNewValue !== -1) {
var newIndexOfOldValue = arrayIndexOf(newArray, oldValue, index);
if (newIndexOfOldValue === -1) {
arrayDiffs.push(new ArrayDiffEntry(index, oldValue, undefined));
oldArrayCopy.splice(index, 1);
done = false;
} else if (newIndexOfOldValue > oldIndexOfNewValue) {
if (oldArrayCopy.length <= newIndexOfOldValue) {
newIndexOfOldValue = oldArrayCopy.length - 1;
}
arrayDiffs.push(new ArrayDiffEntry(index, oldValue, undefined));
oldArrayCopy.splice(index, 1);
arrayDiffs.push(new ArrayDiffEntry(newIndexOfOldValue, undefined, oldValue));
oldArrayCopy.splice(newIndexOfOldValue, 0, oldValue);
done = false;
} else {
arrayDiffs.push(new ArrayDiffEntry(oldIndexOfNewValue, newValue, undefined));
oldArrayCopy.splice(oldIndexOfNewValue, 1);
arrayDiffs.push(new ArrayDiffEntry(index, undefined, newValue));
oldArrayCopy.splice(index, 0, newValue);
}
} else {
oldArrayCopy.splice(index, 0, newValue);
arrayDiffs.push(new ArrayDiffEntry(index, undefined, newValue));
}
} else {
if (doDataChangedCheck && !sameData(oldValue, newValue)) {
arrayDiffs.push(new ArrayDiffEntry(i, oldValue, newValue));
}
}
} while(!done);
}
index++;
}
for (i = oldArrayCopy.length; i > index;) {
arrayDiffs.push(new ArrayDiffEntry(--i, oldArrayCopy[i], undefined));
}
return arrayDiffs;
}