-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathLocalStorageDropdown.tsx
131 lines (104 loc) · 5.91 KB
/
LocalStorageDropdown.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//wrapper around KernDropdown to + input filed to read local storage data and provide it as dropdown options
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
import { useDefaults, useDefaultsByRef } from "../hooks/useDefaults";
import { CompareOptions, inStringList } from "@/submodules/javascript-functions/validations";
import { LOCAL_STORAGE_DROPDOWN_DEFAULTS, LocalStorageDropdownProps } from "../types/localStorageDropdown";
import KernDropdown from "./KernDropdown";
function readFromLocalStorage(group: string, key: string): string[] {
const itemGroupString = localStorage.getItem(group);
if (itemGroupString) {
const itemGroup = JSON.parse(itemGroupString);
return itemGroup[key] ?? [];
}
return [];
}
function valueIsValid(addValue: string, excludedFromStorage?: { values: string[]; compareOptions?: CompareOptions[] }): boolean {
if (!addValue || addValue.trim().length == 0) return false;
if (excludedFromStorage && inStringList(addValue, excludedFromStorage.values, excludedFromStorage.compareOptions)) return false;
return true;
}
function extendLocalStorage(group: string, key: string, addValue: string): string[] {
const itemGroupString = localStorage.getItem(group);
let itemGroup = {};
if (itemGroupString) {
itemGroup = JSON.parse(itemGroupString);
}
if (!itemGroup[key]) itemGroup[key] = [];
if (!Array.isArray(itemGroup[key])) throw new Error("LocalStorageDropdown - extendLocalStorage - itemGroup[key] is not an array");
if (itemGroup[key].includes(addValue)) return itemGroup[key]; //already in list
itemGroup[key].push(addValue);
localStorage.setItem(group, JSON.stringify(itemGroup));
return itemGroup[key]
}
function removeFromLocalStorage(group: string, key: string, rValue: string): string[] {
const itemGroupString = localStorage.getItem(group);
let itemGroup = {};
if (itemGroupString) itemGroup = JSON.parse(itemGroupString);
else return [];
if (!itemGroup[key]) return [];
if (!Array.isArray(itemGroup[key])) throw new Error("LocalStorageDropdown - removeFromLocalStorage - itemGroup[key] is not an array");
if (!itemGroup[key].includes(rValue)) return itemGroup[key]; //not in list
itemGroup[key] = itemGroup[key].filter(x => x != rValue);
localStorage.setItem(group, JSON.stringify(itemGroup));
return itemGroup[key];
}
export const LocalStorageDropdown = forwardRef((_props: LocalStorageDropdownProps, ref) => {
const [props] = useDefaults<LocalStorageDropdownProps>(_props, LOCAL_STORAGE_DROPDOWN_DEFAULTS);
const [propRef] = useDefaultsByRef<LocalStorageDropdownProps>(_props, LOCAL_STORAGE_DROPDOWN_DEFAULTS); // for unmounting
const [options, setOptions] = useState<string[]>(); // initially built with useLocalStorage however as state setters don't work during unmount changed to a common state
const [inputText, setInputText] = useState(props.buttonName ?? ''); // holds the current option independent of input field or dropdown so it can be collected if necessary
const inputTextRef = useRef<string>(); //ref is used to access data from a pointer which in turn can be access in the unmounting
const onOptionSelected = useCallback((option: string) => {
setInputText(option); // ensure it's always set
if (props.onOptionSelected) props.onOptionSelected(option);
}, [props.onOptionSelected]);
const onClickDelete = useCallback((option: string) => {
setOptions(removeFromLocalStorage(props.storageGroupKey, props.storageKey, option));
}, [props.storageGroupKey, props.storageKey]);
useImperativeHandle(ref, () => ({
persistValue() {
if (valueIsValid(inputTextRef.current, propRef.current.excludedFromStorage)) {
setOptions(extendLocalStorage(propRef.current.storageGroupKey, propRef.current.storageKey, inputTextRef.current));
}
}
}), []);
useEffect(() => { inputTextRef.current = inputText }, [inputText]);
useEffect(() => {
setOptions(readFromLocalStorage(props.storageGroupKey, props.storageKey));
}, [props.storageGroupKey, props.storageKey])
useEffect(() => {
// return in useEffect is run on unmount (when the component is destroyed or with a change array "before" the values change)
// since this can't collect data from outdated references & can't use the change array since this would then run on every change & setter aren't available anymore
// we need to use a ref to access the data
return () => {
if (valueIsValid(inputTextRef.current, propRef.current.excludedFromStorage)) {
extendLocalStorage(propRef.current.storageGroupKey, propRef.current.storageKey, inputTextRef.current);
}
}
}, [])
if (!options) return; // wait for options to be loaded
return <>
{options.length > 0 ?
<KernDropdown
buttonName={props.buttonName ?? 'Select'}
searchDefaultValue={props.searchDefaultValue}
options={options}
hasSearchBar={true}
selectedOption={onOptionSelected}
onClickDelete={onClickDelete}
searchTextTyped={(inputText) => {
setInputText(inputText)
onOptionSelected(inputText)
}}
disabled={props.disabled}
/> :
<input value={inputText} onChange={(e) => {
onOptionSelected(e.target.value);
}}
className="h-9 w-full text-sm border-gray-300 rounded-md placeholder-italic border text-gray-900 pl-4 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-300 focus:ring-offset-2 focus:ring-offset-gray-100"
placeholder="Enter value..."
onFocus={(event) => event.target.select()}
disabled={props.disabled} />
}
</>
});