searchreactjsreact-nativefilteringreact-native-listview

Search and filtering with React Native


I am trying to implement a search and filtering bar with react native, but not quite sure how to work with the DataSource object. The data is in JSON form and it should do the following:

Here is a very simple example on RNPlay (Link).

How to implement the search and filtering function(s) inside react native?

import React, {Component} from 'react';
import { AppRegistry, View, ListView, Text, TextInput, StyleSheet, TouchableOpacity } from 'react-native';

const FILTERS = [
        {
          tag: "clever", active: false
        }, {
          tag: "scary", active: false
        }, {
          tag: "friendly", active: false
        }, {
          tag: "obedient", active: false
        }
      ];

const FIELDS = [
        {
          title:"Dog",
          subtitle: "Bulldog",
          tags: [ { tag: "clever" }, { tag: "scary" } ]
        }, {
          title:"Cat",
          subtitle:"Persian cat",
          tags: [ { tag: "friendly" }, { tag: "obedient" } ]
        }, {
          title:"Dog",
          subtitle:"Poodle",
          tags: [ { tag: "obedient" } ]
        }
      ];

class SampleApp extends Component {

  constructor(props) {
    super(props);
    var ds = new ListView.DataSource({
        rowHasChanged: (row1, row2) => row1 !== row2,
    });
    var ds2 = new ListView.DataSource({
        rowHasChanged: (row1, row2) => row1.active !== row2.active,
    });
    this.state = {
      dataSource: ds.cloneWithRows(FIELDS),
      dataSource2: ds2.cloneWithRows(FILTERS),
      filters: FILTERS,
    };
  }

  renderFilter(filter) {
    return (
        <TouchableOpacity onPress={this.handleClick.bind(this, filter)}>
            <Text style={{fontSize: 24, backgroundColor:(filter.active)?'red':'grey', margin:5}}>{filter.tag}</Text>
      </TouchableOpacity>
    ); 
  }

  renderField(field) {
    return (
      <View style={{flexDirection:'column', borderWidth: 3, borderColor: 'yellow'}}>
        <Text style={{fontSize: 24}}>{field.title}</Text>
        <Text style={{fontSize: 24}}>{field.subtitle}</Text>
        {field.tags.map((tagField) => {
          return (
            <View style={{backgroundColor:'blue'}}>
              <Text style={{fontSize: 24}}>{tagField.tag}</Text>
            </View>
          );
        })}
      </View>
    );
  }

  handleClick(filter) {
    const newFilters = this.state.filters.map(a => {
      let copyA = {...a};
      if (copyA.tag === filter.tag) {
        copyA.active = !filter.active;
      }
      return copyA;
    });
    this.setState({
      dataSource2: this.state.dataSource2.cloneWithRows(newFilters),
      filters: newFilters
    }); 
  }

  setSearchText(event) {
   let searchText = event.nativeEvent.text;
   this.setState({searchText});
  }

  render() {
    return (
      <View>
        <TextInput
                    style={styles.searchBar}
                    value={this.state.searchText}
                    onChange={this.setSearchText.bind(this)}
                    placeholder="Search" />
        <ListView
          style={{flexDirection:'row', flex:1, flexWrap:'wrap'}}
          horizontal={true}
          dataSource={this.state.dataSource2}
          renderRow={this.renderFilter.bind(this)}
        />
        <ListView
          dataSource={this.state.dataSource}
          renderRow={this.renderField.bind(this)}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  searchBar: {
    marginTop: 30,
    fontSize: 40,
    height: 50,
    flex: .1,
    borderWidth: 3,
    borderColor: 'red',
  },
});

AppRegistry.registerComponent('SampleApp', () => SampleApp);

Solution

  • Here is the solution, it does both search and filtering in one function. Suggestions on how to improve it are welcome.

    RNPlay (Link)

    import React, {Component} from 'react';
    import { AppRegistry, View, ListView, Text, TextInput, StyleSheet, TouchableOpacity } from 'react-native';
    
    const FILTERS = [
            {
              tag: "clever", active: false
            }, {
              tag: "scary", active: false
            }, {
              tag: "friendly", active: false
            }, {
              tag: "obedient", active: false
            }
          ];
    
    const FIELDS = [
            {
              title:"Dog",
              subtitle: "Bulldog",
              tags: [ "clever", "scary" ],
              active: true,
            }, {
              title:"Cat",
              subtitle:"Persian cat",
              tags: [ "friendly", "obedient" ],
              active: true,
            }, {
              title:"Dog",
              subtitle:"Poodle",
              tags: [ "obedient" ],
              active: true,
            }
          ];
    
