Filter an object by multiple properties

Hi freeCodeCamp fam :wave:

This has been driving me mad nearly all day, if anyone could validate this I’d muchly appreciate.

Ok, so the setup is, I have a Gatsby site with an index page and some data, the data looks a lot like this:

const data = [
  {
    frontmatter: {
      title: 'Alfred',
    },
    headings: [
      {
        value: 'Add custom search',
        depth: 2,
      },
      {
        value: 'Change the default search in Alfred',
        depth: 2,
      },
    ],
    fields: {
      slug: '/alfred/',
    },
  },
  {
    frontmatter: {
      title: 'Fish Shell',
    },
    headings: [
      {
        value: 'Aliases',
        depth: 2,
      },
      {
        value: 'Oh My Fish',
        depth: 2,
      },
      {
        value: 'Use nvm with fish',
        depth: 2,
      },
      {
        value: 'List out added aliases',
        depth: 2,
      },
    ],
    fields: {
      slug: '/fish/',
    },
  },
  {
    frontmatter: {
      title: 'Bash',
    },
    headings: [
      {
        value: 'Add an alias',
        depth: 2,
      },
      {
        value: 'Sort alphabetically 👌',
        depth: 2,
      },
      {
        value:
          'Open the SSH agent each time you open a new terminal.',
        depth: 2,
      },
    ],
    fields: {
      slug: '/bash/',
    },
  },
];

I’m trying to do a fuzzy search, which I’ve partially achieved on the slug and title fields it’s just this value field in the headings array that’s driving me loopy!

function filterData(data, filters = {}) {
    const defaults = {
      fields: { slug: null },
      frontmatter: { title: null },
      headings: { value: null },
    };

    filters = Object.assign({}, defaults, filters);

    return data.filter(sheet => {
      return (
        sheet.fields.slug
          .toLowerCase()
          .includes(filters.fields.slug) ||
        sheet.frontmatter.title
          .toLowerCase()
          .includes(filters.frontmatter.title)
      );
    });
  }

  const result = filterData(nodes, {
    fields: { slug: searchTerm },
    frontmatter: { title: searchTerm },
    headings: { value: searchTerm },
  });

The above works, it’s when I try filter on the headings, so for now let’s take out the filters for slug and title and take a look at the last part:

function filterData(data, filters = {}) {
    const defaults = {
      fields: { slug: null },
      frontmatter: { title: null },
      headings: { value: null },
    };

    filters = Object.assign({}, defaults, filters);

    return data.filter(sheet => {
      return (
        sheet.headings.map(h =>
          h.value.toLowerCase().includes(filters.headings.value)
        )
      );
    });
  }

  const result = filterData(nodes, {
    fields: { slug: searchTerm },
    frontmatter: { title: searchTerm },
    headings: { value: searchTerm },
  });

If I console.log, title and slug I’ll get true for each of the three objects.

If I do the same for the for the headings value I’ll get an array for the headings, like, say if I input fish I’ll get one element in one of the arrays, like:

[false, false]
[false, true, true, false]
[false, false, false]

What I need to do, but cant work out how to right now is return true for the whole node, I think.

What are you thoughts?

I have made a codesandbox.io example for everyone to take a look at too.

If you are trying to return an array of the objects in data where frontmatter.title, fields.slug, or any of the values of the objects the headings array include the search term, the following could work using the example data you posted above.

function filterData(data, searchTerm) {
  return data.filter(({
    frontmatter: { title },
    headings,
    fields: { slug }
  }) => {
    const headingValues = headings.map(({ value }) => value);
    return [ title, ...headingValues, slug ]
      .some(value => value.toLowerCase().includes(searchTerm)); 
  });
}

const result = filterData(data, 'fish');

/*  result contains the following array.
[{
  "frontmatter": {
    "title": "Fish Shell"
  },
  "headings": [{
    "value": "Aliases",
    "depth": 2
  }, {
    "value": "Oh My Fish",
    "depth": 2
  }, {
    "value": "Use nvm with fish",
    "depth": 2
  }, {
    "value": "List out added aliases",
    "depth": 2
  }],
  "fields": {
    "slug": "/fish/"
  }
}]
*/
return data.filter(sheet => {
      return (
        sheet.headings.map(h =>
          h.value.toLowerCase().includes(filters.headings.value)
        )
      );
    });

Remember that a filter function should return a boolean. You’re using map which returns an array. If I understand what you’re trying to do, I believe that you want to use a reduce instead of map.

1 Like

@camperextraordinaire and @ArielLeslie thank you both!

@camperextraordinaire I think your solution will work but I will also use @ArielLeslie’s suggestion of using reduce in this section:

const headingValues = headings.map(({ value }) => value);

So that I’m only getting back the elements I want to show rather than the whole list, brilliant!

Thanks so much :pray:

So in the data headings.value there are two instances of add in the bash and alfred objects:

headings: [
  {
    value: "Add custom search",
    depth: 2
  },

and

headings: [
  {
    value: "Add an alias",
    depth: 2
  },

How it is currently, the whole array is being returned whereas I’d only want to have the two values.

So, currently the results look like, this:

{
  "frontmatter": {
   "title": "Alfred"
  },
  "headings": [
   {
    "value": "Add custom search",
    "depth": 2
   },
   {
    "value": "Change the default search in Alfred",
    "depth": 2
   }
  ],
  "fields": {
   "slug": "/alfred/"
  }
 },
 {
  "frontmatter": {
   "title": "Bash"
  },
  "headings": [
   {
    "value": "Add an alias",
    "depth": 2
   },
   {
    "value": "Sort alphabetically 👌",
    "depth": 2
   },
   {
    "value": "Open the SSH agent each time you open a new terminal.",
    "depth": 2
   }
  ],
  "fields": {
   "slug": "/bash/"
  }
 }

Desired result is like:

{
  "frontmatter": {
   "title": "Alfred"
  },
  "headings": [
   {
    "value": "Add custom search",
    "depth": 2
   }
  ],
  "fields": {
   "slug": "/alfred/"
  }
 },
 {
  "frontmatter": {
   "title": "Bash"
  },
  "headings": [
   {
    "value": "Add an alias",
    "depth": 2
   }
  ],
  "fields": {
   "slug": "/bash/"
  }
 }

Brilliant! Thanks so much @camperextraordinaire :pray: