Question

How to search for multiple strings with AND using JS filter in reactable?

My question is the following. I am trying to create a reactable with R, and I basically want the search bar to be able to global search for multiple strings separated by a whitespace; so for example, I want to be able to insert in the search bar the following: "FOO BAR", and be able to get all the rows that contains (in every order, and in every column) both FOO, and BAR; the terms does not need to be in the same column, but need to be in the same row.

I am struggling to do so; I am following several examples online and found this online: https://github.com/glin/reactable/issues/222 in which a user propose a method (see code below from that question on github) that actually works well concatenating different string with an OR.

library(reactable)
data <- as.data.frame(datasets::Titanic)
myFilterMethod <- JS('function(rows, columnIds, filterValue) {
  /*
    pattern defines a RegEx text based on the users input. 
    In this users input,  all occurences of spacebar are replaced by an OR sign. 
    This is done so that if at least one his keywords is true, the row is displayed
    However, if the expression ends with an OR sign, the OR sign should be removed. 
    If this is not done,  then expressions ending in a spacebar will end in an OR sign 
    and will always give true. This is what the second replace does.
  */
      
  const pattern = new RegExp(filterValue.replace(/ /g, "|").replace(/\\|$/, ""))
  return rows.filter(function(row) {
    return columnIds.some(function(columnId) {
      return pattern.test(row.values[columnId]) 
      // Use the pattern defined above to test the rows 
      // and return those that pass the patternn
    })
  })
}')

reactable(
  data, searchable = TRUE, 
  searchMethod = myFilterMethod)

How can I create a similar thing but concatenating string with AND instead that with OR?

 4  81  4
1 Jan 1970

Solution

 1

The following idea should work:

  1. Split your search term by space ( ) into tokens
  2. Start with the full set of rows and filter it to those rows containing the first token.
  3. On this filtered set of rows apply a filter with the next token until you processed all tokens.

The Javascript function reduce is your friend here:

library(reactable)
data <- as.data.frame(datasets::Titanic)

searchForMultipleStrings  <- JS("
function(rows, columnIds, filterValue) {
  // tokens is an array of all search words
  const tokens = filterValue.split(' ');
  const result = tokens.reduce(
    function (filtered_rows, token) {
      return filtered_rows.filter(function (row) {
        // need to transform object to array
        const vals = Object.keys(row.values).map((key) => row.values[key]);
        // for each entry in the row look if there is at least one single match 
        return vals.some((value) => value == token)
      });
    },
    rows
  )
  return result;
}
")

reactable(
  data, searchable = TRUE, 
  searchMethod = searchForMultipleStrings)

A reactable with a search filed. In this search field the terms "Male" and "Adult" are entered and the table is filtere accordingly. The the terms "Adult" and "Female" are entered and again the table is filtered accordingly.

N.B. I searched for exact matches here (==), if you want some flexibility to use some sort of partial matching, you can incorporate regex in the .some() function.


Update

To allow for a case insensitive 'live' search, adapt the code as follows:

searchForMultipleStrings  <- JS("
function(rows, columnIds, filterValue) {
  // tokens is an array of all search words
  const tokens = filterValue.split(' ');
  const result = tokens.reduce(
    function (filtered_rows, token) {
      return filtered_rows.filter(function (row) {
        // need to transform object to array
        const vals = Object.keys(row.values).map((key) => row.values[key]);
        // for each entry in the row look if there is at least one single match 
        const re = new RegExp('^' + token + '\\S*', 'i');
        return vals.some((value) => re.test(value))
      });
    },
    rows
  )
  return result;
}
")

This allows for partial matching from the beginning of the string. That is a search string of ma ch y 3 will match Male | Child | Yes | 3rd. It is case insensitive. Depending on your final needs, you may need to further adapt the regular expression.

2024-07-01
thothal

Solution

 1

An approach similar to your given one using regex searching: An array of the filter values is defined. Then we loop over this array for getting the boolean values which indicates matching in some column and finally every() is used in order to combine it into one boolean.

library(reactable)

myFilterMethod <- JS('
function(rows, columnIds, filterValue) {
    let arrFilterValues = filterValue.split(" ");

    return rows.filter(function(row) {
        return arrFilterValues.map(function(e) {
            return columnIds.some(function(columnId) {
                return new RegExp(e).test(row.values[columnId])
            });
        }).every(v => v === true);
    });
}')

reactable(
  as.data.frame(datasets::Titanic), searchable=TRUE, 
  searchMethod=myFilterMethod)

enter image description here

2024-07-01
Jan