From ea835df0e45cd404cae043bd00e72c529678b33d Mon Sep 17 00:00:00 2001 From: Tom Southall Date: Thu, 17 Feb 2022 19:47:44 +0000 Subject: [PATCH] Add useItemsState hook Store props in state Enforce minimum of 1 for minQueryLength prop --- src/lib/components/container.jsx | 45 ++++++++------------ src/lib/components/hooks/containerEffects.js | 13 +++++- src/lib/components/hooks/useData.js | 2 - src/lib/context/state.jsx | 7 ++- src/lib/index.jsx | 32 +++++++++++--- src/lib/index.test.jsx | 10 ++++- 6 files changed, 67 insertions(+), 42 deletions(-) diff --git a/src/lib/components/container.jsx b/src/lib/components/container.jsx index cc37ad4..f12701e 100644 --- a/src/lib/components/container.jsx +++ b/src/lib/components/container.jsx @@ -3,7 +3,7 @@ import setify from 'setify' // Sets input value without changing cursor position import { StateContext } from '../context/state' import Items from './items' import { useDebounce } from 'use-debounce' -import { useAutoFocus, useQueryChange } from './hooks/containerEffects' +import { useItemsState, useAutoFocus, useQueryChange } from './hooks/containerEffects' import useData from './hooks/useData' import undef from '../utils/undef' import isUndefined from '../utils/isUndefined' @@ -11,7 +11,6 @@ import startsWithCaseInsensitive from '../utils/startsWithCaseInsensitive' import defaultStyles from './styles/input.styles.js' import { setQuery, - setItems, setHighlighted, clearHighlighted, highlightPrev, @@ -22,24 +21,24 @@ import { export default function Container(props) { // Destructure props const { - autoFocus = false, - debounceWait = 250, + autoFocus, + debounceWait, defaultItemGroups, - defaultItemGroupsAreImmutable = true, + defaultItemGroupsAreImmutable, displayField, data, dataSearchType, - isDisabled = false, - itemGroupsAreImmutable = true, - maxItems = 10, - minQueryLength = 0, + isDisabled, + itemGroupsAreImmutable, + maxItems, + minQueryLength, loadingMessage, noItemsMessage, onChange, onSelect, onEnter, onTab, - placeholder = '' + placeholder } = props // Destructure itemGroups prop @@ -62,7 +61,7 @@ export default function Container(props) { // Component state const [debouncedQuery] = useDebounce(state.query, debounceWait) const [hasFocus, setHasFocus] = useState(false) - const [isLoaded, setIsLoaded] = useState(false) + const [isLoading, setIsLoading] = useState(false) // DOM references const queryInput = useRef(null) @@ -87,10 +86,8 @@ export default function Container(props) { dispatch ).data - // Store retrieved data in global state - useEffect(() => { - dispatch(setItems(swrData || [])) - }, [swrData]) + // Store retrieved data in global state as state.items + useItemsState(swrData) // Autofocus on render if prop is true useAutoFocus(queryInput, autoFocus) @@ -99,19 +96,11 @@ export default function Container(props) { // typeahead value and the query value and fire onChnage useQueryChange(state.query, queryInput, typeaheadInput, onChange) - // // Whenever the dropdown items change, set the highlighted item - // // to either the first or nothing if there are no items - // useEffect(() => { - // setHighlightedState( - // state.items && state.items.length ? { index: 0, text: state.items[0].text } : undef - // ) - // }, [state.items, setHighlightedState]) - - // Figure out whether we are able to display a loading state //TODO: useReducer instead of useeffect? + // Figure out whether we are able to display a loading state useEffect(() => { - if (state.items && state.items.length) setIsLoaded(true) - else if (state.query.length <= minQueryLength) setIsLoaded(false) - }, [state.items, state.query, isLoaded, minQueryLength, setIsLoaded]) + if (state.items && state.items.length) setIsLoading(false) + else if (state.query.length <= minQueryLength) setIsLoading(true) + }, [state.items, state.query, minQueryLength, setIsLoading]) // When the highlighted item changes, make sure the typeahead matches and format // the query text to match the case of the typeahead text @@ -267,7 +256,7 @@ export default function Container(props) { {isDropdown() && ( { + const { dispatch } = useContext(StateContext) + + useEffect(() => { + dispatch(setItems(swrData || [])) + }, [swrData]) +} export const useAutoFocus = (queryInput, autoFocus) => { // TODO: might be able to use autofocus property of input for this - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autofocus useEffect(() => { diff --git a/src/lib/components/hooks/useData.js b/src/lib/components/hooks/useData.js index 895376d..44404de 100644 --- a/src/lib/components/hooks/useData.js +++ b/src/lib/components/hooks/useData.js @@ -78,8 +78,6 @@ const swrOptions = (isImmutable) => { } const useData = (query, isImmutable, itemGroups, defaultItemGroups, minQueryLength, maxItems) => { - console.log({query}) - // See: https://github.com/vercel/swr/discussions/1810 const dummyArgToEnsureCachingOfZeroLengthStrings = 'X' diff --git a/src/lib/context/state.jsx b/src/lib/context/state.jsx index 80503c2..eeb2b3c 100644 --- a/src/lib/context/state.jsx +++ b/src/lib/context/state.jsx @@ -6,14 +6,17 @@ import undef from '../utils/undef' const StateContext = createContext() //TODO: Rename GlobalStateContext const StateContextProvider = (props) => { - const { children, splitChar, styles = {}, text = '', items = [] } = props + const { splitChar, styles = {}, text = '', items = [] } = props + const { children, ...propsMinusChildren} = props const [state, dispatch] = useReducer(reducer, { query: text, items, highlighted: undef, selected: undef, + isLoading: false, customStyles: styles, - splitChar + splitChar, + props: propsMinusChildren }) useEffect(() => dispatch(setQuery(text)), [text]) // TODO: Is this needed? diff --git a/src/lib/index.jsx b/src/lib/index.jsx index 0651a2f..1fa0e60 100644 --- a/src/lib/index.jsx +++ b/src/lib/index.jsx @@ -6,14 +6,24 @@ import { StateContextProvider } from './context/state' import isUndefined from './utils/isUndefined' import Container from './components/container' +// Set prop defaults before passing them on to components +const propDefaults = { + autoFocus: false, + debounceWait: 250, + defaultItemGroupsAreImmutable: true, + isDisabled: false, + itemGroupsAreImmutable: true, + maxItems: 10, + minQueryLength: 1, + placeholder: '' +} + export default function Turnstone(props) { - const { splitChar, styles, text } = props + const newProps = {...propDefaults, ...props} + return ( - - + + ) } @@ -63,7 +73,15 @@ Turnstone.propTypes = { }, itemGroupsAreImmutable: PropTypes.bool, loadingMessage: PropTypes.string, - minQueryLength: PropTypes.number, + minQueryLength: (props) => { + PropTypes.checkPropTypes( + {minQueryLength: PropTypes.number}, + {minQueryLength: props.minQueryLength}, + 'prop', 'Turnstone' + ) + if(props.minQueryLength < propDefaults.minQueryLength) + return new Error(`Prop "minQueryLength" must be a number greater than ${propDefaults.minQueryLength - 1}`) + }, noItemsMessage: PropTypes.string, onChange: PropTypes.func, onSelect: PropTypes.func, diff --git a/src/lib/index.test.jsx b/src/lib/index.test.jsx index 3fa0489..0a32ce7 100644 --- a/src/lib/index.test.jsx +++ b/src/lib/index.test.jsx @@ -23,8 +23,16 @@ describe('Turnstone', () => { expect(tree).toMatchSnapshot() }) - test('Turnstone component passes all props to Container component', () => { + test('Turnstone component passes all props to Container component along with default props', () => { expect(component.root.children[0].children[0].props).toEqual({ + autoFocus: false, + debounceWait: 250, + defaultItemGroupsAreImmutable: true, + isDisabled: false, + itemGroupsAreImmutable: true, + maxItems: 10, + minQueryLength: 1, + placeholder: '', data, dataSearchType })