diff --git a/README.md b/README.md index f4c5ca9..cb70a35 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,12 @@ class MyComponent extends Component { render() { const items = [ - { name: 'first', meta: 'first|123' }, - { name: 'second', meta: 'second|443' }, - { name: 'third', meta: 'third|623' }, + { name: 'first', meta: 'first|123', tag: 'a' }, + { name: 'second', meta: 'second|443', tag: 'b' }, + { name: 'third', meta: 'third|623', tag: 'a' }, ]; const fuseConfig = { - keys: ['meta'] + keys: ['meta', 'tag'] }; return (
@@ -76,7 +76,7 @@ An input field that controls the state used to render the items in `FilterResult ### onChange -`onChange` is an optional callback function that is called BEFORE the value in the input field changes via an `onchange` event. If it returns `false`, the new value will not be propagated to the shared state. (returning nothing or any other return will propagate the state). +`onChange` is an optional callback function that is called BEFORE the value in the input field changes via an `onchange` event. It can optionally return a string, which will then be passed directly to `FilterResults` rather than the original string. This can be used to filter out special inputs (eg: `author:jdlehman`) from fuzzy searching. These special inputs could then be used to change the `items` being passed to `FilterResults`. # FilterResults @@ -105,11 +105,6 @@ Collection of fuzzy filtered items (filtered by the `InputFilter`'s value), each `classPrefix` is a string that is used to prefix the class names in the component. It defaults to `react-fuzzy-filter`. (`react-fuzzy-filter__results-container`) - -### initialSearch - -`initialSearch` is a string that can override the initial search state when the component is created. It defaults to `''`. - ### wrapper `wrapper` is an optional component that will wrap the results if defined. This will be used as the wrapper around the items INSTEAD of `react-fuzzy-filter__results-container`. diff --git a/dist/react-fuzzy-filter.min.js b/dist/react-fuzzy-filter.min.js index a011454..7d8e26f 100644 --- a/dist/react-fuzzy-filter.min.js +++ b/dist/react-fuzzy-filter.min.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("react")):"function"==typeof define&&define.amd?define(["react"],e):"object"==typeof exports?exports.ReactFuzzyFilter=e(require("react")):t.ReactFuzzyFilter=e(t.React)}(this,function(t){return function(t){function e(n){if(r[n])return r[n].exports;var s=r[n]={exports:{},id:n,loaded:!1};return t[n].call(s.exports,s,s.exports,e),s.loaded=!0,s.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){"use strict";function n(t){return t&&t.__esModule?t:{"default":t}}function s(){var t=new i.Subject;return{InputFilter:(0,c["default"])(t),FilterResults:(0,a["default"])(t)}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=s;var i=r(13),o=r(9),c=n(o),u=r(8),a=n(u)},function(t,e,r){"use strict";var n=r(20),s=r(21),i=r(5),o=r(23),c=r(4),u=r(19),a=function(){function t(t){this.isUnsubscribed=!1,t&&(this._unsubscribe=t)}return t.prototype.unsubscribe=function(){var t,e=!1;if(!this.isUnsubscribed){this.isUnsubscribed=!0;var r=this,a=r._unsubscribe,p=r._subscriptions;if(this._subscriptions=null,i.isFunction(a)){var h=o.tryCatch(a).call(this);h===c.errorObject&&(e=!0,(t=t||[]).push(c.errorObject.e))}if(n.isArray(p))for(var f=-1,l=p.length;++f1)throw new Error("Key weight has to be > 0 and <= 1");t=t.name}else this._keyMap[t]={weight:1};this._analyze(t,i(p,t,[]),p,r)}},n.prototype._analyze=function(t,e,n,s){var o,c,u,a,p,h,f,l,b,y,d,v,m,_=this.options,w=!1;if(void 0!==e&&null!==e)if(c=[],"string"==typeof e){if(o=e.split(_.tokenSeparator),_.verbose&&r("---------\nKey:",t),_.verbose&&r("Record:",o),this.options.tokenize){for(v=0;v0){if(e={item:o.item},p.indexOf("matches")!==-1)for(n=o.output,e.matches=[],r=0;rP.maxPatternLength){if(v=t.match(new RegExp(this.pattern.replace(P.tokenSeparator,"|"))),m=!!v)for(w=[],e=0,g=v.length;e=p;r--)if(b=this.patternAlphabet[t.charAt(r-1)],b&&(_[r-1]=1),0===e?f[r]=(f[r+1]<<1|1)&b:f[r]=(f[r+1]<<1|1)&b|((l[r+1]|l[r])<<1|1)|l[r+1],f[r]&this.matchmask&&(y=this._bitapScore(e,r-1),y<=i)){if(i=y,o=r-1,d.push(o),!(o>s))break;p=Math.max(1,2*s-o)}if(this._bitapScore(e+1,s)>i)break;l=f}return w=this._getMatchedIndices(_),{isMatch:o>=0,score:0===y?.001:y,matchedIndices:w}},o.prototype._getMatchedIndices=function(t){for(var e,r=[],n=-1,s=-1,i=0,o=t.length;i1)throw new Error("Key weight has to be > 0 and <= 1");t=t.name}else this._keyMap[t]={weight:1};this._analyze(t,o(p,t,[]),p,r)}},n.prototype._analyze=function(t,e,n,s){var i,c,u,a,p,h,f,l,b,y,d,v,_,m=this.options,w=!1;if(void 0!==e&&null!==e)if(c=[],"string"==typeof e){if(i=e.split(m.tokenSeparator),m.verbose&&r("---------\nKey:",t),m.verbose&&r("Record:",i),this.options.tokenize){for(v=0;v0){if(e={item:i.item},p.indexOf("matches")!==-1)for(n=i.output,e.matches=[],r=0;rP.maxPatternLength){if(v=t.match(new RegExp(this.pattern.replace(P.tokenSeparator,"|"))),_=!!v)for(w=[],e=0,g=v.length;e=p;r--)if(b=this.patternAlphabet[t.charAt(r-1)],b&&(m[r-1]=1),0===e?f[r]=(f[r+1]<<1|1)&b:f[r]=(f[r+1]<<1|1)&b|((l[r+1]|l[r])<<1|1)|l[r+1],f[r]&this.matchmask&&(y=this._bitapScore(e,r-1),y<=o)){if(o=y,i=r-1,d.push(i),!(i>s))break;p=Math.max(1,2*s-i)}if(this._bitapScore(e+1,s)>o)break;l=f}return w=this._getMatchedIndices(m),{isMatch:i>=0,score:0===y?.001:y,matchedIndices:w}},i.prototype._getMatchedIndices=function(t){for(var e,r=[],n=-1,s=-1,o=0,i=t.length;o this.setState({search})); } - componentWillReceiveProps({items, fuseConfig}) { - this.setState({fuse: new Fuse(items, fuseConfig)}); - } - componentWillUnmount() { this.subscription.unsubscribe(); } - renderItems() { - let items; + filterItems() { if (!this.state.search || this.state.search.trim() === '') { - items = this.props.defaultAllItems ? this.props.items : []; + return this.props.defaultAllItems ? this.props.items : []; } else { - items = this.state.fuse.search(this.state.search); + const fuse = new Fuse(this.props.items, this.props.fuseConfig); + return fuse.search(this.state.search); } - return items.map((item, i) => this.props.renderItem(item, i)); + } + + renderItems() { + return this.filterItems().map((item, i) => this.props.renderItem(item, i)); } render() { diff --git a/src/InputFilter.js b/src/InputFilter.js index 22b5c69..45e2e29 100644 --- a/src/InputFilter.js +++ b/src/InputFilter.js @@ -17,16 +17,21 @@ export default function inputFilterFactory(store) { onChange: function() {} }; - state = { - search: this.props.initialSearch - }; + componentDidMount() { + let value = this.props.initialSearch; + const overrideValue = this.props.onChange(value); + if (typeof overrideValue === 'string') { + value = overrideValue; + } + store.next(value); + } handleChange = ({target: {value}}) => { - const continueChange = this.props.onChange(value); - if (continueChange !== false) { - this.setState({search: value}); - store.next(value); + const overrideValue = this.props.onChange(value); + if (typeof overrideValue === 'string') { + value = overrideValue; } + store.next(value); }; render() { diff --git a/src/index.js b/src/index.js index fedbbac..f4ae59c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,9 +1,9 @@ -import {Subject} from 'rxjs/Subject'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import inputFilterFactory from './InputFilter'; import filterResultsFactory from './FilterResults'; export default function fuzzyFilterFactory() { - const store = new Subject(); + const store = new BehaviorSubject(); return { InputFilter: inputFilterFactory(store), FilterResults: filterResultsFactory(store) diff --git a/test/FilterResults_test.js b/test/FilterResults_test.js index 49de4d1..c142892 100644 --- a/test/FilterResults_test.js +++ b/test/FilterResults_test.js @@ -162,17 +162,5 @@ describe('FilterResults', () => { expect(component.find('.my-item').length).toEqual(1); expect(component.find('.my-item').at(0).text()).toEqual('three'); }); - - it('can override the initial search value with initialSearch prop', () => { - const component = shallow( - - ); - expect(component.find('.my-item').length).toEqual(2); - }); }); }); diff --git a/test/InputFilter_test.js b/test/InputFilter_test.js index e19f646..955678a 100644 --- a/test/InputFilter_test.js +++ b/test/InputFilter_test.js @@ -6,8 +6,9 @@ import inputFilterFactory from '../src/InputFilter'; describe('InputFilter', () => { let InputFilter; - const store = new Subject(); + let store; beforeEach(() => { + store = new Subject(); InputFilter = inputFilterFactory(store); }); @@ -15,7 +16,6 @@ describe('InputFilter', () => { it('renders with defaults', () => { const component = shallow(); expect(component.find('.react-fuzzy-filter__input').length).toEqual(1); - expect(component.state()).toEqual({search: undefined}); }); it('sets classPrefix', () => { @@ -31,7 +31,7 @@ describe('InputFilter', () => { it('sets initialSearch', () => { const component = shallow(); - expect(component.state()).toEqual({search: 'first search'}); + expect(component.find('input').html()).toEqual(''); }); }); @@ -50,18 +50,6 @@ describe('InputFilter', () => { expect(spy).toHaveBeenCalledWith('my string'); }); - it('sets the state', () => { - component.find('input').simulate('change', { - target: {value: 'first'} - }); - expect(component.state()).toEqual({search: 'first'}); - - component.find('input').simulate('change', { - target: {value: 'second'} - }); - expect(component.state()).toEqual({search: 'second'}); - }); - it('passes the value to the store', (done) => { store.subscribe(data => { expect(data).toEqual('some input'); @@ -73,12 +61,15 @@ describe('InputFilter', () => { }); }); - it('does not set state or pass value to the store if onChange returns false', () => { - component = shallow( false} />); + it('overrides search value with any return value to onChange', (done) => { + store.subscribe(data => { + expect(data).toEqual('hello'); + done(); + }); + component = shallow( 'hello'} />); component.find('input').simulate('change', { target: {value: 'some input'} }); - expect(component.state()).toEqual({search: undefined}); }); }); }); diff --git a/test/index_test.js b/test/index_test.js index 2e46aec..dd7e862 100644 --- a/test/index_test.js +++ b/test/index_test.js @@ -14,6 +14,29 @@ const defaultFuseConfig = { keys: ['searchData'] }; +function defaultRender({name}, index) { + return
{name}: {index}
; +} +defaultRender.propTypes = { + name: PropTypes.string.isRequired +}; + +function componentFactory(inputFilterProps, filterResultsProps) { + const {InputFilter, FilterResults} = fuzzyFilterFactory(); + function MyComponent() { + return ( +
+

Separate Components

+ +

Any amount of content between

+ +
+ ); + } + + return MyComponent; +} + describe('fuzzyFilterFactory', () => { it('returns FilterResults and InputFilter components', () => { const {InputFilter, FilterResults} = fuzzyFilterFactory(); @@ -24,28 +47,10 @@ describe('fuzzyFilterFactory', () => { }); it('input controls filter results', () => { - const {InputFilter, FilterResults} = fuzzyFilterFactory(); - function MyComponent() { - return ( -
-

Separate Components

- -

Any amount of content between

- -
- ); - } - - function defaultRender({name}, index) { - return
{name}: {index}
; - } - defaultRender.propTypes = { - name: PropTypes.string.isRequired - }; + const MyComponent = componentFactory( + {placeholder: 'Search'}, + {items: items, fuseConfig: defaultFuseConfig, renderItem: defaultRender} + ); const component = mount(); expect(component.find('.react-fuzzy-filter__results-container').length).toEqual(1); expect(component.find('.my-item').length).toEqual(4); @@ -60,4 +65,14 @@ describe('fuzzyFilterFactory', () => { }); expect(component.find('.my-item').length).toEqual(1); }); + + it('uses initialSearch', () => { + const MyComponent = componentFactory( + {placeholder: 'Search', initialSearch: 'gdbye'}, + {items: items, fuseConfig: defaultFuseConfig, renderItem: defaultRender} + ); + const component = mount(); + expect(component.find('.react-fuzzy-filter__results-container').length).toEqual(1); + expect(component.find('.my-item').length).toEqual(1); + }); });