├── tests ├── .eslintrc ├── mock.js └── index-test.js ├── .gitignore ├── nwb.config.js ├── .travis.yml ├── src ├── index.js └── filter.js ├── LICENSE ├── package.json ├── CONTRIBUTING.md ├── demo └── src │ └── index.js └── README.md /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | -------------------------------------------------------------------------------- /nwb.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | type: 'react-component', 3 | npm: { 4 | esModules: true, 5 | umd: { 6 | global: 'ReactSearchFilter', 7 | externals: { 8 | react: 'React' 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 8 6 | 7 | before_install: 8 | - npm install codecov.io coveralls 9 | 10 | after_success: 11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /tests/mock.js: -------------------------------------------------------------------------------- 1 | export const data = [ 2 | { 3 | name: 'Joe', 4 | email: 'joe@joe.joejoe', 5 | company: { 6 | name: 'Cool industries', 7 | title: 'King of cool', 8 | id: 20283 9 | } 10 | }, 11 | { 12 | name: 'Jane', 13 | email: 'jane@jane.janejane', 14 | company: { 15 | name: 'Awesome industries', 16 | title: 'Queen of awesome', 17 | id: 30394 18 | } 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { filter } from './filter'; 4 | 5 | export default class FilterResults extends Component { 6 | render() { 7 | const { value, data, pick } = this.props; 8 | return this.props.renderResults(filter(value, data, pick)); 9 | } 10 | } 11 | 12 | FilterResults.propTypes = { 13 | value: PropTypes.string.isRequired, 14 | data: PropTypes.arrayOf(PropTypes.object).isRequired, 15 | renderResults: PropTypes.func.isRequired, 16 | pick: PropTypes.arrayOf(PropTypes.string) 17 | }; 18 | -------------------------------------------------------------------------------- /src/filter.js: -------------------------------------------------------------------------------- 1 | import pick from 'pick-deep' 2 | 3 | function strOp(str) { 4 | return str 5 | .toString() 6 | .replace(/\s/g, '') 7 | .toLowerCase(); 8 | } 9 | 10 | function objectValues(value, pickAttr) { 11 | return (pickAttr ? Object.values(pick(value, pickAttr)) : Object.values(value)).reduce((string, val) => { 12 | const test = val !== null && val !== undefined; 13 | return ( 14 | string + 15 | (typeof val === 'object' && val !== null 16 | ? strOp(objectValues(val)) 17 | : test ? strOp(val) : '') 18 | ); 19 | }, ''); 20 | } 21 | 22 | export function filter(val, data, pick) { 23 | return data.filter(el => { 24 | return !!val.length ? objectValues(el, pick).includes(strOp(val)) : true; 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joe Dodd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-filter-search", 3 | "version": "1.1.11", 4 | "description": "React Filter Search is a React component for filtering client-side data rendered to your UI.", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "files": [ 8 | "css", 9 | "es", 10 | "lib", 11 | "umd" 12 | ], 13 | "scripts": { 14 | "build": "nwb build-react-component", 15 | "clean": "nwb clean-module && nwb clean-demo", 16 | "prepublishOnly": "npm run build", 17 | "start": "nwb serve-react-demo", 18 | "test": "nwb test-react", 19 | "test:coverage": "nwb test-react --coverage", 20 | "test:watch": "nwb test-react --server" 21 | }, 22 | "dependencies": { 23 | "pick-deep": "^1.0.0" 24 | }, 25 | "peerDependencies": { 26 | "react": ">=16" 27 | }, 28 | "devDependencies": { 29 | "nwb": "0.23.x", 30 | "prop-types": "^15.7.2", 31 | "react": "^16.12.0", 32 | "react-dom": "^16.12.0", 33 | "whatwg-fetch": "^3.0.0" 34 | }, 35 | "author": "Joe Dodd", 36 | "license": "MIT", 37 | "repository": "https://github.com/joehdodd/react-filter-search", 38 | "homepage": "https://github.com/joehdodd/react-filter-search", 39 | "bugs": "https://github.com/joehdodd/react-filter-search/issues", 40 | "keywords": [ 41 | "react-component", 42 | "react-search", 43 | "react-filter", 44 | "filter", 45 | "search", 46 | "ui-filter", 47 | "ui-search" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | First, please fork this repo. 4 | [Node.js](http://nodejs.org/) >= 6 must be installed. 5 | 6 | ## Installation 7 | 8 | - Running `npm install` in the component's root directory will install everything you need for development. 9 | 10 | ## Demo Development Server 11 | 12 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 13 | 14 | ## Running Tests 15 | 16 | - `npm test` will run the tests once. 17 | 18 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 19 | 20 | - `npm run test:watch` will run the tests on every change. 21 | 22 | ## Building 23 | 24 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 25 | 26 | - `npm run clean` will delete built resources. 27 | 28 | ## Making and Sumibtting Changes 29 | 30 | Uisng your fork, ensure you've properly set `git remote` to point back to **this** repo as "upstream", or whatever you want to call it. Read [GitHub's guide on forking](https://help.github.com/articles/fork-a-repo/) for more info. 31 | 32 | Once you have that set up, read [GitHub's guide on keeping a fork in sync](https://help.github.com/articles/syncing-a-fork/). 33 | 34 | If you've completed that, please create a branch off of the latest version of `master` using [this Git workflow](https://github.com/Automattic/wp-calypso/blob/master/docs/git-workflow.md) 35 | 36 | When you're ready, open a PR from your fork to this repo to get the party started. 37 | 38 | Thanks and have fun! 39 | -------------------------------------------------------------------------------- /tests/index-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import React from 'react'; 3 | import { render, unmountComponentAtNode } from 'react-dom'; 4 | import FilterResults from 'src/'; 5 | // import { data } from './mock'; 6 | import 'whatwg-fetch'; 7 | 8 | describe('FilterResults', () => { 9 | let node; 10 | let data; 11 | beforeEach(async () => { 12 | node = document.createElement('div'); 13 | data = await fetch('https://www.reddit.com/r/pics.json') 14 | .then(response => response.json()) 15 | .then(json => json.data.children); 16 | }); 17 | 18 | afterEach(() => { 19 | unmountComponentAtNode(node); 20 | }); 21 | 22 | it('renders a filtered value without crashing', () => { 23 | render( 24 | 28 | results.map(({ data }, i) => {data.ups}) 29 | } 30 | />, 31 | node, 32 | () => { 33 | expect(node); 34 | } 35 | ); 36 | }); 37 | }), 38 | describe('AllResults', () => { 39 | let node; 40 | let data; 41 | beforeEach(async () => { 42 | node = document.createElement('div'); 43 | data = await fetch('https://www.reddit.com/r/pics.json') 44 | .then(response => response.json()) 45 | .then(json => json.data.children); 46 | }); 47 | 48 | afterEach(() => { 49 | unmountComponentAtNode(node); 50 | }); 51 | 52 | it('renders all values without crashing', () => { 53 | render( 54 | 58 | results.map(({ data }, i) => {data.ups}) 59 | } 60 | />, 61 | node, 62 | () => { 63 | expect(node); 64 | } 65 | ); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import SearchResults from '../../src'; 5 | 6 | class Demo extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | data: [], 11 | value: '' 12 | }; 13 | } 14 | componentWillMount() { 15 | fetch('https://www.reddit.com/r/pics.json') 16 | .then(response => response.json()) 17 | .then(json => this.setState({ data: json.data.children })); 18 | } 19 | handleChange = event => { 20 | const { value } = event.target; 21 | this.setState({ value }); 22 | }; 23 | render() { 24 | const { data, value } = this.state; 25 | return ( 26 |
27 | 28 |
36 | ( 40 |
41 | {results.map(({ data }, i) => ( 42 |
53 | {data.title} 54 | {data.title}/ 55 |
56 | ))} 57 |
58 | )} 59 | /> 60 |
61 |             {JSON.stringify(data, null, 2)}
62 |           
63 |
64 |
65 | ); 66 | } 67 | } 68 | 69 | render(, document.querySelector('#demo')); 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Filter Search 🔍 2 | 3 | [![Travis][build-badge]][build] 4 | [![npm package][npm-badge]][npm] 5 | [![Coveralls][coveralls-badge]][coveralls] 6 | 7 | This is a small, unobtrusive React component for filtering client-side application data. 8 | 9 | [build-badge]: https://img.shields.io/travis/joehdodd/react-filter-search/master.png?style=flat-square 10 | [build]: https://travis-ci.org/joehdodd/react-filter-search 11 | 12 | [npm-badge]: https://img.shields.io/npm/v/react-filter-search.png?style=flat-square 13 | [npm]: https://www.npmjs.org/package/react-filter-search 14 | 15 | [coveralls-badge]: https://img.shields.io/coveralls/joehdodd/react-filter-search/master.png?style=flat-square 16 | [coveralls]: https://coveralls.io/github/joehdodd/react-filter-search 17 | 18 | ## Installation 19 | 20 | `npm i react-filter-search` 21 | 22 | `yarn add react-filter-search` 23 | 24 | ## Usage 25 | 26 | React Filter Search is simply a component that requires data in application state (needs to be an `array` of `object`s and an input value. In turn, you'll get back... 27 | 28 | * filtered data based on user input 29 | * all data in absence of any search input 30 | 31 | This data flows back up in the form of `renderResults`, which is a render prop that returns one of the above. So you'll be responsible for setting up passing in data and an input value. 32 | 33 | In this way, React Filter Search is unopinionated about how you store your data and how you handle user input in your application. 🎉 34 | 35 | 36 | 37 | ```javascript 38 | // 39 | /*-Other Imports-*/ 40 | // 41 | import FilterResults from 'react-filter-search'; 42 | 43 | class App extends Component { 44 | constructor(props) { 45 | super(props); 46 | this.state = { 47 | data: [], 48 | value: '' 49 | }; 50 | } 51 | componentWillMount() { 52 | fetch('https://jsonplaceholder.typicode.com/users') 53 | .then(response => response.json()) 54 | .then(json => this.setState({ data: json })); 55 | } 56 | handleChange = event => { 57 | const { value } = event.target; 58 | this.setState({ value }); 59 | }; 60 | render() { 61 | const { data, value } = this.state; 62 | return ( 63 |
64 | 65 | ( 69 |
70 | {results.map(el => ( 71 |
72 | {el.name} 73 | {el.email} 74 |
75 | ))} 76 |
77 | )} 78 | /> 79 |
80 | ); 81 | } 82 | } 83 | ``` 84 | The magic 🧙happens in `renderResults`, which returns an array of objects. Your data has either been filtered based on user input, or not. 85 | 86 | Filtering logic will iterate over any level of nesting in your data structure. Which means a good suggestion for this is something like user data or todo items that aren't heavily nested at many levels. 87 | 88 | If you wish to filter only using certain attributes then you can use the optional `pick` prop. 89 | ```javascript 90 | // if each object is of the form 91 | var obj = { name: "Leanne Graham", username: "Bret", email: "Sincere@april.biz", company: {"name": "Romaguera-Crona"} } 92 | 97 | // your objects will be filtered only with the name and company.name fields 98 | // but you can still render other values like username and email 99 | ``` 100 | 101 | To render your data, simply use .map() to render to the view--the data retains in the same structure. Return some inline JSX, or feed each element into a stateless React component that renders some UI. 102 | 103 | ## `props` 104 | 105 | | name | type | required?| 106 | | ---------------- |----------------------| ---------| 107 | | `value` | `string` | `true` | 108 | | `data` | `array` of `object`s | `true` | 109 | | `renderResults` | `func` | `true` | 110 | | `pick` | `array` of `string`s | `false` | 111 | 112 | ## Contributions 113 | 114 | Read [`CONTRIBUTING.md`](https://github.com/joehdodd/react-filter-search/blob/master/CONTRIBUTING.md) and join the fun! 🎉 115 | --------------------------------------------------------------------------------