Danny Bergs
Danny Bergs

Reputation: 101

How to filter JavaScript Objects with multiple criterias

I've got a JSON file with recipes. Now there is a search bar on my homepage where the user can check and uncheck checkboxes to select which attribute of the recipe (name, ingredients, tags, category) he wants to search in. He can also check multiple criteria.

Now I want to filter my object based on the selected criteria. I know if there is for example only checked the "name" I can just go for

recipes.filter(e -> e.name.indexOf(searchString) >= 0)

But how can I say dynamically "If also the ingredients are checked filter for a found result in the name OR in the ingredients".

I hope you understand. Thank you.

Upvotes: 2

Views: 374

Answers (4)

Zlatko
Zlatko

Reputation: 19569

So you have two things: search term and fields to search.

You can build a filtering function that takes those two, and a single record (single recipe) and returns true or false.

Say your checkboxes are name, description, ingreedients.

What you do is you send your filter function the item name, but also the name of the fields you wanna search. Then insert the values there.

You could have something like this:

// Disclaimer: these recipes are made up
const recipes = [{
    name: 'lemon cake',
    description: 'a delicious cake',
    ingreedients: ['lemon', 'flour', 'sugar'],
  },
  {
    name: 'sour lemon cake',
    description: 'a delicious cake',
    ingreedients: ['lemon', 'flour', 'not sugar'],
  },
  {
    name: 'choco brownie',
    description: 'a sweet chocolate desert',
    ingreedients: ['chocolate', 'milk', 'flour', 'salt', 'sugar'],
  },
  {
    name: 'vanilla croissant',
    description: 'a yummy pastry with vanilla flavour',
    ingreedients: ['vanilla', 'milk', 'flour'],
  }
];

// To search, we need the search term, and an array of fields by which to search
// We return ANY match, meaning if the search term is in any of the fields, it's a match
function searchAnyField(searchTerm, searchFields) {
  return recipes.filter(item => {
    for (let field of searchFields) {
      if (item[field].indexOf(searchTerm) > -1) {
        return true;
      }
    }
    return false;
  });
}

// the same, but we make sure the search term exists in ALL fields (seems dumb here, but could be a req)
function searchAllFields(searchTerm, searchFields) {
  return recipes.filter(item => {
    // A naive approach is to count in how many fields did we find the term and if it matches the search fields length
    let foundInFields = 0;
    for (let field of searchFields) {
      if (item[field].indexOf(searchTerm) > -1) {
        foundInFields++;
      }
    }
    return foundInFields === searchFields.length;
  });
}

// Let's see it in action
// we'lll just print out the names of found items
console.log('Brownie in name:', printNames(
  searchAnyField('brownie', ['name'])));
console.log('Cake in name:', printNames(
  searchAnyField('cake', ['name'])));
// Let's try multiple fields
console.log('Choc in name and desc:', printNames(
  searchAnyField('choc', ['name', 'description'])));
console.log('flour anywhere:', printNames(
  searchAnyField('flour', ['name', 'description', 'ingreedients'])));
console.log('sweet anywhere:', printNames(
  searchAnyField('sweet', ['name', 'description', 'ingreedients'])));

// How about AND search:
console.log('cake in name AND desc:', printNames(
  searchAllFields('cake', ['name', 'description'])));
console.log('cake everywhere:', printNames(
  searchAllFields('cake', ['name', 'description', 'ingreedients'])));



function printNames(recipes) {
  return recipes.map(r => r.name).join(', ');
}


Edit: You also said you have some nested props and whatnot. Here are more examples of how you could go about it.

const FIELDS = {
  name: {
    type: 'string',
    path: 'name',
  },
  description: {
    type: 'string',
    path: 'name',
  },
  ingreedients: {
    type: 'array',
    path: 'name',
  },
  price: {
    type: 'nested',
    path: 'details.price',
    nestedType: 'number',
  }
}

