...
 
Commits (2)
node_modules
build
This diff is collapsed.
{
"name": "react-quickfilter",
"version": "0.0.1",
"description": "Quickly filter lists of objects in React",
"main": "index.mjs",
"browserlist": "modern",
"dependencies": {
"lunr": "^2.3.8",
"ramda": "^0.27.0"
},
"devDependencies": {
"@ava/babel": "^1.0.1",
"@babel/preset-react": "^7.10.4",
"@linguala/eslint-config": "^0.4.3",
"@snowpack/app-scripts-react": "^1.6.0-alpha.0",
"ava": "^3.10.1",
"esbuild": "^0.6.5",
"eslint": "^7.5.0",
"faker": "^4.1.0",
"husky": "^4.2.5",
"prettier": "^2.0.5",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"snowpack": "^2.6.4"
},
"peerDependencies": {
"react": "16.x",
"react-dom": "16.x"
},
"scripts": {
"start": "snowpack dev",
"build": "snowpack build",
"lint": "eslint --ext js,mjs .",
"test": "ava"
},
"repository": {
"type": "git",
"url": "ssh://git@gitlab.linguala.com:1022/linguala/react-quickfilter.git"
},
"keywords": [
"react",
"quickfilter",
"lunr",
"search"
],
"ava": {
"babel": true
},
"babel": {
"presets": [
"@babel/preset-react"
]
},
"eslintConfig": {
"extends": [
"@linguala/eslint-config"
]
},
"prettier": {
"arrowParens": "avoid",
"semi": false,
"singleQuote": true,
"trailingComma": "es5"
},
"author": "Tobias Thüring <t.thuering@linguala.com>",
"license": "ISC"
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Web site created using create-snowpack-app" />
<title>Snowpack App</title>
</head>
<body>
<div id="root"></div>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script type="module" src="/_dist_/index.js"></script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
{
"extends": "@snowpack/app-scripts-react",
"scripts": {
"build:mjs": "esbuild"
},
"plugins": []
}
.App {
text-align: center;
font-weight: 300;
font-size: 0.9em;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
import React, { useState } from 'react'
import faker from 'faker'
import './App.css'
import { QuickFilter } from './QuickFilter.jsx'
const FieldName = 'name'
const entries = Array.from({ length: 10000 }, () => ({
[FieldName]: faker.name.findName(),
}))
function App() {
const [searchTerm, updateSearchTerm] = useState('')
return (
<div className="App">
<header className="App-header">
<label htmlFor={'searchByName'}>Search by Name</label>
<input
id="searchByName"
type="text"
onChange={e => updateSearchTerm(e.target.value)}
/>
<p>searching with Search term: &ldquo;{searchTerm}&rdquo;</p>
<section>
<h2>search results</h2>
<QuickFilter
searchTerm={searchTerm}
results={entries}
indexFields={[FieldName]}
>
{entries => entries.map(({ name }, i) => <div key={i}>{name}</div>)}
</QuickFilter>
</section>
</header>
</div>
)
}
export default App
import * as React from 'react'
import { render } from '@testing-library/react'
import App from './App.jsx'
test('renders learn react link', () => {
const { getByText } = render(<App />)
const linkElement = getByText(/learn react/i)
expect(linkElement).toBeInTheDocument()
})
import React, { useMemo } from 'react'
// import { timer } from '../packages/time/timer.js'
import { filterData, indexData, search } from './lib/index.mjs'
const doIndex = (indexFields, results) =>
indexData({ fields: indexFields })(results)
// TODO:
// Debounce filtering of profiles or
// Debounce Search: https://stackoverflow.com/questions/23123138/perform-debounce-in-react-js
const Search = ({ children, index, searchTerm = '' }) => {
// const searchPerf = timer()
const resIds = useMemo(() => search(index)(searchTerm), [index, searchTerm])
// const searchPerfDiff = searchPerf()
/* eslint-disable-next-line no-console */
// console.debug(` search: ${searchPerfDiff}`)
return <>{children(resIds)}</>
}
const Filter = ({ children, searchTerm, resIds, results }) => {
// State handlers for filtering profiles shown in Profiles component
// const searchResPerf = timer()
const filteredResults = useMemo(
() => filterData(resIds, searchTerm)(results),
[results, resIds, searchTerm]
)
// const searchResPerfDiff = searchResPerf()
/* eslint-disable-next-line no-console */
// console.debug(` result: ${searchResPerfDiff}`)
return <>{children(filteredResults)}</>
}
export const QuickFilter = React.memo(
({ children, searchTerm = '', results = [], indexFields = ['name'] }) => {
const index = doIndex(indexFields, results)
return (
<Search index={index} searchTerm={searchTerm}>
{resIds => (
<Filter resIds={resIds} searchTerm={searchTerm} results={results}>
{results => children(results)}
</Filter>
)}
</Search>
)
}
)
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App.jsx'
import './index.css'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
// Learn more: https://www.snowpack.dev/#hot-module-replacement
if (import.meta.hot) {
import.meta.hot.accept()
}
import * as R from 'ramda'
const { identity, memoizeWith } = R
export const filterData = memoizeWith(
identity,
(searchResultIds, searchTerm) => data =>
searchTerm.length > 0 ? searchResultIds.map(id => data[id]) : data
)
export { default as search } from './search.mjs'
export * from './filterData.mjs'
export { default as indexData } from './indexData.mjs'
import lunr from 'lunr'
const indexData = ({ fields }) => {
return data => {
const idx = lunr(function () {
const that = this
fields.map(f => {
that.ref('__refid')
that.field(f)
})
data.length >= 0 &&
data.map((entry, id) =>
that.add({ __refid: parseInt(id, 10), ...entry })
)
})
return idx
}
}
export default indexData
import {
pipe as _,
__,
always,
concat,
flatten,
identity,
ifElse,
map,
memoizeWith,
prop,
split,
// tap,
test,
tryCatch,
} from 'ramda'
export const genQueries = memoizeWith(identity, q =>
_(
// tap(console.debug),
ifElse(
test(/\s/, __), // if we have whitespace we have more than one search term
_(
split(' '),
map(v => map(concat(v), ['*', '~1'])),
flatten
),
_(
ifElse(
// if there is no whitespace
test(/^$/, __),
() => ['*'],
() => [`*`, `~1`]
),
map(concat(q))
// tap(console.debug)
)
)
)(q)
)
const search = lunrIndex =>
tryCatch(
_(
// tap(console.log),
genQueries,
// tap(console.log),
map(s => lunrIndex.search(s)),
flatten,
// uniqBy(prop('ref')),
map(_(prop('ref'), v => parseInt(v, 10)))
),
_(prop('message'), /* tap(console.warn), */ always([]))
)
export default search
import test from 'ava'
import indexData from '../src/lib/indexData.mjs'
const data = [
{
a: 'abc',
},
{
a: 'bcd',
},
]
const index = indexData({ fields: ['a'] })(data)
test('empty string search does give back all', t => {
const all = index.search('')
t.not(all.length, 0)
t.is(all.length, 2)
})
test('abc is found once', t => {
const abc = index.search('abc')
t.is(abc.length, 1)
})
test('bcd is found once', t => {
const bcd = index.search('bcd')
t.is(bcd.length, 1)
})
test('bc is not found', t => {
const bc = index.search('bc')
t.is(bc.length, 0)
})
test('*bc* is found twice', t => {
const bc = index.search('*bc*')
t.is(bc.length, 2)
})
import test from 'ava'
import { genQueries } from '../src/lib/search.mjs'
test('two words are assembled as separate search terms', t => {
const two = genQueries('abc cde')
t.deepEqual(two, ['abc*', 'abc~1', 'cde*', 'cde~1'])
})