    class SampleApp extends Component {
    
      constructor(props) {
        super(props);
        var ds = new ListView.DataSource({
            rowHasChanged: (row1, row2) => row1 !== row2,
        });
        var ds2 = new ListView.DataSource({
            rowHasChanged: (row1, row2) => row1.active !== row2.active,
        });
        this.state = {
          dataSource: ds.cloneWithRows(FIELDS),
          dataSource2: ds2.cloneWithRows(FILTERS),
          filters: FILTERS,
        };
      }
    
      renderFilter(filter) {
        return (
            <TouchableOpacity onPress={this.handleFilterClick.bind(this, filter)}>
                <Text style={{fontSize: 24, backgroundColor:(filter.active)?'red':'grey', margin:5}}>{filter.tag}</Text>
          </TouchableOpacity>
        ); 
      }
    
      renderField(field) {
    
        var fieldElement = <View style={{flexDirection:'column', borderWidth: 3, borderColor: 'yellow'}}>
            <Text style={{fontSize: 24}}>{field.title}</Text>
            <Text style={{fontSize: 24}}>{field.subtitle}</Text>
            {field.tags.map((tagField) => {
              return (
                <View style={{backgroundColor:'blue'}}>
                  <Text style={{fontSize: 24}}>{tagField}</Text>
                </View>
              );
            })}
          </View>
    
        if (field.active == true) {
          return fieldElement;
        } else {
            return null; 
        }
      }
    
      handleFilterClick(filter) {
        const newFilters = this.state.filters.map(f => {
          let copyF = {...f};
          if (copyF.tag === filter.tag) {
            copyF.active = !filter.active;
          }
          return copyF;
        });
        this.setState({
          dataSource2: this.state.dataSource2.cloneWithRows(newFilters),
          filters: newFilters
        });
        this.searchAndFilter();
      }
    
      setSearchText(event) {
        let searchText = event.nativeEvent.text;
        this.setState({
          searchText,
        });
        this.searchAndFilter();
      }
    
      searchAndFilter() {
        //Get filtered tags
        var filteredTags = [];
    
        this.state.filters.forEach((filter) => {
          if (filter.active) {
            filteredTags.push(filter.tag);
          }
        });
    
        const searchResults = FIELDS.map(f => {
          let copyF = {...f};
    
          //Filter
          if (filteredTags.length !== intersect_safe(filteredTags, copyF.tags).length) {
            copyF.active = false;
            return copyF;
          }
    
          //Search
          if (!this.state.searchText || this.state.searchText == '') {
            copyF.active = true;
          } else if (copyF.title.indexOf(this.state.searchText) != -1) {
            copyF.active = true;
          } else if (copyF.subtitle.indexOf(this.state.searchText) != -1) {
            copyF.active = true;
          } else {
            copyF.active = false;
          }
          return copyF;
        });
    
        this.setState({
          dataSource: this.state.dataSource.cloneWithRows(searchResults),
        });
      }
    
      render() {
        return (
          <View>
            <TextInput
                        style={styles.searchBar}
                        value={this.state.searchText}
                        onChange={this.setSearchText.bind(this)}
                        placeholder="Search" />
            <ListView
              style={{flexDirection:'row', flex:1, flexWrap:'wrap'}}
              horizontal={true}
              dataSource={this.state.dataSource2}
              renderRow={this.renderFilter.bind(this)}
            />
            <ListView
              dataSource={this.state.dataSource}
              renderRow={this.renderField.bind(this)}
            />
          </View>
        );
      }
    }
    
    function intersect_safe(a, b)
    {
      var ai=0, bi=0;
      var result = [];
    
      while( ai < a.length && bi < b.length )
      {
         if      (a[ai] < b[bi] ){ ai++; }
         else if (a[ai] > b[bi] ){ bi++; }
         else /* they're equal */
         {
           result.push(a[ai]);
           ai++;
           bi++;
         }
      }
    
      return result;
    }
    
    const styles = StyleSheet.create({
      searchBar: {
        marginTop: 30,
        fontSize: 40,
        height: 50,
        flex: .1,
        borderWidth: 3,
        borderColor: 'red',
      },
    });
    
    AppRegistry.registerComponent('SampleApp', () => SampleApp);