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 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() && (
<Items
isLoading={!isLoaded}
isLoading={isLoading}
items={state.items}
loadingMessage={loadingMessage}
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 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
useEffect(() => {

View File

@@ -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'

View File

@@ -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?

View File

@@ -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 (
<StateContextProvider
splitChar={splitChar}
styles={styles}
text={text}>
<Container {...props} />
<StateContextProvider {...newProps}>
<Container {...newProps} />
</StateContextProvider>
)
}
@@ -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,

View File

@@ -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
})