URL: https://codesandbox.io/s/62x4mmxr0n

Demo here. The demo uses React and a list of blog post titles that get immediately filtered when you type in a search. I.e. you have the whole list but show less when a search term is entered.

That the demo uses React isn't important. What's important is the search function. It looks like this:


function filterList(q, list) {
  function escapeRegExp(s) {
    return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
  }
  const words = q
    .split(/\s+/g)
    .map(s => s.trim())
    .filter(s => !!s);
  const hasTrailingSpace = q.endsWith(" ");
  const searchRegex = new RegExp(
    words
      .map((word, i) => {
        if (i + 1 === words.length && !hasTrailingSpace) {
          // The last word - ok with the word being "startswith"-like
          return `(?=.*\\b${escapeRegExp(word)})`;
        } else {
          // Not the last word - expect the whole word exactly
          return `(?=.*\\b${escapeRegExp(word)}\\b)`;
        }
      })
      .join("") + ".+",
    "gi"
  );
  return list.filter(item => {
    return searchRegex.test(item.title);
  });
}

In action
I use this in a single-page content management app. There's a list of records and a search input. Every character you put into the search bar updates the list of records shown.

What it does is that it allows you to search texts based on multiple whole words. But the key feature is that the last word doesn't have to be whole. For example, it will positively match "This is a blog post about JavaScript" if the search is "post javascript" or "post javasc". But it won't match on "pos blog".

The idea is that if a user has typed in a full word followed by a space, all previous words needs to be matched fully. For example if the input is "java " it won't match on "This is a blog post about JavaScript" because the word java, alone, isn't in the search text.

Sure, there are different ways to write this but I think this functionality is good for this kind of filtering search. A different implementation would have a function that returns the regex and then it can be used both for filtering and for highlighting.

Hope it helps.

Comments

Post your own comment
Chetan Bhatt

Hi, I wanted to implement something like this. Trying to understand your code, will really appreciate if you can explain the steps you chose.

Like using trim() after split(). I am not able to understand why did you add it.
Please specify the reason, thanks!

Ankan Adhikari [firehawk895]

here is a modified version that takes an additional parameter attribute_list which allows for matching deep within the object using dot notation

```
export function filterList(q, list, attribute_list) {
  /*
  q query string to search for
  list is the list of objects to search for
  attribute_list is the list of attributes of the object to match on, can use dot object notation
  */

  function escapeRegExp(s) {
    return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&";;
  }
  const words = q
    .split(/\s+/g)
    .map(s => s.trim())
    .filter(s => !!s);
  const hasTrailingSpace = q.endsWith(" ");
  const searchRegex = new RegExp(
    words
      .map((word, i) => {
        if (i + 1 === words.length && !hasTrailingSpace) {
          // The last word - ok with the word being "startswith"-like
          return `(?=.*\\b${escapeRegExp(word)})`;
        } else {
          // Not the last word - expect the whole word exactly
          return `(?=.*\\b${escapeRegExp(word)}\\b)`;
        }
      })
      .join("") + ".+",
    "gi"
  );
  return list.filter(item => {
    let result = false;
    attribute_list.forEach(attr => {
      // This beautiful stackoverflow answer converts dot notation into an object reference
      // https://stackoverflow.com/a/6394168/1881812
      // 'a.b.etc'.split('.').reduce((o,i)=>o[i], obj)
      result =
        result ||
        searchRegex.test(attr.split(".").reduce((o, i) => o[i], item));
    });
    return result;
  });
}
```

Neal

Do you have a license on this? Would be great if you could explicitly say if you're ok with this being copied!

Peter Bengtsson

Help yourself! If your code is open source, I think a code comment that is a URL back to this would be fair.

Eddie

You have a very serious and subtle error in your code. You incorrectly miss some results because RegExp.test doesn't reset the lastIndex of the regex. Thus after a successful match, it will start its next search at that index! You can see the bug in your demo by searching for "django." It doesn't return all the results it should. Here's the MDN documentation on RegExp.test which probably explains the issue better than I do: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test

Peter Bengtsson

I'm not surprised it's buggy :)
But what do you mean by searching for "django." in the demo? There's nothing to match for that string in all the titles.

Eddie

Hmm, maybe we are looking at different demos? The demo you linked in this blog post (https://codesandbox.io/s/62x4mmxr0n) should return 20 results when you type in "django" but only returns 18. The word "django" is in two of the first two titles.

Paul

I think the bug is that it's using the "global" flag which is stateful. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
Change flags to "i" and it behaves as expected.

Your email will never ever be published.

Related posts