// Disclaimer: these recipes are made up
const recipes = [{
    name: 'lemon cake',
    description: 'a delicious cake',
    ingreedients: ['lemon', 'flour', 'sugar'],
    details: {
      price: 45,
    }
  },
  {
    name: 'sour lemon cake',
    description: 'a delicious cake',
    ingreedients: ['lemon', 'flour', 'not sugar'],
    details: {
      price: 45,
    }
  },
  {
    name: 'choco brownie',
    description: 'a sweet chocolate desert',
    ingreedients: ['chocolate', 'milk', 'flour', 'salt', 'sugar'],
    details: {
      price: 42,
    },
  },
  {
    name: 'vanilla croissant',
    description: 'a yummy pastry with vanilla flavour',
    ingreedients: ['vanilla', 'milk', 'flour'],
    details: {
      price: 45,
    },
  }
];

// To search, we need the search term, and an array of fields by which to search
// We return ANY match, meaning if the search term is in any of the fields, it's a match
function searchAnyField(searchTerm, searchFields) {
  return recipes.filter(item => {
    for (let field of searchFields) {
      switch (field.type) {
        case 'string':
          if (item[field.path].indexOf(searchTerm) > -1) {
            return true;
          }
        case 'array':
          if (item[field.path].includes(searchTerm) > -1) {
            return true;
          }
        case 'nested':
          const props = field.path.split('.').reverse();
          let prop = props.pop();
          let val = item[prop];
          while (val && props.length > 0) {
            prop = props.pop();
            val = val[prop]
          }
          if (field.nestedType === 'string') {
            if (val && val.indexOf(searchTerm) > -1) {
              return true;
            }
          } else if (field.nestedType === 'number') {
            return val == searchTerm;
          }
      }
    }
  });
  return false;
}


// Let's see it in action
// we'lll just print out the names of found items
console.log('42 anywhere:', printNames(
  searchAnyField(42, [ FIELDS.price])));
  
console.log('42 anywhere:', printNames(
  searchAnyField(42, [ FIELDS.price, FIELDS.name])));




function printNames(recipes) {
  return recipes.map(r => r.name).join(', ');
}

Upvotes: 0

Shilly
Shilly

Reputation: 8589

You can put all the attributes in an array and then use .some() to check if any of the attributes matches.

const recipes = [
  { name: "pizza", ingredients: "dough tomato mince", category: "meal" },
  { name: "pie", ingredients: "dough sugar", category: "dessert" },
  { name: "stew", ingredients: "potato mince onoin", category: "meal" },
  { name: "donut", ingredients: "sugar", category: "dessert" }
];
// Get these from the checkboxes:
const selected_attributes = [
  "name",
  "ingredients"
];
// Get this from the searchbar:
const seachbar_query = "do";
// Filter all recipes where the name or ingredients contains "do".
// We expect "pizza" and "pie', which contain dough in their ingredients.
// And we also expect "donuts", whose name starts with "do"
const filtered = recipes.filter( recipe => {
  return selected_attributes.some( attribute => {
    return recipe[ attribute ].includes( seachbar_query );
  });
});

console.log( filtered );

Upvotes: 1

write a function.

recipes.filter(e => {
  if (whatever) return true;
  else {
    // additional checks.
  }
});

in your case, I guess something like this:

recipes.filter(e => {
  if (ingredientsAreChecked()) {
    return e.name.matchesSearchString() || e.ingredients.some(i => i.matchesSearchString());
  }
  else {
    return e.name.matchesSearchString();
  }
});

or if ingredientsAreChecked() is not a computationally light thing to do, then do something like this:

if (ingredientsAreChecked()) return recipes.filter(e => ...);
else return recipes.filter(e => ...);

Upvotes: 0

Phillip
Phillip

Reputation: 6253

filter is a higher order function that takes a predicate function that either returns true or false depending on the current item should be kept or not. In your case the function is a single one-liner, but any kind of function could be passed.

So if you have complex logic, I suggest that you move it out into a named function, where you can check for several conditions, and finally return a single boolean value:

function isRecipeValid(recipe) {
  if (recipe.something) return false;
  // any arbitrary conditional checking here
  return recipe.conditionOne && recipe.conditionTwo;
}

recipes.filter(isRecipeValid);

Upvotes: 0

Related Questions