recursive json tree filter - angularjs

I'm using angular 1.5.9 and I have a JSON object like this:
var data=[
{"id":1,"name":"object 1", childs:[
{"id":51,"name":"object 51", childs:[]},
]},
{"id":2,"name":"object 2", childs:[
{"id":11,"name":"object 11", childs:[]},
{"id":12,"name":"object 12", childs:[
{"id":13,"name":"object 100", childs:[]},
]},
]},
{"id":3,"name":"object 3", childs:[]},
{"id":1,"name":"object 1", childs:[]}
];
I need to filter this tree so that I get all the elements (branches or leaves whose name contains the filter string and all the parents.
i.e: filtering for "100" will result in
[
{"id":2,"name":"object 2", childs:[
{"id":12,"name":"object 12", childs:[
{"id":13,"name":"object 100", childs:[]},
]},
]},
]
This data will then be rendered in a customized tree directive using ng-repeat over the data itself
I'm wondering if someone can suggest a clean and efficent way to achieve this. All the code I've written seems to be too complex and end up traversing the tree so many times that a better way must exist.
actual metacode is somewhat like
* sequenially read ech JSON object in main array
* if name matches add a property (visible:true) and travel back to the beginning setting all the parents' visible:trre
* if childs array contain something, re-call the main filter function to scan all childrens
This could be somewhat acceptable for small datasets, but on large object will probably be very inefficient.

You can just write some recursive javascript for this, something like:
function findObjectAndParents(item, name) {
if (item.name.split(' ')[1] == name) {
return true;
}
for (var i = 0; i < item.childs.length; i++) {
if (findObjectAndParents(item.childs[i], name)) {
return true;
}
}
return false;
}
And use it like this:
var searchName = "100";
var filtered = data.filter(function(item) {
return findObjectAndParents(item, searchName);
});

Ref the answer:
A Javascript function to filter tree structured json with a search term. exclude any object which donot match the search term
function search(array, name) {
const s = (r, { childs, ...object }) => {
if (object.name.includes(name)) {
r.push({ object, childs: [] });
return r;
}
childs = childs.reduce(s, []);
if (childs.length) r.push({ ...object, childs });
return r;
};
return array.reduce(s, []);
}
console.log(JSON.stringify(search(data, '100'),0,2));

Related

How to Filter Restructured Data in Polymer (dom-repeat)

I am trying to filter an array that is being reindexed on-the-fly.
I would like to have a single input field that matches strings on multiple properties.
<paper-input value="{{val}}" placeholder="Filter Cards"></paper-input>
<template is="dom-repeat" items="[[restructure(data)]]" initial-count="2" filter="{{filter(val, data)}}">
<card-items data="[[item]]" items="[[item.items]]" links="false"></card-items>
</template>
...
This function restructures the data to be formatted for a card layout.
returnInvoices(data) {
let newData = [];
for (let i = 0; i < data.length; i++) {
let noOrder = data[i].noOrder;
if (!newData[noOrder]) {
newData[noOrder] = {
idMaster: data[i].idMaster,
itemId: data[i].itemId,
description: data[i].description,
noOrder: noOrder,
items: []
};
}
newData[noOrder].items.push('{' +
'"idMaster":"' + data[i].idMaster + '",' +
'"itemId":"' + data[i].itemId + '"}');
}
return newData.filter(val => val).sort((a, b) => {return b.noInvoice - a.noInvoice}) // reindex array
}
I would like this function to return objects in the array whom have properties that match a string.
filter(val, data) {
if (!val) return null;
else {
val = val.toLowerCase();
// not sure what to do here
// would like to filter on all object properties (from data)
return data[0];
}
}
...
Example
if(val == 1) return data[0] & data[1];
if(val == 'Item') return data[0] & data[2];
For data array
let data = [
{"itemId": "1", "description": "Nice Item", "noOrder": 123},
{"itemId": "2", "description": "Good Product", "noOrder": 123},
{"itemId": "3", "description": "Fine Item", "noOrder": 444}
}
...
How can I filter strings on all 3 properties?
How can I use the filter as an intermediate function to the restructuring?
The documentation for dom-repeat's filter property includes following statements:
The function should match the sort function passed to Array.filter. Using a filter function has no effect on the underlying items array.
And the Array.filter is documented as
Function is a predicate, to test each element of the array. Return true to keep the element, false otherwise.
So from your filter func just return true if any of the properties matches input and false otherwise, something like
filter(item) {
let val = this.val;
// if the filter is empty show everything
if (!val) return true;
// otherwise see is there a match
val = val.toLowerCase();
return // for the "description" use "contains" logic
(item.description.toLowerCase().includes(val)) ||
// for the "noOrder" use "starting" logic
(item.noOrder.toString().indexOf(val) == 0)
// add matching conditions here ...
}
Now to trigger the filtering you must observe the properties which trigger filtering, ie your html would be like
<paper-input value="{{val}}" placeholder="Filter Cards"></paper-input>
<template is="dom-repeat" filter="[[filter]]" observe="val" items="[[restructure(data)]]" initial-count="2">
<card-items data="[[item]]" items="[[item.items]]" links="false"></card-items>
</template>
BTW why do you push items into newData as strings? Why not as objects, ie
newData[noOrder].items.push({
idMaster: data[i].idMaster,
itemId: data[i].itemId
});
and I think you can lose the newData.filter(val => val) step...

Smart Table st-search inside nested objects

Is there any way to search inside nested elements in smart-table? I feed the table with data from a REST Api that consists of the following form:
{
"id": 1,
"small_name": "Foo",
"large_name": "Bar Foo",
"variants": [{"value": "0"}, {"value": "1"}]
}
What I want to achieve is the possibility to filter the data through the value property of the objects inside the variants.
From the Smart Table documentation:
"The stSetFilter replaces the filter used when searching through Smart Table. When the default behavior for stSearch does not meet your demands, like in a select where one entry is a substring of another, use a custom filter to achieve your goals."
http://lorenzofox3.github.io/smart-table-website/
There is also an example available at that site.
I'll post the solution for my problem, maybe it can help someone.
angular.module('YourModule').filter('CustomFilter', [
'$parse',
function ($parse) {
return function(items, filters) {
console.log(items, filters);
var itemsLeft = items.slice();
Object.keys(filters).forEach(function (model) {
var value = filters[model],
getter = $parse(model);
itemsLeft = itemsLeft.filter(function (item) {
if (model === 'value') {
var variants = item.variants.filter(function (variant) {
return getter(variant).match(value);
});
return variants.length;
} else {
return getter(item).match(value);
}
});
});
return itemsLeft;
}
}
])

Merge objects with different values using Angularjs or Underscore js

I'm trying to merge two objects into a single multidimensional object for use in Angularjs controller by the 'unique_id'. (Note I also have Underscore Js added in).
Object #1 example:
[
{ "unique_id": "001", "title": "Putting Green Challenge - Motion Depends on Force and Mass" },
{ "unique_id": "002", "title": "Molecules to Organisms: Frog Life Cycle" }
]
Object #2 example (has MANY more rows than object 1..):
[
{
"ab_id": "76153F02-29F3-11D8-95EA-951BF95D9AEF",
"unique_id": "001",
"title": "How Speed Relates to Energy",
"state": "NY",
"document_title": "Core Curriculum",
"grade_code": "K-4",
"grade_descr": "Elementary",
"state_id": "1.S2.3a",
"state_text": "Use appropriate \"inquiry and process skills\" to collect data"
},
{
"ab_id": "7980A762-29F3-11D8-BD14-861D7EA8D134",
"unique_id": "001",
"title": "How Speed Relates to Energy",
"state": "NY",
"document_title": "Core Curriculum",
"grade_code": "5-8",
"grade_descr": "Intermediate",
"state_id": "1.S3.2d",
"state_text": "formulate and defend explanations and conclusions as they relate to scientific phenomena"
}
]
My Controller:
abApp.controller("abEE", function(abService, $http, $scope, $q, _) {
var abApp = this;
$scope.abData = $http.get('/data/ab_activities.json', {
cache: false
});
$scope.eeData = $http.get('/activities/eedata', {
cache: false
});
$q.all([$scope.eeData, $scope.abData]).then(function(values) {
var val = ??? This is where I want to merge the objects into one big multidimensional object..
});
Here is the output of console.dir(values);
0 Object { data=[28], status=200, config={...}, more...}
1 Object { data=[743], status=200, config={...}, more...}
This is the desired output I'd like to try and get:
[
{ "unique_id": "001", "title": "Putting Green Challenge - Motion Depends on Force and Mass", "alignments": [{"ab_id": "76153F02-29F3-11D8-95EA-951BF95D9AEF","unique_id": "001","title": "How Speed Relates to Energy",...}, {"ab_id": "7980A762-29F3-11D8-BD14-861D7EA8D134", "unique_id": "001", "title": "How Speed Relates to Energy",...}]
]
Edit
after you updated the question, i created this plunker
hopes it's what you meant
To merge all objects by unique_id
var unions = {};
$q.all([$scope.eeData, $scope.abData]).then(function(values)
{
for (var i = 0; i< values.length; i++)
{
var value = values[i];
if (!unions[value.unique_id])
{
unions[value.unique_id] = {};
}
angular.extend(unions[value.unique_id], value);
}
});
// Do somthing with 'unions'
...
If you could switch to use lodash instead of underscore, it can be achieved like this:
var val = _.values(_.merge(_.indexBy(values[0].data, 'unique_id'), _.indexBy(values[1].data, 'unique_id')));
The underscore doesn't have _.merge(), you have to loop through each property without it.
I don't think angular or underscore have this kind of functionality. I would do something like the following pseudo-code:
tempObject = {}
for object in objectArray
if tempObject[object.unique_id] isnt undefined
tempObject[object.unique_id] = object
else angular.extend(tempObject[object.unique_id], object) // or the other way around depending on your preference
resultingArray = []
for object, key of tempObject
resultingArray.push(object)
You will have to run the for object in objectArray for both the returned arrays but that should work and is probably more efficient than most merge algorithms as at most it will loop through each returned arrays twice.

Angular: mixing ng-repeat, arrays, and objects with ids

I have a collection of items which I'm using with ng-repeat and filtering.
Each item has a unique Id, and is stored in an array:
[ {id: 387, name: "hello"}, {id: 2878, name: "world"}, ...]
But now I need to reference these objects by their unique id instead of array index (as well as use ng-repeat with filters).
So I tried using a sparse array:
array[387] = {name: "hello"}; array[2878] = {name: "world"}...
But ng-repeat craps itself because it sees 'duplicate' undefined keys. (I also tried using 'track by' but ng-repeat still didn't like it).
I can't use an object with ng-repeat because filters don't work.
Soooo, how can I both use ng-repeat with filters, and be able to reference the items by id? The only option I can think of is to have a second data structure to map id's to indexes.
Thanks for your help!
Chris.
Try this:
'track by $index'
ng-repeat="name in Lists track by $index"
When I need to achieve this sort of thing, I do some quick preprocessing of the data grabbed, and create the necessary lookup. For example:
var data = [
{ id: 55, name: 'some name' },
{ id: 65, name: 'another name' }
];
// create a lookup
var lookup = {};
angular.forEach(data, function(item){
lookup[data.id] = item;
});
You could manipulate the data any way you want here, but by referencing the actual array items, you can bind either the data or lookup (or both) to $scope, and any changes will be exhibited on both structures. Bear in mind this won't happen if you add/remove items from the array, as you need to manage the addition and removal of items from the lookup.
I can see your ids are integer. angular filters not working because angular use string comparison for number.
Go to the angular.js search a function name filterFilter. line 13991 for v1.2.10. Then you can use objects in your ng-repeat.
function filterFilter() {
...
var search = function(obj, text){
if (typeof text == 'string' && text.charAt(0) === '!') {
return !search(obj, text.substr(1));
}
switch (typeof obj) {
case "boolean":
case "number": // <-- here
// add return obj === text;
case "string":
return comparator(obj, text);
case "object":
switch (typeof text) {
case "object":
return comparator(obj, text);
default:
for ( var objKey in obj) {
if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) {
return true;
}
}
break;
}
return false;
case "array":
for ( var i = 0; i < obj.length; i++) {
if (search(obj[i], text)) {
return true;
}
}
return false;
default:
return false;
}
};

Getting models in nested collections in Backbonejs

I'm working in a collection that contains a model with collections of "itself". For example:
[{
id: 1
name: "John",
children: [
{
id: 32
name: "Peter",
children: []
},
{
id: 54
name: "Mary",
children: [
{
id:12,
name: "Kevin"
}
]
},
]
}]
Let say that I want to get the Kevin "user" by its Id. But all that I have is the "first collection". How can I do that?? And about setting a user within a collection? Another thing: Its possible to get all the Kevin "parents" from him? Like Mary and John?
Does anyone has come to a issue like that?
Thanks a LOT
Well I've made a recursive function on the User's Collection that seems to solved the problem for now ( the best of this is that I can use for retrieve a "deep" model and change it.). Something like that ( if someone has any suggestions, be free to give it a opinion ):
findUserById: function(id) {
var self = new Backbone.Collection(this.toJSON());
return thisCollection(id, this);
function thisCollection(id, collection, array) {
var innerSelf = collection || this;
var thisArray = array || [];
for(var i = innerSelf.models.length; i--;) {
if(innerSelf.models[i].get('id') == id) {
return [innerSelf.models[i]].concat([thisArray]);
}else {
if(innerSelf.models[i].get('children').length > 0) {
thisArray.push(innerSelf.models[i]);
return thisCollection(id, innerSelf.models[i].get('children'), thisArray);
}else {
innerSelf.remove(innerSelf.models[i]);
return thisCollection(id, self, []);
}
}
}
}
}
Basically I return an array with 2 items. The first is the record that I'm looking for and the second is an array with the parents of this user.
Underscore (which is a Backbone dependency, so you already have it) is great for this sort of thing; if you use its "map" function (which Backbone provides as a method on Collection) with its find function, you can do the following:
findPersonInPeopleCollection: function(nameWeAreLookingFor) {
function findChildren(person) {
if (!person.children) return [person];
var children = _.map(person.children, function(child) {
foundPeople.push(findChildren(child);
})
return _.flatten(children);
}
var allPeople = _.flatten(myCollectionOfPeople.map(findChildren));
return _(allPeople).find(function(person) {
return person.get('name') == nameWeAreLookingFor;
}
}
If you wanted to store the parents initially you could either add logic to your "Person" model class's initialize function, eg.
var Person = Backbone.Model.extend({
initialize: function() {
_.each(this.get('children'), function(child) {
child.parent = this;
}, this);
}
});
You could also do something similar by overriding your collection's add method, or adding an event handler to it that triggers after people get added.

Resources