Add useItemsState hook

Store props in state

Enforce minimum of 1 for minQueryLength prop
This commit is contained in:
Tom Southall
2022-02-17 19:47:44 +00:00
parent c110222c9d
commit ea835df0e4
6 changed files with 67 additions and 42 deletions

View File

@@ -3,7 +3,7 @@ import setify from 'setify' // Sets input value without changing cursor position
import { StateContext } from '../context/state' import { StateContext } from '../context/state'
import Items from './items' import Items from './items'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { useAutoFocus, useQueryChange } from './hooks/containerEffects' import { useItemsState, useAutoFocus, useQueryChange } from './hooks/containerEffects'
import useData from './hooks/useData' import useData from './hooks/useData'
import undef from '../utils/undef' import undef from '../utils/undef'
import isUndefined from '../utils/isUndefined' import isUndefined from '../utils/isUndefined'
@@ -11,7 +11,6 @@ import startsWithCaseInsensitive from '../utils/startsWithCaseInsensitive'
import defaultStyles from './styles/input.styles.js' import defaultStyles from './styles/input.styles.js'
import { import {
setQuery, setQuery,
setItems,
setHighlighted, setHighlighted,
clearHighlighted, clearHighlighted,
highlightPrev, highlightPrev,
@@ -22,24 +21,24 @@ import {
export default function Container(props) { export default function Container(props) {
// Destructure props // Destructure props
const { const {
autoFocus = false, autoFocus,
debounceWait = 250, debounceWait,
defaultItemGroups, defaultItemGroups,
defaultItemGroupsAreImmutable = true, defaultItemGroupsAreImmutable,
displayField, displayField,
data, data,
dataSearchType, dataSearchType,
isDisabled = false, isDisabled,
itemGroupsAreImmutable = true, itemGroupsAreImmutable,
maxItems = 10, maxItems,
minQueryLength = 0, minQueryLength,
loadingMessage, loadingMessage,
noItemsMessage, noItemsMessage,
onChange, onChange,
onSelect, onSelect,
onEnter, onEnter,
onTab, onTab,
placeholder = '' placeholder
} = props } = props
// Destructure itemGroups prop // Destructure itemGroups prop
@@ -62,7 +61,7 @@ export default function Container(props) {
// Component state // Component state
const [debouncedQuery] = useDebounce(state.query, debounceWait) const [debouncedQuery] = useDebounce(state.query, debounceWait)
const [hasFocus, setHasFocus] = useState(false) const [hasFocus, setHasFocus] = useState(false)
const [isLoaded, setIsLoaded] = useState(false) const [isLoading, setIsLoading] = useState(false)
// DOM references // DOM references
const queryInput = useRef(null) const queryInput = useRef(null)
@@ -87,10 +86,8 @@ export default function Container(props) {
dispatch dispatch
).data ).data
// Store retrieved data in global state // Store retrieved data in global state as state.items
useEffect(() => { useItemsState(swrData)
dispatch(setItems(swrData || []))
}, [swrData])
// Autofocus on render if prop is true // Autofocus on render if prop is true
useAutoFocus(queryInput, autoFocus) useAutoFocus(queryInput, autoFocus)
@@ -99,19 +96,11 @@ export default function Container(props) {
// typeahead value and the query value and fire onChnage // typeahead value and the query value and fire onChnage
useQueryChange(state.query, queryInput, typeaheadInput, onChange) useQueryChange(state.query, queryInput, typeaheadInput, onChange)
// // Whenever the dropdown items change, set the highlighted item // Figure out whether we are able to display a loading state
// // 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?
useEffect(() => { useEffect(() => {
if (state.items && state.items.length) setIsLoaded(true) if (state.items && state.items.length) setIsLoading(false)
else if (state.query.length <= minQueryLength) setIsLoaded(false) else if (state.query.length <= minQueryLength) setIsLoading(true)
}, [state.items, state.query, isLoaded, minQueryLength, setIsLoaded]) }, [state.items, state.query, minQueryLength, setIsLoading])
// When the highlighted item changes, make sure the typeahead matches and format // When the highlighted item changes, make sure the typeahead matches and format
// the query text to match the case of the typeahead text // the query text to match the case of the typeahead text
@@ -267,7 +256,7 @@ export default function Container(props) {
{isDropdown() && ( {isDropdown() && (
<Items <Items
isLoading={!isLoaded} isLoading={isLoading}
items={state.items} items={state.items}
loadingMessage={loadingMessage} loadingMessage={loadingMessage}
noItemsMessage={noItemsMessage} noItemsMessage={noItemsMessage}

View File

@@ -1,6 +1,15 @@
import { useEffect } from 'react' import { useEffect, useContext } from 'react'
import setify from 'setify' // Sets input value without changing cursor position import setify from 'setify' // Sets input value without changing cursor position
//import startsWithCaseInsensitive from '../../utils/startsWithCaseInsensitive' import { StateContext } from '../../context/state'
import { setItems } from '../../actions/actions'
export const useItemsState = (swrData) => {
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 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(() => { useEffect(() => {

View File

@@ -78,8 +78,6 @@ const swrOptions = (isImmutable) => {
} }
const useData = (query, isImmutable, itemGroups, defaultItemGroups, minQueryLength, maxItems) => { const useData = (query, isImmutable, itemGroups, defaultItemGroups, minQueryLength, maxItems) => {
console.log({query})
// See: https://github.com/vercel/swr/discussions/1810 // See: https://github.com/vercel/swr/discussions/1810
const dummyArgToEnsureCachingOfZeroLengthStrings = 'X' const dummyArgToEnsureCachingOfZeroLengthStrings = 'X'

View File

@@ -6,14 +6,17 @@ import undef from '../utils/undef'
const StateContext = createContext() //TODO: Rename GlobalStateContext const StateContext = createContext() //TODO: Rename GlobalStateContext
const StateContextProvider = (props) => { 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, { const [state, dispatch] = useReducer(reducer, {
query: text, query: text,
items, items,
highlighted: undef, highlighted: undef,
selected: undef, selected: undef,
isLoading: false,
customStyles: styles, customStyles: styles,
splitChar splitChar,
props: propsMinusChildren
}) })
useEffect(() => dispatch(setQuery(text)), [text]) // TODO: Is this needed? useEffect(() => dispatch(setQuery(text)), [text]) // TODO: Is this needed?

View File

@@ -6,14 +6,24 @@ import { StateContextProvider } from './context/state'
import isUndefined from './utils/isUndefined' import isUndefined from './utils/isUndefined'
import Container from './components/container' 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) { export default function Turnstone(props) {
const { splitChar, styles, text } = props const newProps = {...propDefaults, ...props}
return ( return (
<StateContextProvider <StateContextProvider {...newProps}>
splitChar={splitChar} <Container {...newProps} />
styles={styles}
text={text}>
<Container {...props} />
</StateContextProvider> </StateContextProvider>
) )
} }
@@ -63,7 +73,15 @@ Turnstone.propTypes = {
}, },
itemGroupsAreImmutable: PropTypes.bool, itemGroupsAreImmutable: PropTypes.bool,
loadingMessage: PropTypes.string, 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, noItemsMessage: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
onSelect: PropTypes.func, onSelect: PropTypes.func,

View File

@@ -23,8 +23,16 @@ describe('Turnstone', () => {
expect(tree).toMatchSnapshot() 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({ 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, data,
dataSearchType dataSearchType
}) })