@codeleap/hooks 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +30 -0
- package/package.json.bak +30 -0
- package/src/GlobalContext.tsx +51 -0
- package/src/index.ts +47 -0
- package/src/onMount.ts +8 -0
- package/src/onUpdate.ts +8 -0
- package/src/useBooleanToggle.ts +15 -0
- package/src/useConditionalState.ts +26 -0
- package/src/useCounter.ts +5 -0
- package/src/useDebounce.ts +25 -0
- package/src/useEffectOnce.ts +5 -0
- package/src/useForceRender.ts +6 -0
- package/src/useInterval.ts +21 -0
- package/src/useListState.ts +92 -0
- package/src/useModal.ts +10 -0
- package/src/usePartialState.ts +21 -0
- package/src/usePlaces.ts +63 -0
- package/src/usePlacesAutocompleteUtils.ts +59 -0
- package/src/usePrevious.ts +9 -0
- package/src/usePromise.ts +46 -0
- package/src/useSearch/index.tsx +71 -0
- package/src/useSearch/types.ts +14 -0
- package/src/useToggle.ts +16 -0
- package/src/useUncontrolled.ts +69 -0
- package/src/useUnmount.ts +11 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeleap/hooks",
|
|
3
|
+
"version": "4.3.0",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
8
|
+
"type": "git",
|
|
9
|
+
"directory": "packages/hooks"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@codeleap/config": "4.3.0",
|
|
13
|
+
"@codeleap/types": "4.3.0",
|
|
14
|
+
"@codeleap/utils": "4.3.0",
|
|
15
|
+
"@codeleap/logger": "4.3.0",
|
|
16
|
+
"ts-node-dev": "1.1.8"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "echo 'No build needed'"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@codeleap/types": "4.3.0",
|
|
23
|
+
"@codeleap/utils": "4.3.0",
|
|
24
|
+
"@codeleap/logger": "4.3.0",
|
|
25
|
+
"axios": "^1.7.9",
|
|
26
|
+
"typescript": "5.0.4",
|
|
27
|
+
"react": "18.1.0",
|
|
28
|
+
"@tanstack/react-query": "5.60.6"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json.bak
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeleap/hooks",
|
|
3
|
+
"version": "4.3.0",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
8
|
+
"type": "git",
|
|
9
|
+
"directory": "packages/hooks"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@codeleap/config": "workspace:*",
|
|
13
|
+
"@codeleap/types": "workspace:*",
|
|
14
|
+
"@codeleap/utils": "workspace:*",
|
|
15
|
+
"@codeleap/logger": "workspace:*",
|
|
16
|
+
"ts-node-dev": "1.1.8"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "echo 'No build needed'"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@codeleap/types": "workspace:*",
|
|
23
|
+
"@codeleap/utils": "workspace:*",
|
|
24
|
+
"@codeleap/logger": "workspace:*",
|
|
25
|
+
"axios": "^1.7.9",
|
|
26
|
+
"typescript": "5.0.4",
|
|
27
|
+
"react": "18.1.0",
|
|
28
|
+
"@tanstack/react-query": "5.60.6"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { createContext, ReactNode, useContext } from 'react'
|
|
2
|
+
import { Logger, silentLogger } from '@codeleap/logger'
|
|
3
|
+
import { AppSettings } from '@codeleap/types'
|
|
4
|
+
|
|
5
|
+
type ContextProps = {
|
|
6
|
+
logger: Logger
|
|
7
|
+
Settings: AppSettings
|
|
8
|
+
isBrowser: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const GlobalContext = createContext<ContextProps>({} as ContextProps)
|
|
12
|
+
|
|
13
|
+
type ProviderProps = {
|
|
14
|
+
logger: Logger
|
|
15
|
+
settings: AppSettings
|
|
16
|
+
isBrowser: boolean
|
|
17
|
+
children: ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const GlobalContextProvider = (props: ProviderProps) => {
|
|
21
|
+
const {
|
|
22
|
+
logger,
|
|
23
|
+
settings,
|
|
24
|
+
isBrowser,
|
|
25
|
+
children
|
|
26
|
+
} = props
|
|
27
|
+
|
|
28
|
+
const value: ContextProps = {
|
|
29
|
+
logger: logger || silentLogger,
|
|
30
|
+
Settings: settings,
|
|
31
|
+
isBrowser,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<GlobalContext.Provider
|
|
36
|
+
value={value}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</GlobalContext.Provider>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const useGlobalContext = () => {
|
|
44
|
+
const context = useContext(GlobalContext)
|
|
45
|
+
|
|
46
|
+
if (!context) {
|
|
47
|
+
throw new Error('GlobalContext not found')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return context
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useMemo,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
useDebugValue,
|
|
11
|
+
useReducer,
|
|
12
|
+
} from 'react'
|
|
13
|
+
|
|
14
|
+
export * from './useConditionalState'
|
|
15
|
+
export * from './usePromise'
|
|
16
|
+
export * from './useListState'
|
|
17
|
+
export * from './useUncontrolled'
|
|
18
|
+
export * from './useCounter'
|
|
19
|
+
export * from './useForceRender'
|
|
20
|
+
export * from './useDebounce'
|
|
21
|
+
export * from './useInterval'
|
|
22
|
+
export * from './onMount'
|
|
23
|
+
export * from './onUpdate'
|
|
24
|
+
export * from './usePrevious'
|
|
25
|
+
export * from './useToggle'
|
|
26
|
+
export * from './useBooleanToggle'
|
|
27
|
+
export * from './useModal'
|
|
28
|
+
export * from './usePlaces'
|
|
29
|
+
export * from './usePlacesAutocompleteUtils'
|
|
30
|
+
export * from './useEffectOnce'
|
|
31
|
+
export * from './useUnmount'
|
|
32
|
+
export * from './useSearch'
|
|
33
|
+
export * from './usePartialState'
|
|
34
|
+
export * from './GlobalContext'
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
useEffect,
|
|
38
|
+
useRef,
|
|
39
|
+
useState,
|
|
40
|
+
useImperativeHandle,
|
|
41
|
+
useMemo,
|
|
42
|
+
useCallback,
|
|
43
|
+
useContext,
|
|
44
|
+
useLayoutEffect,
|
|
45
|
+
useDebugValue,
|
|
46
|
+
useReducer,
|
|
47
|
+
}
|
package/src/onMount.ts
ADDED
package/src/onUpdate.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export function useBooleanToggle(initial: boolean) {
|
|
4
|
+
const [v, setV] = useState(initial)
|
|
5
|
+
|
|
6
|
+
function toggleOrSet(value?: boolean) {
|
|
7
|
+
if (typeof value === 'boolean') {
|
|
8
|
+
setV(value)
|
|
9
|
+
} else {
|
|
10
|
+
setV((previous) => !previous)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return [v, toggleOrSet] as const
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useState, SetStateAction, Dispatch } from 'react'
|
|
2
|
+
import { AnyFunction } from '@codeleap/types'
|
|
3
|
+
import { useBooleanToggle } from './useBooleanToggle'
|
|
4
|
+
|
|
5
|
+
type UseConditionalStateOptions<T> = {
|
|
6
|
+
initialValue?: T
|
|
7
|
+
isBooleanToggle?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type SetState<T> = Dispatch<SetStateAction<T>> & ((value: T) => void) & (() => void)
|
|
11
|
+
|
|
12
|
+
export const useConditionalState = <T>(
|
|
13
|
+
value: T | undefined,
|
|
14
|
+
setter: AnyFunction,
|
|
15
|
+
options: UseConditionalStateOptions<T> = {}
|
|
16
|
+
): [T, SetState<T>] => {
|
|
17
|
+
const state = options?.isBooleanToggle
|
|
18
|
+
? useBooleanToggle(options?.initialValue as boolean)
|
|
19
|
+
: useState(options?.initialValue)
|
|
20
|
+
|
|
21
|
+
if (!value && typeof setter === 'function') {
|
|
22
|
+
return [value, setter]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return state as unknown as [T, SetState<T>]
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export function useDebounce<T>(
|
|
4
|
+
value: T,
|
|
5
|
+
debounce: number,
|
|
6
|
+
): [T, () => void] {
|
|
7
|
+
const [debouncedValue, setDebouncedValue] = useState(value)
|
|
8
|
+
|
|
9
|
+
const timeoutRef = useRef(null)
|
|
10
|
+
|
|
11
|
+
const reset = () => {
|
|
12
|
+
if (timeoutRef.current) {
|
|
13
|
+
clearTimeout(timeoutRef.current)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
timeoutRef.current = setTimeout(() => {
|
|
18
|
+
setDebouncedValue(value)
|
|
19
|
+
}, debounce)
|
|
20
|
+
|
|
21
|
+
return reset
|
|
22
|
+
}, [value])
|
|
23
|
+
|
|
24
|
+
return [debouncedValue, reset]
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import { AnyFunction } from '@codeleap/types'
|
|
3
|
+
|
|
4
|
+
export function useInterval(callback: AnyFunction, interval: number) {
|
|
5
|
+
const intervalRef = useRef(null)
|
|
6
|
+
|
|
7
|
+
function clear() {
|
|
8
|
+
clearInterval(intervalRef.current)
|
|
9
|
+
intervalRef.current = null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function start() {
|
|
13
|
+
intervalRef.current = setInterval(callback, interval)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
clear,
|
|
18
|
+
start,
|
|
19
|
+
interval: intervalRef.current,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface UseListStateHandler<T> {
|
|
4
|
+
setState: React.Dispatch<React.SetStateAction<T[]>>
|
|
5
|
+
append: (...items: T[]) => void
|
|
6
|
+
prepend: (...items: T[]) => void
|
|
7
|
+
insert: (index: number, ...items: T[]) => void
|
|
8
|
+
pop: () => void
|
|
9
|
+
shift: () => void
|
|
10
|
+
apply: (fn: (item: T, index?: number) => T) => void
|
|
11
|
+
applyWhere: (
|
|
12
|
+
condition: (item: T, index: number) => boolean,
|
|
13
|
+
fn: (item: T, index?: number) => T
|
|
14
|
+
) => void
|
|
15
|
+
remove: (...indices: number[]) => void
|
|
16
|
+
reorder: ({ from, to }: { from: number; to: number }) => void
|
|
17
|
+
setItem: (index: number, item: T) => void
|
|
18
|
+
setItemProp: <K extends keyof T, U extends T[K]>(index: number, prop: K, value: U) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type UseListState<T> = [T[], UseListStateHandler<T>]
|
|
22
|
+
|
|
23
|
+
export function useListState<T>(initialValue: (T[] | (() => T[])) = []): UseListState<T> {
|
|
24
|
+
const [state, setState] = useState(initialValue)
|
|
25
|
+
|
|
26
|
+
const append = (...items: T[]) => setState((current) => [...current, ...items])
|
|
27
|
+
const prepend = (...items: T[]) => setState((current) => [...items, ...current])
|
|
28
|
+
|
|
29
|
+
const insert = (index: number, ...items: T[]) => setState((current) => [...current.slice(0, index), ...items, ...current.slice(index)])
|
|
30
|
+
|
|
31
|
+
const apply = (fn: (item: T, index?: number) => T) => setState((current) => current.map((item, index) => fn(item, index)))
|
|
32
|
+
|
|
33
|
+
const remove = (...indices: number[]) => setState((current) => current.filter((_, index) => !indices.includes(index)))
|
|
34
|
+
|
|
35
|
+
const pop = () => setState((current) => {
|
|
36
|
+
const cloned = [...current]
|
|
37
|
+
cloned.pop()
|
|
38
|
+
return cloned
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const shift = () => setState((current) => {
|
|
42
|
+
const cloned = [...current]
|
|
43
|
+
cloned.shift()
|
|
44
|
+
return cloned
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const reorder = ({ from, to }: { from: number; to: number }) => setState((current) => {
|
|
48
|
+
const cloned = [...current]
|
|
49
|
+
const item = current[from]
|
|
50
|
+
|
|
51
|
+
cloned.splice(from, 1)
|
|
52
|
+
cloned.splice(to, 0, item)
|
|
53
|
+
|
|
54
|
+
return cloned
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const setItem = (index: number, item: T) => setState((current) => {
|
|
58
|
+
const cloned = [...current]
|
|
59
|
+
cloned[index] = item
|
|
60
|
+
return cloned
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const setItemProp = <K extends keyof T, U extends T[K]>(index: number, prop: K, value: U) => setState((current) => {
|
|
64
|
+
const cloned = [...current]
|
|
65
|
+
cloned[index] = { ...cloned[index], [prop]: value }
|
|
66
|
+
return cloned
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const applyWhere = (
|
|
70
|
+
condition: (item: T, index: number) => boolean,
|
|
71
|
+
fn: (item: T, index?: number) => T,
|
|
72
|
+
) => setState((current) => current.map((item, index) => (condition(item, index) ? fn(item, index) : item)),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
state,
|
|
77
|
+
{
|
|
78
|
+
setState,
|
|
79
|
+
append,
|
|
80
|
+
prepend,
|
|
81
|
+
insert,
|
|
82
|
+
pop,
|
|
83
|
+
shift,
|
|
84
|
+
apply,
|
|
85
|
+
applyWhere,
|
|
86
|
+
remove,
|
|
87
|
+
reorder,
|
|
88
|
+
setItem,
|
|
89
|
+
setItemProp,
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
}
|
package/src/useModal.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { DeepPartial } from '@codeleap/types'
|
|
3
|
+
import { deepMerge } from '@codeleap/utils'
|
|
4
|
+
|
|
5
|
+
type SetPartialStateCallback<T> = (value: T) => DeepPartial<T>
|
|
6
|
+
|
|
7
|
+
export function usePartialState<T= any>(initial: T | (() => T)) {
|
|
8
|
+
const [state, setState] = useState(initial)
|
|
9
|
+
|
|
10
|
+
function setPartial(
|
|
11
|
+
value: DeepPartial<T> | SetPartialStateCallback<T>,
|
|
12
|
+
) {
|
|
13
|
+
if (typeof value === 'function') {
|
|
14
|
+
setState((v) => deepMerge(v, value(v as T)))
|
|
15
|
+
} else {
|
|
16
|
+
setState(deepMerge(state, value))
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return [state as T, setPartial] as const
|
|
21
|
+
}
|
package/src/usePlaces.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import { useQuery } from '@tanstack/react-query'
|
|
3
|
+
|
|
4
|
+
const BASE_URL = 'https://maps.googleapis.com/maps/api/place/autocomplete/json'
|
|
5
|
+
const BASE_URL_DETAILS = 'https://maps.googleapis.com/maps/api/place/details/json'
|
|
6
|
+
const BASE_URL_GEOCODING = 'https://maps.googleapis.com/maps/api/geocode/json'
|
|
7
|
+
|
|
8
|
+
const latLngRegex = /^-?\d+(\.\d+)?,-?\d+(\.\d+)?$/
|
|
9
|
+
|
|
10
|
+
type Params = {
|
|
11
|
+
input?: string
|
|
12
|
+
key?: string
|
|
13
|
+
showDetails?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const retrievePlaceDetails = async (placeId: string, apiKey: string) => {
|
|
17
|
+
const response = await axios.get(BASE_URL_DETAILS, {
|
|
18
|
+
params: {
|
|
19
|
+
place_id: placeId,
|
|
20
|
+
key: apiKey,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return response?.data?.result
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const retrievePlaces = async (params: Params) => {
|
|
28
|
+
let response
|
|
29
|
+
const inputWithoutSpaces = params?.input?.replace(/\s/g, '')
|
|
30
|
+
const isLatLng = latLngRegex?.test(inputWithoutSpaces)
|
|
31
|
+
|
|
32
|
+
if (isLatLng) {
|
|
33
|
+
response = await axios?.get(BASE_URL_GEOCODING, { params: { latlng: params?.input, key: params?.key } })
|
|
34
|
+
} else {
|
|
35
|
+
response = await axios?.get(BASE_URL, { params })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let places = response?.data?.results || response?.data?.predictions
|
|
39
|
+
|
|
40
|
+
if (params?.showDetails) {
|
|
41
|
+
const apiKey = params?.key
|
|
42
|
+
places = await Promise.all(
|
|
43
|
+
places?.map(async (place) => {
|
|
44
|
+
const placeId = place?.place_id
|
|
45
|
+
const details = await retrievePlaceDetails(placeId, apiKey)
|
|
46
|
+
return { ...place, details }
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return places
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type UsePlacesParams = Params
|
|
55
|
+
|
|
56
|
+
export const usePlaces = (params: Params) => {
|
|
57
|
+
const places = useQuery({
|
|
58
|
+
queryKey: ['places', params],
|
|
59
|
+
queryFn: () => retrievePlaces(params),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return places
|
|
63
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { TypeGuards } from '@codeleap/types'
|
|
3
|
+
|
|
4
|
+
export type UsePlacesAutocompleteUtilsProps<T extends Record<string, any>> = {
|
|
5
|
+
debounce?: number
|
|
6
|
+
onValueChange?: (address: string) => void
|
|
7
|
+
onPress?: (address: string, item: T) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const usePlacesAutocompleteUtils = <T extends Record<string, any>>(props: UsePlacesAutocompleteUtilsProps<T>) => {
|
|
11
|
+
const {
|
|
12
|
+
debounce = 250,
|
|
13
|
+
onValueChange,
|
|
14
|
+
onPress,
|
|
15
|
+
} = props
|
|
16
|
+
|
|
17
|
+
const [address, setAddress] = React.useState('')
|
|
18
|
+
const [isTyping, setIsTyping] = React.useState(false)
|
|
19
|
+
|
|
20
|
+
const setSearchTimeout = React.useRef<NodeJS.Timeout | null>(null)
|
|
21
|
+
|
|
22
|
+
const handleChangeAddress = (address: string) => {
|
|
23
|
+
setAddress(address)
|
|
24
|
+
|
|
25
|
+
if (TypeGuards.isNil(debounce)) {
|
|
26
|
+
onValueChange?.(address)
|
|
27
|
+
setTimeout(() => setIsTyping(false), 250)
|
|
28
|
+
} else {
|
|
29
|
+
if (setSearchTimeout.current) {
|
|
30
|
+
clearTimeout(setSearchTimeout.current)
|
|
31
|
+
setSearchTimeout.current = null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setSearchTimeout.current = setTimeout(() => {
|
|
35
|
+
onValueChange?.(address)
|
|
36
|
+
setIsTyping(false)
|
|
37
|
+
}, debounce ?? 0)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handlePressAddress = (address: string, item: T) => {
|
|
42
|
+
setAddress(address)
|
|
43
|
+
onPress?.(address, item)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const handleClearAddress = () => {
|
|
47
|
+
setAddress('')
|
|
48
|
+
onValueChange?.('')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
handleChangeAddress,
|
|
53
|
+
handlePressAddress,
|
|
54
|
+
handleClearAddress,
|
|
55
|
+
address,
|
|
56
|
+
isTyping,
|
|
57
|
+
setIsTyping,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import { TypeGuards, AnyFunction } from '@codeleap/types'
|
|
3
|
+
|
|
4
|
+
type UsePromiseOptions<T = any> = {
|
|
5
|
+
onResolve?: (value: T) => void
|
|
6
|
+
onReject?: (err: any) => void
|
|
7
|
+
timeout?: number
|
|
8
|
+
debugName?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const usePromise = <T = any>(options?: UsePromiseOptions<T>) => {
|
|
12
|
+
const rejectRef = useRef<AnyFunction>()
|
|
13
|
+
const resolveRef = useRef<(v:T) => any>()
|
|
14
|
+
const timeoutRef = useRef(null)
|
|
15
|
+
const reject = async (err: any) => {
|
|
16
|
+
await rejectRef.current?.(err)
|
|
17
|
+
options?.onReject?.(err)
|
|
18
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
|
19
|
+
rejectRef.current = null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const resolve = async (value: T) => {
|
|
23
|
+
await resolveRef.current?.(value)
|
|
24
|
+
options?.onResolve?.(value)
|
|
25
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current)
|
|
26
|
+
resolveRef.current = null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const _await = () => {
|
|
30
|
+
return new Promise<T>((resolve, reject) => {
|
|
31
|
+
rejectRef.current = reject
|
|
32
|
+
resolveRef.current = resolve
|
|
33
|
+
if (TypeGuards.isNumber(options?.timeout) && options?.timeout > 0) {
|
|
34
|
+
timeoutRef.current = setTimeout(() => {
|
|
35
|
+
reject(new Error(`usePromise: ${options?.debugName || ''} timed out after ${options?.timeout}ms`))
|
|
36
|
+
}, options?.timeout)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
_await,
|
|
43
|
+
resolve,
|
|
44
|
+
reject,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useBooleanToggle } from '../useBooleanToggle'
|
|
3
|
+
import { useSearchParams } from './types'
|
|
4
|
+
import { TypeGuards } from '@codeleap/types'
|
|
5
|
+
|
|
6
|
+
export function useSearch<T extends string|number = string, Multi extends boolean = false>(props: useSearchParams<T, Multi>) {
|
|
7
|
+
const {
|
|
8
|
+
value,
|
|
9
|
+
multiple,
|
|
10
|
+
options,
|
|
11
|
+
filterItems,
|
|
12
|
+
debugName,
|
|
13
|
+
defaultOptions,
|
|
14
|
+
|
|
15
|
+
loadOptions,
|
|
16
|
+
onLoadOptionsError,
|
|
17
|
+
} = { ...props }
|
|
18
|
+
|
|
19
|
+
const [loading, setLoading] = useBooleanToggle(false)
|
|
20
|
+
const isValueArray = TypeGuards.isArray(value) && multiple
|
|
21
|
+
const [labelOptions, setLabelOptions] = useState(() => {
|
|
22
|
+
if (isValueArray) {
|
|
23
|
+
return defaultOptions.filter(o => value.includes(o.value))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const _option = defaultOptions.find(o => o.value === value)
|
|
27
|
+
|
|
28
|
+
if (!_option) {
|
|
29
|
+
return []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return [_option]
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const [filteredOptions, setFilteredOptions] = useState(defaultOptions)
|
|
36
|
+
const [, setSearchInput] = useState('')
|
|
37
|
+
|
|
38
|
+
async function load() {
|
|
39
|
+
setLoading(true)
|
|
40
|
+
try {
|
|
41
|
+
const options = await loadOptions('')
|
|
42
|
+
setFilteredOptions(options)
|
|
43
|
+
} catch (e) {
|
|
44
|
+
onLoadOptionsError(e)
|
|
45
|
+
}
|
|
46
|
+
setLoading(false)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const onChangeSearch = async (searchValue:string) => {
|
|
50
|
+
setSearchInput(searchValue)
|
|
51
|
+
|
|
52
|
+
if (!!loadOptions) {
|
|
53
|
+
setLoading(true)
|
|
54
|
+
try {
|
|
55
|
+
const _opts = await loadOptions(searchValue)
|
|
56
|
+
setFilteredOptions(_opts)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error(`Error loading select options [${debugName}], e`)
|
|
59
|
+
onLoadOptionsError?.(e)
|
|
60
|
+
}
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
setLoading(false)
|
|
63
|
+
}, 0)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const _opts = filterItems(searchValue, options)
|
|
67
|
+
setFilteredOptions(_opts)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { loading, setLoading, labelOptions, setLabelOptions, filteredOptions, load, onChangeSearch }
|
|
71
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type AutocompleteValue<T, Multi extends boolean = false> = Multi extends true ? T[] : T
|
|
2
|
+
|
|
3
|
+
type Options<T> = { label: any; value: T }[]
|
|
4
|
+
|
|
5
|
+
export type useSearchParams<T, Multi extends boolean = false> = {
|
|
6
|
+
value: AutocompleteValue<T, Multi>
|
|
7
|
+
multiple:Multi
|
|
8
|
+
options: Options<T>
|
|
9
|
+
filterItems: (search: string, items: Options<T>) => Options<T>
|
|
10
|
+
debugName: string
|
|
11
|
+
defaultOptions: Options<T>
|
|
12
|
+
loadOptions: (search: string) => Promise<Options<T>>
|
|
13
|
+
onLoadOptionsError: (error: any) => void
|
|
14
|
+
}
|
package/src/useToggle.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export function useToggle<T extends readonly [any, any], V extends T[0] | T[1]>(
|
|
4
|
+
options: T,
|
|
5
|
+
initial: V,
|
|
6
|
+
) {
|
|
7
|
+
const [value, setValue] = useState(initial)
|
|
8
|
+
|
|
9
|
+
function toggleOrSetValue(newValue?: V) {
|
|
10
|
+
const v: V = newValue || (value === options[0] ? options[1] : options[0])
|
|
11
|
+
|
|
12
|
+
setValue(v)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return [value, toggleOrSetValue] as const
|
|
16
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
export type UncontrolledMode = 'initial' | 'controlled' | 'uncontrolled'
|
|
4
|
+
|
|
5
|
+
export interface UncontrolledOptions<T> {
|
|
6
|
+
value: T | null | undefined
|
|
7
|
+
defaultValue: T | null | undefined
|
|
8
|
+
finalValue: T | null
|
|
9
|
+
onChange(value: T | null): void
|
|
10
|
+
onValueUpdate?(value: T | null): void
|
|
11
|
+
rule: (value: T | null | undefined) => boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useUncontrolled<T>({
|
|
15
|
+
value,
|
|
16
|
+
defaultValue,
|
|
17
|
+
finalValue,
|
|
18
|
+
rule,
|
|
19
|
+
onChange,
|
|
20
|
+
onValueUpdate,
|
|
21
|
+
}: UncontrolledOptions<T>): readonly [T | null, (nextValue: T | null) => void, UncontrolledMode] {
|
|
22
|
+
// determine, whether new props indicate controlled state
|
|
23
|
+
const shouldBeControlled = rule(value)
|
|
24
|
+
|
|
25
|
+
// initialize state
|
|
26
|
+
const modeRef = useRef<UncontrolledMode>('initial')
|
|
27
|
+
const initialValue = rule(defaultValue) ? defaultValue : finalValue
|
|
28
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(initialValue)
|
|
29
|
+
|
|
30
|
+
// compute effective value
|
|
31
|
+
let effectiveValue = shouldBeControlled ? value : uncontrolledValue
|
|
32
|
+
|
|
33
|
+
if (!shouldBeControlled && modeRef.current === 'controlled') {
|
|
34
|
+
// We are transitioning from controlled to uncontrolled
|
|
35
|
+
// this transition is special as it happens when clearing out
|
|
36
|
+
// the input using "invalid" value (typically null or undefined).
|
|
37
|
+
//
|
|
38
|
+
// Since the value is invalid, doing nothing would mean just
|
|
39
|
+
// transitioning to uncontrolled state and using whatever value
|
|
40
|
+
// it currently holds which is likely not the behavior
|
|
41
|
+
// user expects, so lets change the state to finalValue.
|
|
42
|
+
//
|
|
43
|
+
// The value will be propagated to internal state by useEffect below.
|
|
44
|
+
|
|
45
|
+
effectiveValue = finalValue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
modeRef.current = shouldBeControlled ? 'controlled' : 'uncontrolled'
|
|
49
|
+
const mode = modeRef.current
|
|
50
|
+
|
|
51
|
+
const handleChange = (nextValue: T | null) => {
|
|
52
|
+
typeof onChange === 'function' && onChange(nextValue)
|
|
53
|
+
|
|
54
|
+
// Controlled input only triggers onChange event and expects
|
|
55
|
+
// the controller to propagate new value back.
|
|
56
|
+
if (mode === 'uncontrolled') {
|
|
57
|
+
setUncontrolledValue(nextValue)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (mode === 'uncontrolled') {
|
|
63
|
+
setUncontrolledValue(effectiveValue)
|
|
64
|
+
}
|
|
65
|
+
typeof onValueUpdate === 'function' && onValueUpdate(effectiveValue)
|
|
66
|
+
}, [mode, effectiveValue])
|
|
67
|
+
|
|
68
|
+
return [effectiveValue, handleChange, modeRef.current] as const
|
|
69
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import { useEffectOnce } from './useEffectOnce'
|
|
3
|
+
|
|
4
|
+
export const useUnmount = (fn: () => any): void => {
|
|
5
|
+
const fnRef = useRef(fn)
|
|
6
|
+
|
|
7
|
+
// update the ref each render so if it change the newest callback will be invoked
|
|
8
|
+
fnRef.current = fn
|
|
9
|
+
|
|
10
|
+
useEffectOnce(() => () => fnRef.current())
|
|
11
|
+
}
|