-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tag Use Case #8
Tag Use Case #8
Changes from 1 commit
6b93e6e
845e5a0
0c9d2c0
8d1b902
53bb3e0
152a810
4d572e8
2d30eee
41dc125
7166dd6
d4a79ee
0b1b344
a74ce35
5ffc4e7
e033268
96783d4
67276b6
3b450a2
655f397
aaded5a
5e7e8b9
d13e762
bad0890
9c6a1e9
5434c43
ff867ef
4e55f63
7d0d0b3
f72f4ca
b9f7697
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ import { | |
fetchTags | ||
} from './utils'; | ||
import { useCombobox } from '../../combobox/hooks/useCombobox'; | ||
import { useAbortController } from '../../useAbortController/useAbortController'; | ||
import useDebouncedCallback from '../../useDebounce/src/hooks/useDebouncedCallback'; | ||
import useAsync from '../../useDebounce/src/hooks/useAsync'; | ||
import { SearchLoader } from '../../loader/SearchLoader'; | ||
|
@@ -41,10 +42,54 @@ import './TagEditor.scss'; | |
|
||
export const TagEditor = () => { | ||
const [input, setInput] = React.useState(''); | ||
const prevInput = React.useRef(''); | ||
const [selectedTags, setSelectedTags] = React.useState<BottomlineTags>({}); | ||
const [tagSuggestions, setTagSuggestions] = React.useState< | ||
BottomlineTag[] | undefined | ||
>(); | ||
>([ | ||
{ | ||
id: 1, | ||
name: 'material-analysis', | ||
count: 5, | ||
excerpt: | ||
"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." | ||
}, | ||
{ | ||
id: 2, | ||
name: 'class-analysis', | ||
count: 33, | ||
excerpt: | ||
"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." | ||
}, | ||
{ | ||
id: 3, | ||
name: 'materialism', | ||
count: 12, | ||
excerpt: | ||
"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." | ||
}, | ||
{ | ||
id: 4, | ||
name: 'dialectical-materialism', | ||
count: 9, | ||
excerpt: | ||
"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." | ||
}, | ||
{ | ||
id: 5, | ||
name: 'historical-materialism', | ||
count: 345435345, | ||
excerpt: | ||
"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." | ||
}, | ||
{ | ||
id: 6, | ||
name: 'materialist-theory', | ||
count: 2, | ||
excerpt: | ||
"What is Lorem Ipsum? Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." | ||
} | ||
]); | ||
|
||
// we define state and change handler callbacks instead of a ref because we don't need to handle | ||
// we need the "appearance" of focus handling for the container when the input element is focused | ||
|
@@ -57,7 +102,7 @@ export const TagEditor = () => { | |
(dispatch) => { | ||
dispatch(); | ||
}, | ||
2000, | ||
1000, | ||
{ trailing: true } | ||
); | ||
|
||
|
@@ -66,10 +111,15 @@ export const TagEditor = () => { | |
status: UseAsyncStatus.IDLE | ||
} as UseAsyncState | ||
}); | ||
if (error) { | ||
console.log('[APP] error:', error); | ||
} | ||
|
||
const { getSignal, forceAbort } = useAbortController(); | ||
|
||
React.useEffect(() => { | ||
if (!input || input === '') return; | ||
run(fetchTags(input)); | ||
if (!input || (prevInput.current === '' && input === '')) return; | ||
run(fetchTags(input, getSignal)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We must explicitly pass the abortSignal function in order to abort a fetch call. |
||
}, [input, run]); | ||
|
||
if (status === UseAsyncStatus.PENDING) derivedLoaderState = true; | ||
|
@@ -95,9 +145,18 @@ export const TagEditor = () => { | |
return recommendations; | ||
} | ||
case BL.ComboboxActions.INPUT_BLUR: { | ||
forceAbort(); | ||
recommendations.inputValue = state.inputValue; | ||
return recommendations; | ||
} | ||
case BL.ComboboxActions.INPUT_VALUE_CHANGE: { | ||
if (action.inputValue === '' && changes.inputValue !== '') { | ||
recommendations.isOpen = false; | ||
setTagSuggestions(undefined); | ||
} | ||
recommendations.inputValue = action.inputValue; | ||
return recommendations; | ||
} | ||
default: { | ||
return changes; | ||
} | ||
|
@@ -116,17 +175,19 @@ export const TagEditor = () => { | |
onInputValueChange: (changes: Partial<BL.ComboboxState<string>>) => { | ||
// piggy-back on the state change | ||
// set our own input value change | ||
prevInput.current = input; | ||
setInput(changes as string); | ||
}, | ||
stateReducer, | ||
items: tagSuggestions, | ||
initialIsOpen: tagSuggestions ? true : false | ||
}); | ||
|
||
// place our own ref on the input | ||
// we use a useeffect to detect when the input ref is focused | ||
// when the input ref is focused, then we focus the div | ||
const noResultsFound = isOpen && tagSuggestions && tagSuggestions.length == 0; | ||
const resultsFound = isOpen && tagSuggestions && tagSuggestions.length >= 1; | ||
|
||
console.log('resultsFound:', resultsFound); | ||
console.log('noResultsFound:', noResultsFound); | ||
return ( | ||
<section className="tag-editor-section"> | ||
<div className="tag-editor"> | ||
|
@@ -172,14 +233,19 @@ export const TagEditor = () => { | |
className="tag-search-input" | ||
ref={null} | ||
/> | ||
{derivedLoaderState ? ( | ||
<span className="tag-search-loader"> | ||
<SearchLoader /> | ||
</span> | ||
) : null} | ||
{/*{derivedLoaderState ? (*/} | ||
<span className="tag-search-loader"> | ||
<SearchLoader /> | ||
</span> | ||
{/*) : null}*/} | ||
</div> | ||
<div className="tag-results-container" {...getPopupProps()}> | ||
{isOpen && tagSuggestions ? ( | ||
{noResultsFound ? ( | ||
<span className="tag-no-results"> | ||
<span>No results found</span> | ||
</span> | ||
) : null} | ||
{resultsFound ? ( | ||
<ul className="tag-results"> | ||
{tagSuggestions.map((tag, index: number) => ( | ||
<li | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import React from 'react'; | ||
export function useAbortController() { | ||
const abortControllerRef = React.useRef<AbortController>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Create an abort controller that we control when it changes/updates between renders |
||
|
||
// our abort controller is declared once on initial render | ||
const getAbortController = React.useCallback(() => { | ||
if (!abortControllerRef.current) { | ||
abortControllerRef.current = new AbortController(); | ||
} | ||
return abortControllerRef.current; | ||
}, []); | ||
|
||
// callback ran when we need to create a new abort controller | ||
const createNewAbortController = React.useCallback(() => { | ||
abortControllerRef.current = new AbortController(); | ||
}, []); | ||
Comment on lines
+13
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might cancel a request. When that happens, and a new request comes around, we need to create a new 'instance' of an abort controller |
||
|
||
const forceAbort = React.useCallback(() => { | ||
if (getAbortController()) { | ||
// abort the previous request | ||
getAbortController().abort(); | ||
} | ||
}, []); | ||
Comment on lines
+18
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A convenience function that we expose as a part of our API for the user to explicitly cancel an inflight request |
||
|
||
// when the component unmounts/re-renders, abort any existing requests | ||
// to prevent memory leaks | ||
React.useEffect(() => { | ||
return () => getAbortController().abort(); | ||
}, [getAbortController]); | ||
|
||
// when we call getSignal, we cancel any outstanding requests | ||
// and create a new instance of an abort controller | ||
// then return the signal for the requesting code | ||
const getSignal = React.useCallback(() => { | ||
if (getAbortController()) { | ||
// abort the previous request | ||
getAbortController().abort(); | ||
createNewAbortController(); | ||
} | ||
|
||
return getAbortController().signal; | ||
}, [getAbortController, createNewAbortController]); | ||
|
||
return { getSignal, forceAbort }; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be item, woops.