From aafeba8ad7a94078c39a13fcdc0250190f064a76 Mon Sep 17 00:00:00 2001 From: hunginf Date: Fri, 4 Oct 2024 20:57:26 +0700 Subject: [PATCH 1/3] Implement feature. --- .gitignore | 1 + src/components/Input/index.tsx | 78 +++++++++++++++++++++++++++++++-- src/components/Input/input.scss | 22 ++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 87b58f0..30a5941 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ storybook-static *.njsproj *.sln *.sw? +*.history \ No newline at end of file diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 453c742..9297b5d 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -1,7 +1,7 @@ import "./input.scss"; import { fetchData } from "../../utils/fetch-data"; import { debounce } from "../../utils/deboucne"; -import Loader from "../Loader"; +import { ChangeEvent, useCallback, useRef, useState } from "react"; export interface InputProps { /** Placeholder of the input */ @@ -10,12 +10,84 @@ export interface InputProps { onSelectItem: (item: string) => void; } +const SearchResultList: React.FC<{list: string[], isLoading?: boolean, isError: boolean, onSelectItem: (item: string) => void}> = ({list, isLoading, isError, onSelectItem}) => { + if (isLoading) { + return

Loading...

; + } + + if (isError) { + return

Occur an error.

+ } + + if (!list.length) { + return

No results.

+ } + + return +} + const Input = ({ placeholder, onSelectItem }: InputProps) => { // DO NOT remove this log - console.log('input re-render') + console.log('input re-render'); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [list, setList] = useState([]); + const latestRequestRef = useRef(0); // Your code start here - return + const _onChangeFnc = async (e:ChangeEvent) => { + const { value } = e.target; + + setIsLoading(true); + setIsError(false); + + if(!value.trim()) { + if (!list.length) { + setList([]); + } + + if (isLoading) { + setIsLoading(false); + } + + if (isError) { + setIsError(false); + } + + return; + } + + const currentFetchId = ++latestRequestRef.current; + + try { + const data = await fetchData(value); + if (currentFetchId === latestRequestRef.current) { + setList(data); + } + } catch { + setIsError(true); + } finally { + if (currentFetchId === latestRequestRef.current) { + setIsLoading(false); + } + } + } + + const _onChangeDebounce = useCallback( + debounce(_onChangeFnc, 300), + [] + ); + + return
+ + +
// Your code end here }; diff --git a/src/components/Input/input.scss b/src/components/Input/input.scss index 1dafbe7..3b235ad 100644 --- a/src/components/Input/input.scss +++ b/src/components/Input/input.scss @@ -4,3 +4,25 @@ html{ font-size: 16px; } + +.search-input { + &--text { + padding: 5px 10px; + } + &--list { + list-style: none; + padding: 0; + margin: 0; + border: 1px solid #000; + border-top: none; + max-height: 350px; + overflow-y: auto; + &--item { + padding: 10px; + cursor: pointer; + &:hover { + background-color: antiquewhite; + } + } + } +} \ No newline at end of file From 5149e868dce8fa2498f7165e954328cd424e7bbf Mon Sep 17 00:00:00 2001 From: hunginf Date: Fri, 4 Oct 2024 20:57:46 +0700 Subject: [PATCH 2/3] Update code. --- src/components/Input/index.tsx | 101 +++++++++++++++++++++------------ src/utils/constants.ts | 6 ++ 2 files changed, 70 insertions(+), 37 deletions(-) create mode 100644 src/utils/constants.ts diff --git a/src/components/Input/index.tsx b/src/components/Input/index.tsx index 9297b5d..9709187 100644 --- a/src/components/Input/index.tsx +++ b/src/components/Input/index.tsx @@ -2,6 +2,7 @@ import "./input.scss"; import { fetchData } from "../../utils/fetch-data"; import { debounce } from "../../utils/deboucne"; import { ChangeEvent, useCallback, useRef, useState } from "react"; +import { FETCHING_STATUS } from "../../utils/constants"; export interface InputProps { /** Placeholder of the input */ @@ -10,73 +11,99 @@ export interface InputProps { onSelectItem: (item: string) => void; } -const SearchResultList: React.FC<{list: string[], isLoading?: boolean, isError: boolean, onSelectItem: (item: string) => void}> = ({list, isLoading, isError, onSelectItem}) => { - if (isLoading) { +const SearchResultList: React.FC<{ + list: string[], + status: string | null, + onSelectItem: (item: string) => void}> = ({ + list, + status, + onSelectItem + }) => { + if (status === FETCHING_STATUS.FETCHING) { return

Loading...

; } - if (isError) { + if (status === FETCHING_STATUS.ERROR) { return

Occur an error.

} - if (!list.length) { + if (!list.length && status !== FETCHING_STATUS.INITIAL) { return

No results.

} - return
    + if (list.length) { + return
      { list.map((item:string) =>
    • onSelectItem(item)} role="presentation">{item}
    • ) }
    + } + + return null; } const Input = ({ placeholder, onSelectItem }: InputProps) => { // DO NOT remove this log console.log('input re-render'); - const [isLoading, setIsLoading] = useState(false); - const [isError, setIsError] = useState(false); + // Your code start here + const [status, setStatus] = useState(FETCHING_STATUS.INITIAL); const [list, setList] = useState([]); - const latestRequestRef = useRef(0); + const inputRef = useRef(); + const fetchDataPromise = useRef>(); - // Your code start here - const _onChangeFnc = async (e:ChangeEvent) => { + const _onChangeFnc = (e:ChangeEvent) => { const { value } = e.target; - setIsLoading(true); - setIsError(false); - + // Return when there is empty value. if(!value.trim()) { - if (!list.length) { - setList([]); - } - - if (isLoading) { - setIsLoading(false); - } - - if (isError) { - setIsError(false); - } + setList([]); + setStatus(FETCHING_STATUS.INITIAL); return; } - const currentFetchId = ++latestRequestRef.current; + inputRef.current = value; + fetchDataPromise.current = undefined; + + setStatus(FETCHING_STATUS.FETCHING); + + // Fetch API. + const _fetchDataPromise = fetchData(value); + fetchDataPromise.current = _fetchDataPromise; - try { - const data = await fetchData(value); - if (currentFetchId === latestRequestRef.current) { - setList(data); + _fetchDataPromise + .then((res) => { + if (isCheckXhr(_fetchDataPromise)) { + setStatus(FETCHING_STATUS.INITIAL); + return; } - } catch { - setIsError(true); - } finally { - if (currentFetchId === latestRequestRef.current) { - setIsLoading(false); + setList(res); + setStatus(FETCHING_STATUS.SUCCESS); + }) + .catch(() => { + if (isCheckXhr(_fetchDataPromise)) { + setStatus(FETCHING_STATUS.INITIAL); + return; } - } + setList([]); + setStatus(FETCHING_STATUS.ERROR); + }) + .finally(() => { + fetchDataPromise.current = undefined; + }); + } + + const isCheckXhr = (promise: Promise) => { + // Skip previous request. + return fetchDataPromise.current != promise; + } + + const isChangeInputRef = (e: string) => { + const isChanged = e !== inputRef.current; + + return isChanged; } const _onChangeDebounce = useCallback( @@ -85,8 +112,8 @@ const Input = ({ placeholder, onSelectItem }: InputProps) => { ); return
    - - + +
    // Your code end here }; diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000..1078f4d --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,6 @@ +export const FETCHING_STATUS = { + "INITIAL": "initial", + "FETCHING": "fetching", + "ERROR": "error", + "SUCCESS": "success" +} \ No newline at end of file From 43879fe360c5432fcb2837b77eff44505d0cffdb Mon Sep 17 00:00:00 2001 From: hunginf Date: Fri, 4 Oct 2024 21:13:27 +0700 Subject: [PATCH 3/3] Update .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 30a5941..5eea626 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ storybook-static *.njsproj *.sln *.sw? -*.history \ No newline at end of file +*.history +.vercel