< back home

Creating a search context

Searching for user input in a dataset

This article was originally published on #dev-a-day.

There are a handful of tasks common to nearly all front-end development projects - today we're going to look at filtering a dataset by user input. First off, let's take a look at our dataset:

const data = [
  { name: 'John Doe', job: 'Developer', office: '3A' },
  { name: 'Jane Doe', job: 'Designer', office: '5B' },
  { name: 'Foo Bar', job: 'Engineer', office: '7C' },
];

In our project, this dataset is displayed in a table with an input that allows users to search for the value of any field present on a row, something like

Name Job Office
John Doe Developer 3A
Jane Doe Designer 5B
Foo Bar Engineer 7C

If the user searches for 'Doe', then the table should filter accordingly:

Name Job Office
John Doe Developer 3A
Jane Doe Designer 5B

Implementation

There are a few ways you could go about implementing this, but let's look at a simple one. We will use Array.prototype.filter to return only the rows in which the user input matches some part of at least one field value. Here's our first attempt*:

function getRowsMatchingUserInput(input) {
  return data.filter(row => {
    const searchContext = `${row.name}${row.job}${row.office}`;
    return searchContext.indexOf(input) !== -1;
  });
}

Let's walk through that for a single row:

const searchContext = `${row.name}${row.job}${row.office}`;
// searchContext === 'JohnDoeDeveloper3A'

return searchContext.indexOf(input) !== -1;
// searchContext.indexOf('Doe') === 4
// return true

It works! What about uppercase vs. lowercase inputs? Let's make sure it matches regardless, since users generally prefer lowercase:

function getRowsMatchingUserInput(input) {
  return data.filter(row => {
    const searchContext = `${row.name}${row.job}${row.office}`.toLowerCase();
    return searchContext.indexOf(input.toLowerCase()) !== -1;
  });
}

Now that should work and be case-insensitive:

const searchContext = `${row.name}${row.job}${row.office}`.toLowerCase();
// searchContext === 'johndoedeveloper3a'

return searchContext.indexOf(input.toLowerCase()) !== -1;
// searchContext.indexOf('doe') === 4
// return true

Now we wouldn't be real developers if we didn't consider edge cases. Wikipedia defines edge cases as a situation that "involves input values that require special handling." Let's take the input 'bare' as an example. This shouldn't return anything since no user has a property that contains it, right?

Name Job Office
Foo Bar Engineer 7C

Wait, what? The table should be empty, but it isn't. Why is that? Let's step through our filter for this row:

const searchContext = `${row.name}${row.job}${row.office}`.toLowerCase();
// searchContext === 'foobarengineer7c'

return searchContext.indexOf(input.toLowerCase()) !== -1;
// searchContext.indexOf('bare') === 3
// return true

Oh! The input 'bare' is matching the end of 'Foo Bar' and the beginning of 'Engineer'! That's no good. How can we solve this? We need to add a delimiter between fields when constructing our search context, some character that the user can't type on their keyboard that will prevent this behavior. It just so happens that JavaScript has the perfect candidate - the null character! JS represents this with an escape character since we can't type it from a keyboard: '\0'. Let's rework our filter function:

function getRowsMatchingUserInput(input) {
  return data.filter(row => {
    const searchContext = `${row.name}\0${row.job}\0${row.office}`.toLowerCase();
    return searchContext.indexOf(input.toLowerCase()) !== -1;
  });
}

And it works!

Name Job Office
no rows found matching bare

Let's clean up our implementation using a helper function*:

function createSearchContext(...fields) {
  return fields.join('\0').toLowerCase();
}

function getRowsMatchingUserInput(input) {
  return data.filter(row => {
    const searchContext = createSearchContext(row.name, row.job, row.office);
    return searchContext.indexOf(input.toLowerCase()) !== -1;
  });
}

And there you have it, a clean user input search implementation in just 10 lines. Be sure to check back tomorrow, where we'll reduce our way to victory!

 

 

*NOTE: If you're confused by template literals like `${value}` or the parameter spread operator (eg - function unknownParameterCount(...params) {}), check out this cool ES6 reference by @melanieseltzer.