@thom1729/react-utils 0.0.2
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/LICENSE +21 -0
- package/dist/esm/components/helpers.d.ts +2 -0
- package/dist/esm/components/index.d.ts +6 -0
- package/dist/esm/components/input.d.ts +18 -0
- package/dist/esm/components/select.d.ts +27 -0
- package/dist/esm/hooks/index.d.ts +1 -0
- package/dist/esm/hooks/useEventListener.d.ts +5 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +266 -0
- package/dist/esm/substate/hooks.d.ts +3 -0
- package/dist/esm/substate/index.d.ts +2 -0
- package/dist/esm/substate/providers.d.ts +21 -0
- package/dist/esm/substate/providers.test.d.ts +1 -0
- package/jest.config.cjs +8 -0
- package/package.json +37 -0
- package/rollup.config.js +16 -0
- package/src/components/helpers.ts +20 -0
- package/src/components/index.tsx +11 -0
- package/src/components/input.tsx +95 -0
- package/src/components/select.tsx +116 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useEventListener.ts +42 -0
- package/src/index.ts +3 -0
- package/src/substate/hooks.ts +18 -0
- package/src/substate/index.ts +2 -0
- package/src/substate/providers.test.ts +99 -0
- package/src/substate/providers.ts +126 -0
- package/tsconfig.json +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Thomas Smith
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export type ExtendBuiltin<TElementName extends keyof JSX.IntrinsicElements, TAdditional extends object = {}, TOmit extends string = never> = TAdditional & Omit<JSX.IntrinsicElements[TElementName], keyof TAdditional | TOmit>;
|
|
2
|
+
export declare function useWrappedHandler<TEvent, TReturn>(callback: undefined | ((value: TReturn) => void), convert: (event: TEvent) => TReturn, dependencies: readonly unknown[]): ((event: TEvent) => void) | undefined;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type FC } from 'react';
|
|
2
|
+
import { type ExtendBuiltin } from './helpers';
|
|
3
|
+
export declare const TextInput: FC<ExtendBuiltin<'input', {
|
|
4
|
+
value: string;
|
|
5
|
+
onChange?: (value: string) => void;
|
|
6
|
+
}>>;
|
|
7
|
+
export declare const NumberInput: FC<ExtendBuiltin<'input', {
|
|
8
|
+
value: number;
|
|
9
|
+
onChange?: (value: number) => void;
|
|
10
|
+
}>>;
|
|
11
|
+
export declare const CheckboxInput: FC<ExtendBuiltin<'input', {
|
|
12
|
+
value: boolean;
|
|
13
|
+
onChange?: (value: boolean) => void;
|
|
14
|
+
}, 'checked' | 'type'>>;
|
|
15
|
+
export declare const Textarea: FC<ExtendBuiltin<'textarea', {
|
|
16
|
+
value: string;
|
|
17
|
+
onChange?: (value: string) => void;
|
|
18
|
+
}>>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ExtendBuiltin } from './helpers';
|
|
2
|
+
export type SelectOptionLeaf<TValue> = {
|
|
3
|
+
value: TValue & (string | number);
|
|
4
|
+
key?: string | number;
|
|
5
|
+
label?: string;
|
|
6
|
+
children?: undefined;
|
|
7
|
+
} | {
|
|
8
|
+
value: TValue;
|
|
9
|
+
key: string | number;
|
|
10
|
+
label: string;
|
|
11
|
+
children?: undefined;
|
|
12
|
+
};
|
|
13
|
+
export type SelectOption<TValue> = SelectOptionLeaf<TValue> | {
|
|
14
|
+
value?: undefined;
|
|
15
|
+
key: string | number;
|
|
16
|
+
label: string;
|
|
17
|
+
children: readonly SelectOption<TValue>[];
|
|
18
|
+
};
|
|
19
|
+
export declare function getSelectMaps<TValue>(options: readonly SelectOption<TValue>[]): {
|
|
20
|
+
keyToValue: Map<string | number, TValue | (TValue & (string | number))>;
|
|
21
|
+
valueToKey: Map<TValue | (TValue & (string | number)), string | number>;
|
|
22
|
+
};
|
|
23
|
+
export declare function Select<const TValue>({ options, value, onChange, ...rest }: ExtendBuiltin<'select', {
|
|
24
|
+
options: readonly SelectOption<TValue>[];
|
|
25
|
+
value: TValue;
|
|
26
|
+
onChange?: ((value: TValue) => void) | undefined;
|
|
27
|
+
}, 'children'>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './useEventListener.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type EventTypes<TTarget extends EventTarget> = {
|
|
2
|
+
[K in keyof TTarget as (K extends `on${infer Name}` ? Name : never)]: Exclude<TTarget[K], null | undefined> extends ((event: infer TEvent extends Event) => unknown) ? TEvent : never;
|
|
3
|
+
};
|
|
4
|
+
export declare function useEventListener<TTarget extends EventTarget, TEventType extends keyof EventTypes<TTarget>>(target: TTarget | undefined, type: TEventType, listener: ((event: EventTypes<TTarget>[TEventType]) => void) | undefined, dependencies: readonly unknown[]): void;
|
|
5
|
+
export declare function useEventListener(target: EventTarget | undefined, type: string, listener: ((event: Event) => void) | undefined, dependencies: readonly unknown[]): void;
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { useMemo, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
function useWrappedHandler(callback, convert, dependencies) {
|
|
5
|
+
return useMemo(() => callback && ((event) => {
|
|
6
|
+
callback(convert(event));
|
|
7
|
+
}), [dependencies]);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TextInput = ({ type = 'text', value, onChange, ...rest }) => {
|
|
11
|
+
const onChangeCallback = useWrappedHandler(onChange, event => event.currentTarget.value, [onChange]);
|
|
12
|
+
return jsx("input", { type: type, value: value, onChange: onChangeCallback, ...rest });
|
|
13
|
+
};
|
|
14
|
+
const NumberInput = ({ type = 'number', value, onChange, ...rest }) => {
|
|
15
|
+
const onChangeCallback = useWrappedHandler(onChange, event => event.currentTarget.valueAsNumber, [onChange]);
|
|
16
|
+
return jsx("input", { type: type, value: value, onChange: onChangeCallback, ...rest });
|
|
17
|
+
};
|
|
18
|
+
const CheckboxInput = ({ value, onChange, ...rest }) => {
|
|
19
|
+
const onChangeCallback = useWrappedHandler(onChange, event => event.currentTarget.checked, [onChange]);
|
|
20
|
+
return jsx("input", { type: 'checkbox', checked: value, onChange: onChangeCallback, ...rest });
|
|
21
|
+
};
|
|
22
|
+
const Textarea = ({ value, onChange, ...rest }) => {
|
|
23
|
+
const onChangeCallback = useWrappedHandler(onChange, event => event.currentTarget.value, [onChange]);
|
|
24
|
+
return jsx("textarea", { value: value, onChange: onChangeCallback, ...rest });
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function* iterateOptions(options) {
|
|
28
|
+
for (const option of options) {
|
|
29
|
+
if (option.children !== undefined) {
|
|
30
|
+
yield* iterateOptions(option.children);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
yield option;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function getSelectMaps(options) {
|
|
38
|
+
const optionsArray = Array.from(iterateOptions(options));
|
|
39
|
+
return {
|
|
40
|
+
keyToValue: new Map(optionsArray.map(({ key, value }) => [key ?? value, value])),
|
|
41
|
+
valueToKey: new Map(optionsArray.map(({ key, value }) => [value, key ?? value])),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function Select({ options, value, onChange, ...rest }) {
|
|
45
|
+
const { keyToValue, valueToKey } = useMemo(() => getSelectMaps(options), [options]);
|
|
46
|
+
const onChangeCallback = useWrappedHandler(onChange, event => {
|
|
47
|
+
const result = keyToValue.get(event.currentTarget.value);
|
|
48
|
+
if (result === undefined)
|
|
49
|
+
throw new TypeError(typeof event.currentTarget.value);
|
|
50
|
+
return result;
|
|
51
|
+
}, [onChange, keyToValue]);
|
|
52
|
+
const selectedKey = valueToKey.get(value);
|
|
53
|
+
if (selectedKey === undefined)
|
|
54
|
+
throw new TypeError();
|
|
55
|
+
return jsx("select", { onChange: onChangeCallback, value: selectedKey, ...rest, children: jsx(SelectOptions, { options: options }) });
|
|
56
|
+
}
|
|
57
|
+
function SelectOptions({ options, }) {
|
|
58
|
+
return jsx(Fragment, { children: options.map(option => {
|
|
59
|
+
if (option.children !== undefined) {
|
|
60
|
+
return jsx("optgroup", { label: option.label, children: jsx(SelectOptions, { options: option.children }) }, option.key);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
return jsx("option", { value: option.key ?? option.value, children: option.label }, option.key ?? option.value);
|
|
64
|
+
}
|
|
65
|
+
}) });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const Button = ({ type = 'button', ...rest }) => jsx("button", { type: type, ...rest });
|
|
69
|
+
|
|
70
|
+
function useEventListener(target, type, listener, dependencies) {
|
|
71
|
+
const callback = useMemo(() => listener, dependencies);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (target !== undefined && callback !== undefined) {
|
|
74
|
+
target.addEventListener(type, callback);
|
|
75
|
+
return () => { target.removeEventListener(type, callback); };
|
|
76
|
+
}
|
|
77
|
+
}, [target, type, callback]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/******************************************************************************
|
|
81
|
+
Copyright (c) Microsoft Corporation.
|
|
82
|
+
|
|
83
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
84
|
+
purpose with or without fee is hereby granted.
|
|
85
|
+
|
|
86
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
87
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
88
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
89
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
90
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
91
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
92
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
93
|
+
***************************************************************************** */
|
|
94
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
98
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
99
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
100
|
+
var target = ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
101
|
+
var descriptor = (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
102
|
+
var _, done = false;
|
|
103
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
104
|
+
var context = {};
|
|
105
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
106
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
107
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
108
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
109
|
+
if (kind === "accessor") {
|
|
110
|
+
if (result === void 0) continue;
|
|
111
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
112
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
113
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
114
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
115
|
+
}
|
|
116
|
+
else if (_ = accept(result)) {
|
|
117
|
+
if (kind === "field") initializers.unshift(_);
|
|
118
|
+
else descriptor[key] = _;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
122
|
+
done = true;
|
|
123
|
+
}
|
|
124
|
+
function __runInitializers(thisArg, initializers, value) {
|
|
125
|
+
var useValue = arguments.length > 2;
|
|
126
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
127
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
128
|
+
}
|
|
129
|
+
return useValue ? value : void 0;
|
|
130
|
+
}
|
|
131
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
132
|
+
var e = new Error(message);
|
|
133
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
function applyStateUpdater(value, prevState) {
|
|
137
|
+
if (typeof value === 'function') {
|
|
138
|
+
return value(prevState);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function cached(originalMethod, context) {
|
|
145
|
+
const cacheProperty = Symbol(`__cache__${context.name.toString()}`);
|
|
146
|
+
context.addInitializer(function () {
|
|
147
|
+
this[cacheProperty] = new Map();
|
|
148
|
+
});
|
|
149
|
+
const wrappedName = `__wrapped__${context.name.toString()}`;
|
|
150
|
+
return {
|
|
151
|
+
[wrappedName](key) {
|
|
152
|
+
const cache = this[cacheProperty];
|
|
153
|
+
if (cache.has(key)) {
|
|
154
|
+
return cache.get(key);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const result = originalMethod.call(this, key);
|
|
158
|
+
cache.set(key, result);
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
}[wrappedName];
|
|
163
|
+
}
|
|
164
|
+
let ObjectSubstateProvider = (() => {
|
|
165
|
+
let _instanceExtraInitializers = [];
|
|
166
|
+
let _set_decorators;
|
|
167
|
+
let _delete_decorators;
|
|
168
|
+
return class ObjectSubstateProvider {
|
|
169
|
+
static {
|
|
170
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
171
|
+
_set_decorators = [cached];
|
|
172
|
+
_delete_decorators = [cached];
|
|
173
|
+
__esDecorate(this, null, _set_decorators, { kind: "method", name: "set", static: false, private: false, access: { has: obj => "set" in obj, get: obj => obj.set }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
174
|
+
__esDecorate(this, null, _delete_decorators, { kind: "method", name: "delete", static: false, private: false, access: { has: obj => "delete" in obj, get: obj => obj.delete }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
175
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
176
|
+
}
|
|
177
|
+
setState = __runInitializers(this, _instanceExtraInitializers);
|
|
178
|
+
constructor(setState) {
|
|
179
|
+
this.setState = setState;
|
|
180
|
+
}
|
|
181
|
+
set(key) {
|
|
182
|
+
return value => this.setState(prevState => ({
|
|
183
|
+
...prevState,
|
|
184
|
+
[key]: applyStateUpdater(value, prevState[key]),
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
delete(key) {
|
|
188
|
+
return () => this.setState(({ [key]: _, ...rest }) => rest);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
})();
|
|
192
|
+
let ArraySubstateProvider = (() => {
|
|
193
|
+
let _instanceExtraInitializers = [];
|
|
194
|
+
let _set_decorators;
|
|
195
|
+
let _delete_decorators;
|
|
196
|
+
let _moveTo_decorators;
|
|
197
|
+
let _moveBy_decorators;
|
|
198
|
+
return class ArraySubstateProvider {
|
|
199
|
+
static {
|
|
200
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
201
|
+
_set_decorators = [cached];
|
|
202
|
+
_delete_decorators = [cached];
|
|
203
|
+
_moveTo_decorators = [cached];
|
|
204
|
+
_moveBy_decorators = [cached];
|
|
205
|
+
__esDecorate(this, null, _set_decorators, { kind: "method", name: "set", static: false, private: false, access: { has: obj => "set" in obj, get: obj => obj.set }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
206
|
+
__esDecorate(this, null, _delete_decorators, { kind: "method", name: "delete", static: false, private: false, access: { has: obj => "delete" in obj, get: obj => obj.delete }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
207
|
+
__esDecorate(this, null, _moveTo_decorators, { kind: "method", name: "moveTo", static: false, private: false, access: { has: obj => "moveTo" in obj, get: obj => obj.moveTo }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
208
|
+
__esDecorate(this, null, _moveBy_decorators, { kind: "method", name: "moveBy", static: false, private: false, access: { has: obj => "moveBy" in obj, get: obj => obj.moveBy }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
209
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
210
|
+
}
|
|
211
|
+
setState = __runInitializers(this, _instanceExtraInitializers);
|
|
212
|
+
constructor(setState) {
|
|
213
|
+
this.setState = setState;
|
|
214
|
+
}
|
|
215
|
+
set(index) {
|
|
216
|
+
return value => this.setState(prevState => [
|
|
217
|
+
...prevState.slice(0, index),
|
|
218
|
+
applyStateUpdater(value, prevState[index]),
|
|
219
|
+
...prevState.slice(index + 1),
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
222
|
+
delete(index) {
|
|
223
|
+
return () => this.setState(prevState => [
|
|
224
|
+
...prevState.slice(0, index),
|
|
225
|
+
...prevState.slice(index + 1),
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
;
|
|
229
|
+
moveTo(index) {
|
|
230
|
+
return (newIndex) => this.setState(prevState => {
|
|
231
|
+
const rest = [
|
|
232
|
+
...prevState.slice(0, index),
|
|
233
|
+
...prevState.slice(index + 1),
|
|
234
|
+
];
|
|
235
|
+
return [
|
|
236
|
+
...rest.slice(0, newIndex),
|
|
237
|
+
prevState[index],
|
|
238
|
+
...rest.slice(newIndex),
|
|
239
|
+
];
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
;
|
|
243
|
+
moveBy(index) {
|
|
244
|
+
const moveTo = this.moveTo(index);
|
|
245
|
+
return (diff) => moveTo(index + diff);
|
|
246
|
+
}
|
|
247
|
+
;
|
|
248
|
+
prepend = value => this.setState(prevState => [
|
|
249
|
+
applyStateUpdater(value, prevState),
|
|
250
|
+
...prevState,
|
|
251
|
+
]);
|
|
252
|
+
append = value => this.setState(prevState => [
|
|
253
|
+
...prevState,
|
|
254
|
+
applyStateUpdater(value, prevState),
|
|
255
|
+
]);
|
|
256
|
+
};
|
|
257
|
+
})();
|
|
258
|
+
|
|
259
|
+
function useObjectSubstates(setState) {
|
|
260
|
+
return useMemo(() => new ObjectSubstateProvider(setState), [setState]);
|
|
261
|
+
}
|
|
262
|
+
function useArraySubstates(setState) {
|
|
263
|
+
return useMemo(() => new ArraySubstateProvider(setState), [setState]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export { ArraySubstateProvider, Button, CheckboxInput, NumberInput, ObjectSubstateProvider, Select, TextInput, Textarea, applyStateUpdater, getSelectMaps, useArraySubstates, useEventListener, useObjectSubstates, useWrappedHandler };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ObjectSubstateProvider, ArraySubstateProvider, type StateUpdater } from './providers.js';
|
|
2
|
+
export declare function useObjectSubstates<TBase extends object>(setState: StateUpdater<TBase>): ObjectSubstateProvider<TBase>;
|
|
3
|
+
export declare function useArraySubstates<TItem>(setState: StateUpdater<TItem[], readonly TItem[]>): ArraySubstateProvider<TItem>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type StateUpdater<TState, TPrevState = TState> = (value: TState | ((prevState: TPrevState) => TState)) => void;
|
|
2
|
+
export declare function applyStateUpdater<TState, TPrevState>(value: TState | ((prevState: TPrevState) => void), prevState: TPrevState): TState;
|
|
3
|
+
export type OptionalKeyOf<T extends object> = {
|
|
4
|
+
[K in keyof T]: T extends Record<K, T[K]> ? never : K;
|
|
5
|
+
}[keyof T];
|
|
6
|
+
export declare class ObjectSubstateProvider<TBase extends object> {
|
|
7
|
+
private readonly setState;
|
|
8
|
+
constructor(setState: StateUpdater<TBase>);
|
|
9
|
+
set<TKey extends keyof TBase>(key: TKey): StateUpdater<TBase[TKey]>;
|
|
10
|
+
delete(key: OptionalKeyOf<TBase>): () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare class ArraySubstateProvider<TItem> {
|
|
13
|
+
private readonly setState;
|
|
14
|
+
constructor(setState: StateUpdater<TItem[], readonly TItem[]>);
|
|
15
|
+
set(index: number): StateUpdater<TItem>;
|
|
16
|
+
delete(index: number): () => void;
|
|
17
|
+
moveTo(index: number): (newIndex: number) => void;
|
|
18
|
+
moveBy(index: number): (diff: number) => void;
|
|
19
|
+
prepend: StateUpdater<TItem, readonly TItem[]>;
|
|
20
|
+
append: StateUpdater<TItem, readonly TItem[]>;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/jest.config.cjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thom1729/react-utils",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "rollup --config rollup.config.js",
|
|
10
|
+
"test": "jest"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/Thom1729/react-utils.git"
|
|
15
|
+
},
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/Thom1729/react-utils/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/Thom1729/react-utils#readme",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@rollup/plugin-typescript": "^11.1.6",
|
|
24
|
+
"@types/jest": "^29.5.12",
|
|
25
|
+
"@types/react": "^18.3.5",
|
|
26
|
+
"@types/react-dom": "^18.3.0",
|
|
27
|
+
"jest": "^29.7.0",
|
|
28
|
+
"rollup": "^4.21.2",
|
|
29
|
+
"rollup-plugin-ts": "^3.4.5",
|
|
30
|
+
"ts-jest": "^29.1.3",
|
|
31
|
+
"typescript": "^5.6.2"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"react": "^18.3.1",
|
|
35
|
+
"tslib": "^2.7.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ExtendBuiltin<
|
|
4
|
+
TElementName extends keyof JSX.IntrinsicElements,
|
|
5
|
+
TAdditional extends object = {},
|
|
6
|
+
TOmit extends string = never,
|
|
7
|
+
> = TAdditional & Omit<JSX.IntrinsicElements[TElementName], keyof TAdditional | TOmit>;
|
|
8
|
+
|
|
9
|
+
export function useWrappedHandler<TEvent, TReturn>(
|
|
10
|
+
callback: undefined | ((value: TReturn) => void),
|
|
11
|
+
convert: (event: TEvent) => TReturn,
|
|
12
|
+
dependencies: readonly unknown[],
|
|
13
|
+
) {
|
|
14
|
+
return useMemo(
|
|
15
|
+
() => callback && ((event: TEvent) => {
|
|
16
|
+
callback(convert(event));
|
|
17
|
+
}),
|
|
18
|
+
[dependencies], // eslint-disable-line react-hooks/exhaustive-deps
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './helpers.js';
|
|
2
|
+
export * from './input.js';
|
|
3
|
+
export * from './select.js';
|
|
4
|
+
|
|
5
|
+
import { type FC } from 'react';
|
|
6
|
+
import type { ExtendBuiltin } from './helpers.js';
|
|
7
|
+
|
|
8
|
+
export const Button: FC<ExtendBuiltin<'button'>> = ({
|
|
9
|
+
type = 'button',
|
|
10
|
+
...rest
|
|
11
|
+
}) => <button type={type} {...rest} />;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FC,
|
|
3
|
+
type ChangeEvent,
|
|
4
|
+
} from 'react';
|
|
5
|
+
|
|
6
|
+
import { useWrappedHandler, type ExtendBuiltin } from './helpers';
|
|
7
|
+
|
|
8
|
+
export const TextInput: FC<ExtendBuiltin<'input', {
|
|
9
|
+
value: string,
|
|
10
|
+
onChange?: (value: string) => void,
|
|
11
|
+
}>> = ({
|
|
12
|
+
type = 'text',
|
|
13
|
+
value,
|
|
14
|
+
onChange,
|
|
15
|
+
...rest
|
|
16
|
+
}) => {
|
|
17
|
+
const onChangeCallback = useWrappedHandler<ChangeEvent<HTMLInputElement>, string>(
|
|
18
|
+
onChange,
|
|
19
|
+
event => event.currentTarget.value,
|
|
20
|
+
[onChange],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
return <input
|
|
24
|
+
type={type}
|
|
25
|
+
value={value}
|
|
26
|
+
onChange={onChangeCallback}
|
|
27
|
+
{...rest}
|
|
28
|
+
/>
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const NumberInput: FC<ExtendBuiltin<'input', {
|
|
32
|
+
value: number,
|
|
33
|
+
onChange?: (value: number) => void,
|
|
34
|
+
}>> = ({
|
|
35
|
+
type = 'number',
|
|
36
|
+
value,
|
|
37
|
+
onChange,
|
|
38
|
+
...rest
|
|
39
|
+
}) => {
|
|
40
|
+
const onChangeCallback = useWrappedHandler<ChangeEvent<HTMLInputElement>, number>(
|
|
41
|
+
onChange,
|
|
42
|
+
event => event.currentTarget.valueAsNumber,
|
|
43
|
+
[onChange],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return <input
|
|
47
|
+
type={type}
|
|
48
|
+
value={value}
|
|
49
|
+
onChange={onChangeCallback}
|
|
50
|
+
{...rest}
|
|
51
|
+
/>
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const CheckboxInput: FC<ExtendBuiltin<'input', {
|
|
55
|
+
value: boolean,
|
|
56
|
+
onChange?: (value: boolean) => void,
|
|
57
|
+
}, 'checked' | 'type'>> = ({
|
|
58
|
+
value,
|
|
59
|
+
onChange,
|
|
60
|
+
...rest
|
|
61
|
+
}) => {
|
|
62
|
+
const onChangeCallback = useWrappedHandler<ChangeEvent<HTMLInputElement>, boolean>(
|
|
63
|
+
onChange,
|
|
64
|
+
event => event.currentTarget.checked,
|
|
65
|
+
[onChange],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return <input
|
|
69
|
+
type='checkbox'
|
|
70
|
+
checked={value}
|
|
71
|
+
onChange={onChangeCallback}
|
|
72
|
+
{...rest}
|
|
73
|
+
/>
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const Textarea: FC<ExtendBuiltin<'textarea', {
|
|
77
|
+
value: string,
|
|
78
|
+
onChange?: (value: string) => void,
|
|
79
|
+
}>> = ({
|
|
80
|
+
value,
|
|
81
|
+
onChange,
|
|
82
|
+
...rest
|
|
83
|
+
}) => {
|
|
84
|
+
const onChangeCallback = useWrappedHandler<ChangeEvent<HTMLTextAreaElement>, string>(
|
|
85
|
+
onChange,
|
|
86
|
+
event => event.currentTarget.value,
|
|
87
|
+
[onChange],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return <textarea
|
|
91
|
+
value={value}
|
|
92
|
+
onChange={onChangeCallback}
|
|
93
|
+
{...rest}
|
|
94
|
+
/>
|
|
95
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
type ChangeEvent,
|
|
4
|
+
} from 'react';
|
|
5
|
+
|
|
6
|
+
import { useWrappedHandler, type ExtendBuiltin } from './helpers';
|
|
7
|
+
|
|
8
|
+
export type SelectOptionLeaf<TValue> =
|
|
9
|
+
| {
|
|
10
|
+
value: TValue & (string | number),
|
|
11
|
+
key?: string | number,
|
|
12
|
+
label?: string,
|
|
13
|
+
children?: undefined,
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
value: TValue,
|
|
17
|
+
key: string | number,
|
|
18
|
+
label: string,
|
|
19
|
+
children?: undefined,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SelectOption<TValue> =
|
|
23
|
+
| SelectOptionLeaf<TValue>
|
|
24
|
+
| {
|
|
25
|
+
value?: undefined,
|
|
26
|
+
key: string | number,
|
|
27
|
+
label: string,
|
|
28
|
+
children: readonly SelectOption<TValue>[],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function *iterateOptions<TValue>(
|
|
32
|
+
options: readonly SelectOption<TValue>[],
|
|
33
|
+
): Iterable<SelectOptionLeaf<TValue>> {
|
|
34
|
+
for (const option of options) {
|
|
35
|
+
if (option.children !== undefined) {
|
|
36
|
+
yield* iterateOptions(option.children);
|
|
37
|
+
} else {
|
|
38
|
+
yield option;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getSelectMaps<TValue>(
|
|
44
|
+
options: readonly SelectOption<TValue>[],
|
|
45
|
+
) {
|
|
46
|
+
const optionsArray = Array.from(iterateOptions(options));
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
keyToValue: new Map(optionsArray.map(({ key, value }) => [key ?? (value as string | number), value])),
|
|
50
|
+
valueToKey: new Map(optionsArray.map(({ key, value }) => [value, key ?? (value as string | number)])),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function Select<const TValue>({
|
|
55
|
+
options,
|
|
56
|
+
value,
|
|
57
|
+
onChange,
|
|
58
|
+
...rest
|
|
59
|
+
}: ExtendBuiltin<
|
|
60
|
+
'select',
|
|
61
|
+
{
|
|
62
|
+
options: readonly SelectOption<TValue>[],
|
|
63
|
+
value: TValue,
|
|
64
|
+
onChange?: ((value: TValue) => void) | undefined,
|
|
65
|
+
},
|
|
66
|
+
'children'
|
|
67
|
+
>) {
|
|
68
|
+
const { keyToValue, valueToKey } = useMemo(() => getSelectMaps(options), [options]);
|
|
69
|
+
|
|
70
|
+
const onChangeCallback = useWrappedHandler<ChangeEvent<HTMLSelectElement>, TValue>(
|
|
71
|
+
onChange,
|
|
72
|
+
event => {
|
|
73
|
+
const result = keyToValue.get(event.currentTarget.value);
|
|
74
|
+
if (result === undefined) throw new TypeError(typeof event.currentTarget.value);
|
|
75
|
+
return result;
|
|
76
|
+
},
|
|
77
|
+
[onChange, keyToValue],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const selectedKey = valueToKey.get(value);
|
|
81
|
+
if (selectedKey === undefined) throw new TypeError();
|
|
82
|
+
|
|
83
|
+
return <select
|
|
84
|
+
onChange={onChangeCallback}
|
|
85
|
+
value={selectedKey}
|
|
86
|
+
{...rest}
|
|
87
|
+
>
|
|
88
|
+
<SelectOptions options={options} />
|
|
89
|
+
</select>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function SelectOptions<TValue>({
|
|
93
|
+
options,
|
|
94
|
+
}: {
|
|
95
|
+
options: readonly SelectOption<TValue>[],
|
|
96
|
+
}) {
|
|
97
|
+
return <>
|
|
98
|
+
{options.map(option => {
|
|
99
|
+
if (option.children !== undefined) {
|
|
100
|
+
return <optgroup
|
|
101
|
+
key={option.key}
|
|
102
|
+
label={option.label}
|
|
103
|
+
>
|
|
104
|
+
<SelectOptions options={option.children}/>
|
|
105
|
+
</optgroup>
|
|
106
|
+
} else {
|
|
107
|
+
return <option
|
|
108
|
+
key={option.key ?? (option.value as string | number)}
|
|
109
|
+
value={option.key ?? (option.value as string | number)}
|
|
110
|
+
>
|
|
111
|
+
{option.label}
|
|
112
|
+
</option>;
|
|
113
|
+
}
|
|
114
|
+
})}
|
|
115
|
+
</>;
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './useEventListener.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useMemo, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export type EventTypes<TTarget extends EventTarget> = {
|
|
4
|
+
[K in keyof TTarget as (K extends `on${infer Name}` ? Name : never)]:
|
|
5
|
+
Exclude<TTarget[K], null | undefined> extends ((event: infer TEvent extends Event) => unknown)
|
|
6
|
+
? TEvent
|
|
7
|
+
: never
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function useEventListener<
|
|
11
|
+
TTarget extends EventTarget,
|
|
12
|
+
TEventType extends keyof EventTypes<TTarget>
|
|
13
|
+
>(
|
|
14
|
+
target: TTarget | undefined,
|
|
15
|
+
type: TEventType,
|
|
16
|
+
listener: ((event: EventTypes<TTarget>[TEventType]) => void) | undefined,
|
|
17
|
+
dependencies: readonly unknown[],
|
|
18
|
+
): void;
|
|
19
|
+
|
|
20
|
+
export function useEventListener(
|
|
21
|
+
target: EventTarget | undefined,
|
|
22
|
+
type: string,
|
|
23
|
+
listener: ((event: Event) => void) | undefined,
|
|
24
|
+
dependencies: readonly unknown[],
|
|
25
|
+
): void;
|
|
26
|
+
|
|
27
|
+
export function useEventListener(
|
|
28
|
+
target: EventTarget | undefined,
|
|
29
|
+
type: string,
|
|
30
|
+
listener: ((event: Event) => void) | undefined,
|
|
31
|
+
dependencies: readonly unknown[],
|
|
32
|
+
) {
|
|
33
|
+
const callback = useMemo(() => listener, dependencies);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (target !== undefined && callback !== undefined) {
|
|
37
|
+
target.addEventListener(type, callback);
|
|
38
|
+
|
|
39
|
+
return () => { target.removeEventListener(type, callback) };
|
|
40
|
+
}
|
|
41
|
+
}, [target, type, callback]);
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ObjectSubstateProvider, ArraySubstateProvider,
|
|
5
|
+
type StateUpdater,
|
|
6
|
+
} from './providers.js';
|
|
7
|
+
|
|
8
|
+
export function useObjectSubstates<TBase extends object>(
|
|
9
|
+
setState: StateUpdater<TBase>,
|
|
10
|
+
) {
|
|
11
|
+
return useMemo(() => new ObjectSubstateProvider(setState), [setState]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useArraySubstates<TItem>(
|
|
15
|
+
setState: StateUpdater<TItem[], readonly TItem[]>,
|
|
16
|
+
) {
|
|
17
|
+
return useMemo(() => new ArraySubstateProvider(setState), [setState]);
|
|
18
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { SetStateAction } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ObjectSubstateProvider, ArraySubstateProvider,
|
|
4
|
+
applyStateUpdater,
|
|
5
|
+
} from './providers.js';
|
|
6
|
+
|
|
7
|
+
class MockState<T> {
|
|
8
|
+
state: T;
|
|
9
|
+
|
|
10
|
+
constructor(state: T) {
|
|
11
|
+
this.state = state;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
setState(action: SetStateAction<T>) {
|
|
15
|
+
this.state = applyStateUpdater(action, this.state);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe(ObjectSubstateProvider, () => {
|
|
20
|
+
type State = {
|
|
21
|
+
string: string;
|
|
22
|
+
optional?: string;
|
|
23
|
+
number: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const initialState: State = {
|
|
27
|
+
string: 'a',
|
|
28
|
+
optional: 'a',
|
|
29
|
+
number: 1,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function setup() {
|
|
33
|
+
const mockState = new MockState(initialState);
|
|
34
|
+
const provider = new ObjectSubstateProvider(mockState.setState.bind(mockState));
|
|
35
|
+
return [mockState, provider] as const;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('set', () => {
|
|
39
|
+
const [mockState, provider] = setup();
|
|
40
|
+
|
|
41
|
+
expect(provider.set('string')).toBe(provider.set('string'));
|
|
42
|
+
|
|
43
|
+
provider.set('string')('b');
|
|
44
|
+
expect(mockState.state).toStrictEqual({
|
|
45
|
+
string: 'b',
|
|
46
|
+
optional: 'a',
|
|
47
|
+
number: 1,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('delete', () => {
|
|
52
|
+
const [mockState, provider] = setup();
|
|
53
|
+
|
|
54
|
+
expect(provider.delete('optional')).toBe(provider.delete('optional'));
|
|
55
|
+
|
|
56
|
+
provider.delete('optional')();
|
|
57
|
+
expect(mockState.state).not.toHaveProperty('optional');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe(ArraySubstateProvider, () => {
|
|
62
|
+
function setup() {
|
|
63
|
+
const mockState = new MockState([0, 1, 2]);
|
|
64
|
+
const provider = new ArraySubstateProvider(mockState.setState.bind(mockState));
|
|
65
|
+
return [mockState, provider] as const;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test('set', () => {
|
|
69
|
+
const [mockState, provider] = setup();
|
|
70
|
+
|
|
71
|
+
expect(provider.set(0)).toBe(provider.set(0));
|
|
72
|
+
|
|
73
|
+
provider.set(0)(10);
|
|
74
|
+
expect(mockState.state).toStrictEqual([10, 1, 2]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('delete', () => {
|
|
78
|
+
const [mockState, provider] = setup();
|
|
79
|
+
|
|
80
|
+
expect(provider.delete(0)).toBe(provider.delete(0));
|
|
81
|
+
|
|
82
|
+
provider.delete(1)();
|
|
83
|
+
expect(mockState.state).toStrictEqual([0, 2]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('prepend', () => {
|
|
87
|
+
const [mockState, provider] = setup();
|
|
88
|
+
|
|
89
|
+
(provider.prepend)(-1);
|
|
90
|
+
expect(mockState.state).toStrictEqual([-1, 0, 1, 2]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('apppend', () => {
|
|
94
|
+
const [mockState, provider] = setup();
|
|
95
|
+
|
|
96
|
+
(provider.append)(3);
|
|
97
|
+
expect(mockState.state).toStrictEqual([0, 1, 2, 3]);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export type StateUpdater<TState, TPrevState = TState> =
|
|
2
|
+
(value: TState | ((prevState: TPrevState) => TState)) => void;
|
|
3
|
+
|
|
4
|
+
export function applyStateUpdater<TState, TPrevState>(
|
|
5
|
+
value: TState | ((prevState: TPrevState) => void),
|
|
6
|
+
prevState: TPrevState,
|
|
7
|
+
) {
|
|
8
|
+
if (typeof value === 'function') {
|
|
9
|
+
return (value as (prevState: TPrevState) => TState)(prevState);
|
|
10
|
+
} else {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cached<TThis, TKey, TReturn>(
|
|
16
|
+
originalMethod: (this: TThis, key: TKey) => TReturn,
|
|
17
|
+
context: ClassMethodDecoratorContext,
|
|
18
|
+
): (this: TThis, key: TKey) => TReturn {
|
|
19
|
+
const cacheProperty = Symbol(`__cache__${context.name.toString()}`);
|
|
20
|
+
type HasCache = { [cacheProperty]: Map<TKey, TReturn> };
|
|
21
|
+
|
|
22
|
+
context.addInitializer(function() {
|
|
23
|
+
(this as HasCache)[cacheProperty] = new Map<TKey, TReturn>();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const wrappedName = `__wrapped__${context.name.toString()}`;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
[wrappedName](this: TThis, key: TKey): TReturn {
|
|
30
|
+
const cache = (this as HasCache)[cacheProperty];
|
|
31
|
+
if (cache.has(key)) {
|
|
32
|
+
return cache.get(key)!;
|
|
33
|
+
} else {
|
|
34
|
+
const result = originalMethod.call(this, key);
|
|
35
|
+
cache.set(key, result);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}[wrappedName];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type OptionalKeyOf<T extends object> = {
|
|
43
|
+
[K in keyof T]: T extends Record<K, T[K]> ? never : K
|
|
44
|
+
}[keyof T];
|
|
45
|
+
|
|
46
|
+
export class ObjectSubstateProvider<TBase extends object> {
|
|
47
|
+
private readonly setState: StateUpdater<TBase>;
|
|
48
|
+
|
|
49
|
+
constructor(setState: StateUpdater<TBase>) {
|
|
50
|
+
this.setState = setState;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@cached
|
|
54
|
+
set<TKey extends keyof TBase>(key: TKey): StateUpdater<TBase[TKey]> {
|
|
55
|
+
return value => this.setState(prevState => ({
|
|
56
|
+
...prevState,
|
|
57
|
+
[key]: applyStateUpdater(value, prevState[key]),
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@cached
|
|
62
|
+
delete(key: OptionalKeyOf<TBase>) {
|
|
63
|
+
return () => this.setState(({
|
|
64
|
+
[key]: _,
|
|
65
|
+
...rest
|
|
66
|
+
}) => rest as TBase);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ArraySubstateProvider<TItem> {
|
|
71
|
+
private readonly setState: StateUpdater<TItem[], readonly TItem[]>;
|
|
72
|
+
|
|
73
|
+
constructor(setState: StateUpdater<TItem[], readonly TItem[]>) {
|
|
74
|
+
this.setState = setState;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@cached
|
|
78
|
+
set(index: number): StateUpdater<TItem> {
|
|
79
|
+
return value => this.setState(prevState => [
|
|
80
|
+
...prevState.slice(0, index),
|
|
81
|
+
applyStateUpdater(value, prevState[index]),
|
|
82
|
+
...prevState.slice(index + 1),
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@cached
|
|
87
|
+
delete (index: number) {
|
|
88
|
+
return () => this.setState(prevState => [
|
|
89
|
+
...prevState.slice(0, index),
|
|
90
|
+
...prevState.slice(index + 1),
|
|
91
|
+
])
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
@cached
|
|
95
|
+
moveTo(index: number) {
|
|
96
|
+
return (newIndex: number) => this.setState(prevState => {
|
|
97
|
+
const rest = [
|
|
98
|
+
...prevState.slice(0, index),
|
|
99
|
+
...prevState.slice(index + 1),
|
|
100
|
+
];
|
|
101
|
+
return [
|
|
102
|
+
...rest.slice(0, newIndex),
|
|
103
|
+
prevState[index],
|
|
104
|
+
...rest.slice(newIndex),
|
|
105
|
+
];
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
@cached
|
|
110
|
+
moveBy(index: number) {
|
|
111
|
+
const moveTo = this.moveTo(index);
|
|
112
|
+
return (diff: number) => moveTo(index + diff);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
prepend: StateUpdater<TItem, readonly TItem[]> =
|
|
116
|
+
value => this.setState(prevState => [
|
|
117
|
+
applyStateUpdater(value, prevState),
|
|
118
|
+
...prevState,
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
append: StateUpdater<TItem, readonly TItem[]> =
|
|
122
|
+
value => this.setState(prevState => [
|
|
123
|
+
...prevState,
|
|
124
|
+
applyStateUpdater(value, prevState),
|
|
125
|
+
]);
|
|
126
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["es2022", "DOM"],
|
|
4
|
+
"target": "es2022",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationDir": "./dist/esm",
|
|
7
|
+
|
|
8
|
+
"module": "esnext",
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
|
|
13
|
+
"paths": {
|
|
14
|
+
"@": ["src"],
|
|
15
|
+
"@/*": ["src/*"],
|
|
16
|
+
},
|
|
17
|
+
"forceConsistentCasingInFileNames": true,
|
|
18
|
+
|
|
19
|
+
"strict": true
|
|
20
|
+
},
|
|
21
|
+
}
|