yoyoma
yoyoma

Reputation: 3536

Using array.filter down multiple levels

I have a filter function, which uses filter to quickly search a text in an array:

filtered = filtered.filter((row) => {
   return Object.keys(row).some((key) => {
        return String(row[key]).toLowerCase().indexOf(this.quickSearch.toLowerCase()) > -1
      })
   })

This works great for single level array, but not sure how to adapt it to work down unknown number of levels of array of objects like this

{
   'name': 'james',
   'post': {
        'name': 'my favorite teams'
    }
}

The code above finds james, no problem, but it will not find teams as its not going deep enough.

Naturally I don't want to hardcode something like if row[key] == 'post', because I'm using this code for multiple data sources and need it to be dynamic.

How do I adapt this to work in multi level arrays like the example above?

Upvotes: 4

Views: 7786

Answers (2)

ibrahim mahrir
ibrahim mahrir

Reputation: 31712

If there are many levels, then recursion is the best solution:

let searchString = this.quickSearch.toLowerCase();                       // do this only once instead of calling toLowerCase over and over again, besides we are using a regular function (not an arrow one) so "this" will be messed up anyways
filtered = filtered.filter(function search(row) {                        // name the function so we can use recursion (thus we can't use an arrow function)
   return Object.keys(row).some((key) => {                               // ...
       if(typeof row[key] === "string") {                                // if the current property is a string
           return row[key].toLowerCase().indexOf(searchString) > -1;     // then check if it contains the search string
       } else if(row[key] && typeof row[key] === "object") {             // oterwise, if it's an object
           return search(row[key]);                                      // do a recursive check
       }
       return false;                                                     // return false for any other type (not really necessary as undefined will be returned implicitly)
   });
});

Upvotes: 4

SimpleJ
SimpleJ

Reputation: 14788

You could use a recursive function that calls itself on the values of objects and arrays (using Object.values and Array#some):

const containsDeep = (text) => (value) => {
  if(!value) return false;
  const valueType = typeof value;
  
  if(valueType === 'string') {
    return value.toLowerCase().indexOf(text.toLowerCase()) > -1;
  }
  if(Array.isArray(value)) {
    return value.some(containsDeep(text));
  }
  if(valueType === 'object') {
    return Object.values(value).some(containsDeep(text));
  }
  return false;
};

const data = [
  {
   'name': 'bar',
   'post': {
      'name': 'my favorite teams'
    }
  },
  {
    'name': 'fizz',
    'posts': [{'name': 'buzz'}]
  },
  {
    'name': 'bla',
    'post': {
      'name': 'blee',
      'comments': [null, {'name': 'bar'}]
    }
  },
  {
   'name': 'foo',
   'post': {
      'name': 'bar'
    }
  }
];

const result = data.filter(containsDeep('bar'));
console.log(result);

Upvotes: 2

Related Questions