@faasjs/react 8.0.0-beta.5 → 8.0.0-beta.7
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/README.md +23 -17
- package/dist/index.cjs +939 -674
- package/dist/index.d.ts +359 -260
- package/dist/index.mjs +937 -672
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,741 +1,1006 @@
|
|
|
1
|
-
import { FaasBrowserClient } from
|
|
2
|
-
import {
|
|
3
|
-
import { jsx, jsxs } from
|
|
1
|
+
import { FaasBrowserClient } from "@faasjs/browser";
|
|
2
|
+
import { Component, cloneElement, createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
//#region src/equal.ts
|
|
6
|
+
const AsyncFunction = (async () => {}).constructor;
|
|
7
|
+
/**
|
|
8
|
+
* Compares two values for deep equality.
|
|
9
|
+
*
|
|
10
|
+
* This function checks if two values are deeply equal by comparing their types and contents.
|
|
11
|
+
* It handles various data types including primitives, arrays, dates, regular expressions, functions,
|
|
12
|
+
* maps, sets, and promises.
|
|
13
|
+
*
|
|
14
|
+
* @param a - The first value to compare.
|
|
15
|
+
* @param b - The second value to compare.
|
|
16
|
+
* @returns `true` if the values are deeply equal, `false` otherwise.
|
|
17
|
+
*/
|
|
8
18
|
function equal(a, b) {
|
|
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
|
-
return a.toString() === b.toString();
|
|
36
|
-
case Map:
|
|
37
|
-
case Set:
|
|
38
|
-
return equal(Array.from(a), Array.from(b));
|
|
39
|
-
case Promise:
|
|
40
|
-
return a === b;
|
|
41
|
-
case Object: {
|
|
42
|
-
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]))
|
|
43
|
-
if (!equal(a[key], b[key])) return false;
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
default:
|
|
47
|
-
throw Error(`Unsupported type: ${ctor}`);
|
|
48
|
-
}
|
|
19
|
+
if (a === b) return true;
|
|
20
|
+
if ((a === null || a === void 0) && (b === null || b === void 0)) return true;
|
|
21
|
+
if (typeof a !== typeof b) return false;
|
|
22
|
+
if (a === null || a === void 0 || b === null || b === void 0) return false;
|
|
23
|
+
const ctor = a.constructor;
|
|
24
|
+
if (ctor !== b.constructor) return false;
|
|
25
|
+
switch (ctor) {
|
|
26
|
+
case String:
|
|
27
|
+
case Boolean: return a === b;
|
|
28
|
+
case Number: return Number.isNaN(a) && Number.isNaN(b) || a === b;
|
|
29
|
+
case Array:
|
|
30
|
+
if (a.length !== b.length) return false;
|
|
31
|
+
for (let i = 0; i < a.length; i++) if (!equal(a[i], b[i])) return false;
|
|
32
|
+
return true;
|
|
33
|
+
case Date: return a.getTime() === b.getTime();
|
|
34
|
+
case RegExp:
|
|
35
|
+
case Function:
|
|
36
|
+
case AsyncFunction: return a.toString() === b.toString();
|
|
37
|
+
case Map:
|
|
38
|
+
case Set: return equal(Array.from(a), Array.from(b));
|
|
39
|
+
case Promise: return a === b;
|
|
40
|
+
case Object:
|
|
41
|
+
for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) if (!equal(a[key], b[key])) return false;
|
|
42
|
+
return true;
|
|
43
|
+
default: throw Error(`Unsupported type: ${ctor}`);
|
|
44
|
+
}
|
|
49
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Custom hook that memoizes a value using deep equality comparison.
|
|
48
|
+
*
|
|
49
|
+
* @param value - The value to be memoized.
|
|
50
|
+
* @returns The memoized value.
|
|
51
|
+
*/
|
|
50
52
|
function useEqualMemoize(value) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
const ref = useRef(value);
|
|
54
|
+
const signalRef = useRef(0);
|
|
55
|
+
if (!equal(value, ref.current)) {
|
|
56
|
+
ref.current = value;
|
|
57
|
+
signalRef.current += 1;
|
|
58
|
+
}
|
|
59
|
+
return useMemo(() => ref.current, [signalRef.current]);
|
|
58
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Custom hook that works like `useEffect` but uses deep comparison on dependencies.
|
|
63
|
+
*
|
|
64
|
+
* @param callback - The effect callback function to run.
|
|
65
|
+
* @param dependencies - The list of dependencies for the effect.
|
|
66
|
+
* @returns The result of the `useEffect` hook with memoized dependencies.
|
|
67
|
+
*/
|
|
59
68
|
function useEqualEffect(callback, dependencies) {
|
|
60
|
-
|
|
69
|
+
return useEffect(callback, useEqualMemoize(dependencies));
|
|
61
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Custom hook that works like `useMemo` but uses deep comparison on dependencies.
|
|
73
|
+
*
|
|
74
|
+
* @param callback - The callback function to run.
|
|
75
|
+
* @param dependencies - The list of dependencies.
|
|
76
|
+
* @returns The result of the `useMemo` hook with memoized dependencies.
|
|
77
|
+
*/
|
|
62
78
|
function useEqualMemo(callback, dependencies) {
|
|
63
|
-
|
|
79
|
+
return useMemo(callback, useEqualMemoize(dependencies));
|
|
64
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Custom hook that works like `useCallback` but uses deep comparison on dependencies.
|
|
83
|
+
*
|
|
84
|
+
* @param callback - The callback function to run.
|
|
85
|
+
* @param dependencies - The list of dependencies.
|
|
86
|
+
* @returns The result of the `useCallback` hook with memoized dependencies.
|
|
87
|
+
*/
|
|
65
88
|
function useEqualCallback(callback, dependencies) {
|
|
66
|
-
|
|
67
|
-
(...args) => callback(...args),
|
|
68
|
-
useEqualMemoize(dependencies)
|
|
69
|
-
);
|
|
89
|
+
return useCallback((...args) => callback(...args), useEqualMemoize(dependencies));
|
|
70
90
|
}
|
|
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
|
-
return child;
|
|
105
|
-
}
|
|
106
|
-
);
|
|
107
|
-
Object.assign(FaasDataWrapper, {
|
|
108
|
-
displayName: "FaasDataWrapper"
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/FaasDataWrapper.tsx
|
|
94
|
+
const fixedForwardRef = forwardRef;
|
|
95
|
+
const FaasDataWrapper = fixedForwardRef((props, ref) => {
|
|
96
|
+
const requestOptions = {
|
|
97
|
+
...props.data !== void 0 ? { data: props.data } : {},
|
|
98
|
+
...props.setData ? { setData: props.setData } : {}
|
|
99
|
+
};
|
|
100
|
+
const request = getClient(props.baseUrl).useFaas(props.action, props.params ?? {}, requestOptions);
|
|
101
|
+
const [loaded, setLoaded] = useState(false);
|
|
102
|
+
useImperativeHandle(ref, () => request, [request]);
|
|
103
|
+
useEqualEffect(() => {
|
|
104
|
+
if (!request.loading) setLoaded((prev) => prev === false ? true : prev);
|
|
105
|
+
}, [request.loading]);
|
|
106
|
+
useEqualEffect(() => {
|
|
107
|
+
if (props.onDataChange) props.onDataChange(request);
|
|
108
|
+
}, [request.data]);
|
|
109
|
+
return useEqualMemo(() => {
|
|
110
|
+
if (loaded) {
|
|
111
|
+
if (props.children) return cloneElement(props.children, request);
|
|
112
|
+
if (props.render) return props.render(request);
|
|
113
|
+
}
|
|
114
|
+
return props.fallback || null;
|
|
115
|
+
}, [
|
|
116
|
+
loaded,
|
|
117
|
+
request.action,
|
|
118
|
+
request.params,
|
|
119
|
+
request.data,
|
|
120
|
+
request.error,
|
|
121
|
+
request.loading
|
|
122
|
+
]);
|
|
109
123
|
});
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
Object.assign(FaasDataWrapper, { displayName: "FaasDataWrapper" });
|
|
125
|
+
/**
|
|
126
|
+
* HOC to wrap a component with FaasDataWrapper
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```tsx
|
|
130
|
+
* const MyComponent = withFaasData(({ data }) => <div>{data.name}</div>, { action: 'test', params: { a: 1 } })
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
function withFaasData(Component, faasProps) {
|
|
134
|
+
return (props) => /* @__PURE__ */ jsx(FaasDataWrapper, {
|
|
135
|
+
...faasProps,
|
|
136
|
+
children: /* @__PURE__ */ jsx(Component, { ...props })
|
|
137
|
+
});
|
|
112
138
|
}
|
|
113
139
|
|
|
114
|
-
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/faas.ts
|
|
142
|
+
/**
|
|
143
|
+
* Request faas server
|
|
144
|
+
*
|
|
145
|
+
* @param action {string} action name
|
|
146
|
+
* @param params {object} action params
|
|
147
|
+
* @returns {Promise<Response<any>>}
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* faas<{ title: string }>('post/get', { id: 1 }).then(res => {
|
|
152
|
+
* console.log(res.data.title)
|
|
153
|
+
* })
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
115
156
|
async function faas(action, params, options) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
157
|
+
const client = getClient(options?.baseUrl);
|
|
158
|
+
const onError = client.onError;
|
|
159
|
+
if (onError) return client.browserClient.action(action, params, options).catch(async (res) => {
|
|
160
|
+
await onError(action, params)(res);
|
|
161
|
+
return Promise.reject(res);
|
|
162
|
+
});
|
|
163
|
+
return client.browserClient.action(action, params, options);
|
|
123
164
|
}
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/useFaas.tsx
|
|
168
|
+
/**
|
|
169
|
+
* Request faas server with React hook
|
|
170
|
+
*
|
|
171
|
+
* @param action {string} action name
|
|
172
|
+
* @param defaultParams {object} initial action params
|
|
173
|
+
* @returns {FaasDataInjection<any>}
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```tsx
|
|
177
|
+
* function Post ({ id }) {
|
|
178
|
+
* const { data } = useFaas<{ title: string }>('post/get', { id })
|
|
179
|
+
* return <h1>{data.title}</h1>
|
|
180
|
+
* }
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
124
183
|
function useFaas(action, defaultParams, options = {}) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
setError
|
|
234
|
-
};
|
|
184
|
+
const [loading, setLoading] = useState(true);
|
|
185
|
+
const [data, setData] = useState();
|
|
186
|
+
const [error, setError] = useState();
|
|
187
|
+
const [params, setParams] = useState(defaultParams);
|
|
188
|
+
const [reloadTimes, setReloadTimes] = useState(0);
|
|
189
|
+
const [fails, setFails] = useState(0);
|
|
190
|
+
const [skip, setSkip] = useState(typeof options.skip === "function" ? options.skip(defaultParams) : options.skip);
|
|
191
|
+
const promiseRef = useRef(null);
|
|
192
|
+
const controllerRef = useRef(null);
|
|
193
|
+
const localSetData = setData;
|
|
194
|
+
const pendingReloadsRef = useRef(/* @__PURE__ */ new Map());
|
|
195
|
+
const reloadCounterRef = useRef(0);
|
|
196
|
+
useEqualEffect(() => {
|
|
197
|
+
setSkip(typeof options.skip === "function" ? options.skip(params) : options.skip);
|
|
198
|
+
}, [typeof options.skip === "function" ? params : options.skip]);
|
|
199
|
+
useEqualEffect(() => {
|
|
200
|
+
if (!equal(defaultParams, params)) setParams(defaultParams);
|
|
201
|
+
}, [defaultParams]);
|
|
202
|
+
useEqualEffect(() => {
|
|
203
|
+
if (!action || skip) {
|
|
204
|
+
setLoading(false);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
setLoading(true);
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
controllerRef.current = controller;
|
|
210
|
+
const client = getClient(options.baseUrl);
|
|
211
|
+
const requestParams = options.params ?? params;
|
|
212
|
+
function send() {
|
|
213
|
+
const request = client.faas(action, requestParams, { signal: controller.signal });
|
|
214
|
+
promiseRef.current = request;
|
|
215
|
+
request.then((r) => {
|
|
216
|
+
const nextData = r.data;
|
|
217
|
+
setFails(0);
|
|
218
|
+
setError(null);
|
|
219
|
+
options.setData ? options.setData(nextData) : localSetData(nextData);
|
|
220
|
+
setLoading(false);
|
|
221
|
+
for (const { resolve } of pendingReloadsRef.current.values()) resolve(nextData);
|
|
222
|
+
pendingReloadsRef.current.clear();
|
|
223
|
+
}).catch(async (e) => {
|
|
224
|
+
if (typeof e?.message === "string" && e.message.toLowerCase().indexOf("aborted") >= 0) return;
|
|
225
|
+
if (!fails && typeof e?.message === "string" && e.message.indexOf("Failed to fetch") >= 0) {
|
|
226
|
+
console.warn(`FaasReactClient: ${e.message} retry...`);
|
|
227
|
+
setFails(1);
|
|
228
|
+
return send();
|
|
229
|
+
}
|
|
230
|
+
let error = e;
|
|
231
|
+
if (client.onError) try {
|
|
232
|
+
await client.onError(action, requestParams)(e);
|
|
233
|
+
} catch (newError) {
|
|
234
|
+
error = newError;
|
|
235
|
+
}
|
|
236
|
+
setError(error);
|
|
237
|
+
setLoading(false);
|
|
238
|
+
for (const { reject } of pendingReloadsRef.current.values()) reject(error);
|
|
239
|
+
pendingReloadsRef.current.clear();
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (options.debounce) {
|
|
243
|
+
const timeout = setTimeout(send, options.debounce);
|
|
244
|
+
return () => {
|
|
245
|
+
clearTimeout(timeout);
|
|
246
|
+
controllerRef.current?.abort();
|
|
247
|
+
setLoading(false);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
send();
|
|
251
|
+
return () => {
|
|
252
|
+
controllerRef.current?.abort();
|
|
253
|
+
setLoading(false);
|
|
254
|
+
};
|
|
255
|
+
}, [
|
|
256
|
+
action,
|
|
257
|
+
options.params || params,
|
|
258
|
+
reloadTimes,
|
|
259
|
+
skip
|
|
260
|
+
]);
|
|
261
|
+
const reload = useEqualCallback((params) => {
|
|
262
|
+
if (skip) setSkip(false);
|
|
263
|
+
if (params) setParams(params);
|
|
264
|
+
const reloadCounter = ++reloadCounterRef.current;
|
|
265
|
+
setReloadTimes((prev) => prev + 1);
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
pendingReloadsRef.current.set(reloadCounter, {
|
|
268
|
+
resolve,
|
|
269
|
+
reject
|
|
270
|
+
});
|
|
271
|
+
setReloadTimes((prev) => prev + 1);
|
|
272
|
+
});
|
|
273
|
+
}, [params, skip]);
|
|
274
|
+
const currentData = options.data ?? data;
|
|
275
|
+
const currentPromise = promiseRef.current ?? Promise.resolve({});
|
|
276
|
+
return {
|
|
277
|
+
action,
|
|
278
|
+
params,
|
|
279
|
+
loading,
|
|
280
|
+
data: currentData,
|
|
281
|
+
reloadTimes,
|
|
282
|
+
error,
|
|
283
|
+
promise: currentPromise,
|
|
284
|
+
reload,
|
|
285
|
+
setData: options.setData ?? localSetData,
|
|
286
|
+
setLoading,
|
|
287
|
+
setPromise: (newPromise) => {
|
|
288
|
+
promiseRef.current = typeof newPromise === "function" ? newPromise(currentPromise) : newPromise;
|
|
289
|
+
},
|
|
290
|
+
setError
|
|
291
|
+
};
|
|
235
292
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
293
|
+
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/client.tsx
|
|
296
|
+
const clients = {};
|
|
297
|
+
/**
|
|
298
|
+
* Before use faas, you should initialize a FaasReactClient.
|
|
299
|
+
*
|
|
300
|
+
* @returns FaasReactClient instance.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* const client = FaasReactClient({
|
|
305
|
+
* baseUrl: 'localhost:8080/api/'
|
|
306
|
+
* })
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
function FaasReactClient({ baseUrl, options: clientOptions, onError } = { baseUrl: "/" }) {
|
|
310
|
+
const resolvedBaseUrl = baseUrl ?? "/";
|
|
311
|
+
const client = new FaasBrowserClient(resolvedBaseUrl, clientOptions);
|
|
312
|
+
function withBaseUrl(options) {
|
|
313
|
+
if (options?.baseUrl) return options;
|
|
314
|
+
return {
|
|
315
|
+
...options ?? {},
|
|
316
|
+
baseUrl: resolvedBaseUrl
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const reactClient = {
|
|
320
|
+
id: client.id,
|
|
321
|
+
faas: async (action, params, requestOptions) => faas(action, params, withBaseUrl(requestOptions)),
|
|
322
|
+
useFaas: (action, defaultParams, requestOptions) => useFaas(action, defaultParams, withBaseUrl(requestOptions)),
|
|
323
|
+
FaasDataWrapper: (props) => /* @__PURE__ */ jsx(FaasDataWrapper, {
|
|
324
|
+
...props,
|
|
325
|
+
baseUrl: resolvedBaseUrl
|
|
326
|
+
}),
|
|
327
|
+
...onError ? { onError } : {},
|
|
328
|
+
browserClient: client
|
|
329
|
+
};
|
|
330
|
+
clients[resolvedBaseUrl] = reactClient;
|
|
331
|
+
return reactClient;
|
|
251
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Get FaasReactClient instance
|
|
335
|
+
*
|
|
336
|
+
* @param host {string} empty string for default host
|
|
337
|
+
* @returns {FaasReactClientInstance}
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```ts
|
|
341
|
+
* getClient()
|
|
342
|
+
* // or
|
|
343
|
+
* getClient('another-host')
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
252
346
|
function getClient(host) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
347
|
+
const client = clients[host || Object.keys(clients)[0]];
|
|
348
|
+
if (!client) {
|
|
349
|
+
console.warn("FaasReactClient is not initialized manually, use default.");
|
|
350
|
+
return FaasReactClient();
|
|
351
|
+
}
|
|
352
|
+
return client;
|
|
259
353
|
}
|
|
354
|
+
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/constant.ts
|
|
357
|
+
/**
|
|
358
|
+
* Returns a constant value that is created by the given function.
|
|
359
|
+
*/
|
|
260
360
|
function useConstant(fn) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
return ref.current.v;
|
|
361
|
+
const ref = useRef(null);
|
|
362
|
+
if (!ref.current) ref.current = { v: fn() };
|
|
363
|
+
return ref.current.v;
|
|
266
364
|
}
|
|
365
|
+
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/ErrorBoundary.tsx
|
|
267
368
|
var ErrorBoundary = class extends Component {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
] });
|
|
299
|
-
}
|
|
300
|
-
return this.props.children;
|
|
301
|
-
}
|
|
369
|
+
static displayName = "ErrorBoundary";
|
|
370
|
+
constructor(props) {
|
|
371
|
+
super(props);
|
|
372
|
+
this.state = {
|
|
373
|
+
error: null,
|
|
374
|
+
info: { componentStack: "" }
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
componentDidCatch(error, info) {
|
|
378
|
+
this.setState({
|
|
379
|
+
error,
|
|
380
|
+
info
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
render() {
|
|
384
|
+
const { error, info } = this.state;
|
|
385
|
+
const errorMessage = String(error ?? "");
|
|
386
|
+
const errorDescription = info.componentStack || void 0;
|
|
387
|
+
if (error) {
|
|
388
|
+
if (this.props.onError) this.props.onError(error, info);
|
|
389
|
+
if (this.props.errorChildren) return cloneElement(this.props.errorChildren, {
|
|
390
|
+
error,
|
|
391
|
+
info,
|
|
392
|
+
errorMessage,
|
|
393
|
+
...errorDescription ? { errorDescription } : {}
|
|
394
|
+
});
|
|
395
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", { children: errorMessage }), /* @__PURE__ */ jsx("pre", { children: errorDescription })] });
|
|
396
|
+
}
|
|
397
|
+
return this.props.children ?? null;
|
|
398
|
+
}
|
|
302
399
|
};
|
|
400
|
+
|
|
401
|
+
//#endregion
|
|
402
|
+
//#region src/useStateRef.ts
|
|
403
|
+
/**
|
|
404
|
+
* Custom hook that returns a stateful value and a ref to that value.
|
|
405
|
+
*
|
|
406
|
+
* @template T - The type of the value.
|
|
407
|
+
* @param {T} initialValue - The initial value of the state.
|
|
408
|
+
* @returns {[T, (value: T) => void, RefObject<T>]} - The stateful value, a function to set the value, and a ref to the value.
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* ```tsx
|
|
412
|
+
* import { useStateRef } from '@faasjs/react'
|
|
413
|
+
*
|
|
414
|
+
* function MyComponent() {
|
|
415
|
+
* const [value, setValue, ref] = useStateRef(0)
|
|
416
|
+
*
|
|
417
|
+
* return (
|
|
418
|
+
* <div>
|
|
419
|
+
* <p>Value: {value}</p>
|
|
420
|
+
* <button onClick={() => setValue(value + 1)}>Increment</button>
|
|
421
|
+
* <button onClick={() => console.log(ref.current)}>Submit</button>
|
|
422
|
+
* </div>
|
|
423
|
+
* )
|
|
424
|
+
*/
|
|
303
425
|
function useStateRef(initialValue) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
426
|
+
const [state, setState] = useState(initialValue ?? null);
|
|
427
|
+
const ref = useRef(state);
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
ref.current = state;
|
|
430
|
+
}, [state]);
|
|
431
|
+
return [
|
|
432
|
+
state,
|
|
433
|
+
setState,
|
|
434
|
+
ref
|
|
435
|
+
];
|
|
310
436
|
}
|
|
437
|
+
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/splittingState.tsx
|
|
440
|
+
/**
|
|
441
|
+
* A hook that initializes and splits state variables and their corresponding setters.
|
|
442
|
+
*
|
|
443
|
+
* @template T - A generic type that extends a record with string keys and any values.
|
|
444
|
+
* @param {T} initialStates - An object containing the initial states.
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* ```tsx
|
|
448
|
+
* function Counter() {
|
|
449
|
+
* const { count, setCount, name, setName } = useSplittingState({ count: 0, name: 'John' });
|
|
450
|
+
*
|
|
451
|
+
* return <>{name}: {count}</>
|
|
452
|
+
* }
|
|
453
|
+
* ```
|
|
454
|
+
*/
|
|
311
455
|
function useSplittingState(initialStates) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
456
|
+
const states = {};
|
|
457
|
+
for (const key of Object.keys(initialStates)) {
|
|
458
|
+
const state = useState(initialStates[key]);
|
|
459
|
+
Object.assign(states, {
|
|
460
|
+
[key]: state[0],
|
|
461
|
+
[`set${String(key).charAt(0).toUpperCase()}${String(key).slice(1)}`]: state[1]
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return states;
|
|
321
465
|
}
|
|
466
|
+
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/splittingContext.tsx
|
|
469
|
+
/**
|
|
470
|
+
* Creates a splitting context with the given default value.
|
|
471
|
+
*
|
|
472
|
+
* @param defaultValue The default value of the splitting context.
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* ```tsx
|
|
476
|
+
* const { Provider, use } = createSplittingContext<{
|
|
477
|
+
* value: number
|
|
478
|
+
* setValue: React.Dispatch<React.SetStateAction<number>>
|
|
479
|
+
* }>({
|
|
480
|
+
* value: 0,
|
|
481
|
+
* setValue: null,
|
|
482
|
+
* })
|
|
483
|
+
*
|
|
484
|
+
* function ReaderComponent() {
|
|
485
|
+
* const { value } = use()
|
|
486
|
+
*
|
|
487
|
+
* return <div>{value}</div>
|
|
488
|
+
* }
|
|
489
|
+
*
|
|
490
|
+
* function WriterComponent() {
|
|
491
|
+
* const { setValue } = use()
|
|
492
|
+
*
|
|
493
|
+
* return (
|
|
494
|
+
* <button type='button' onClick={() => setValue((p: number) => p + 1)}>
|
|
495
|
+
* Change
|
|
496
|
+
* </button>
|
|
497
|
+
* )
|
|
498
|
+
* }
|
|
499
|
+
*
|
|
500
|
+
* function App() {
|
|
501
|
+
* const [value, setValue] = useState(0)
|
|
502
|
+
*
|
|
503
|
+
* return (
|
|
504
|
+
* <Provider value={{ value, setValue }}>
|
|
505
|
+
* <ReaderComponent />
|
|
506
|
+
* <WriterComponent />
|
|
507
|
+
* </Provider>
|
|
508
|
+
* )
|
|
509
|
+
* }
|
|
510
|
+
* ```
|
|
511
|
+
*/
|
|
322
512
|
function createSplittingContext(defaultValue) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
return Object.freeze(obj);
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
return {
|
|
364
|
-
Provider,
|
|
365
|
-
use
|
|
366
|
-
};
|
|
513
|
+
const keys = Array.isArray(defaultValue) ? defaultValue : Object.keys(defaultValue);
|
|
514
|
+
const defaultValues = Array.isArray(defaultValue) ? keys.reduce((prev, cur) => {
|
|
515
|
+
prev[cur] = null;
|
|
516
|
+
return prev;
|
|
517
|
+
}, {}) : defaultValue;
|
|
518
|
+
const contexts = {};
|
|
519
|
+
for (const key of keys) contexts[key] = createContext(defaultValues[key]);
|
|
520
|
+
function Provider(props) {
|
|
521
|
+
const states = props.initializeStates ? useSplittingState(props.initializeStates) : {};
|
|
522
|
+
let children = props.memo ? useEqualMemo(() => props.children, props.memo === true ? [] : props.memo) : props.children;
|
|
523
|
+
for (const key of keys) {
|
|
524
|
+
const Context = contexts[key];
|
|
525
|
+
const value = props.value?.[key] ?? states[key] ?? defaultValues[key];
|
|
526
|
+
children = /* @__PURE__ */ jsx(Context.Provider, {
|
|
527
|
+
value,
|
|
528
|
+
children
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
return children;
|
|
532
|
+
}
|
|
533
|
+
Provider.displayName = "SplittingContextProvider";
|
|
534
|
+
function use() {
|
|
535
|
+
return useConstant(() => {
|
|
536
|
+
const obj = Object.create(null);
|
|
537
|
+
for (const key of Object.keys(contexts)) Object.defineProperty(obj, key, { get: () => {
|
|
538
|
+
if (!contexts[key]) throw new Error(`Context for key "${key}" is undefined`);
|
|
539
|
+
return useContext(contexts[key]);
|
|
540
|
+
} });
|
|
541
|
+
return Object.freeze(obj);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
Provider,
|
|
546
|
+
use
|
|
547
|
+
};
|
|
367
548
|
}
|
|
368
549
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/Form/context.tsx
|
|
552
|
+
const FormContext = createSplittingContext([
|
|
553
|
+
"items",
|
|
554
|
+
"onSubmit",
|
|
555
|
+
"Elements",
|
|
556
|
+
"lang",
|
|
557
|
+
"rules",
|
|
558
|
+
"submitting",
|
|
559
|
+
"setSubmitting",
|
|
560
|
+
"values",
|
|
561
|
+
"setValues",
|
|
562
|
+
"errors",
|
|
563
|
+
"setErrors",
|
|
564
|
+
"valuesRef"
|
|
383
565
|
]);
|
|
384
|
-
|
|
385
|
-
|
|
566
|
+
const FormContextProvider = FormContext.Provider;
|
|
567
|
+
const useFormContext = FormContext.use;
|
|
568
|
+
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/Form/Input.tsx
|
|
386
571
|
function processValue(input, rules) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
default:
|
|
393
|
-
return input;
|
|
394
|
-
}
|
|
572
|
+
switch (rules?.type) {
|
|
573
|
+
case "number": return Number(input);
|
|
574
|
+
case "string": return String(input);
|
|
575
|
+
default: return input;
|
|
576
|
+
}
|
|
395
577
|
}
|
|
396
|
-
function FormInput({
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
return /* @__PURE__ */ jsx(
|
|
418
|
-
Elements.Input,
|
|
419
|
-
{
|
|
420
|
-
name,
|
|
421
|
-
value,
|
|
422
|
-
onChange: (v) => setValues((prev) => ({
|
|
423
|
-
...prev,
|
|
424
|
-
[name]: processValue(v, rules)
|
|
425
|
-
})),
|
|
426
|
-
...rest.props
|
|
427
|
-
}
|
|
428
|
-
);
|
|
578
|
+
function FormInput({ name, rules, ...rest }) {
|
|
579
|
+
const { Elements, values, setValues } = useFormContext();
|
|
580
|
+
const value = values?.[name];
|
|
581
|
+
if (rest.Input) return /* @__PURE__ */ jsx(rest.Input, {
|
|
582
|
+
name,
|
|
583
|
+
value,
|
|
584
|
+
onChange: (v) => setValues((prev) => ({
|
|
585
|
+
...prev,
|
|
586
|
+
[name]: processValue(v, rules)
|
|
587
|
+
})),
|
|
588
|
+
...rest.props
|
|
589
|
+
});
|
|
590
|
+
return /* @__PURE__ */ jsx(Elements.Input, {
|
|
591
|
+
name,
|
|
592
|
+
value,
|
|
593
|
+
onChange: (v) => setValues((prev) => ({
|
|
594
|
+
...prev,
|
|
595
|
+
[name]: processValue(v, rules)
|
|
596
|
+
})),
|
|
597
|
+
...rest.props
|
|
598
|
+
});
|
|
429
599
|
}
|
|
430
600
|
FormInput.displayName = "FormInput";
|
|
601
|
+
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/Form/Item.tsx
|
|
431
604
|
function FormItem(props) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
605
|
+
const { Elements, errors } = useFormContext();
|
|
606
|
+
return /* @__PURE__ */ jsx(props.label?.Label ?? Elements.Label, {
|
|
607
|
+
name: props.name,
|
|
608
|
+
...props.label,
|
|
609
|
+
error: errors[props.name],
|
|
610
|
+
children: /* @__PURE__ */ jsx(FormInput, {
|
|
611
|
+
name: props.name,
|
|
612
|
+
...props.input,
|
|
613
|
+
...props.rules ? { rules: props.rules } : {}
|
|
614
|
+
})
|
|
615
|
+
});
|
|
435
616
|
}
|
|
436
617
|
FormItem.displayName = "FormItem";
|
|
618
|
+
|
|
619
|
+
//#endregion
|
|
620
|
+
//#region src/Form/Body.tsx
|
|
437
621
|
function FormBody() {
|
|
438
|
-
|
|
439
|
-
|
|
622
|
+
const { items } = useFormContext();
|
|
623
|
+
return items.map((item) => /* @__PURE__ */ jsx(FormItem, { ...item }, item.name));
|
|
440
624
|
}
|
|
441
625
|
FormBody.displayName = "FormBody";
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
));
|
|
626
|
+
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/Form/elements/Button.tsx
|
|
629
|
+
const FormButtonElement = forwardRef(({ children, submit, submitting, ...props }, ref) => /* @__PURE__ */ jsx("button", {
|
|
630
|
+
type: "button",
|
|
631
|
+
disabled: submitting,
|
|
632
|
+
onClick: submit,
|
|
633
|
+
...props,
|
|
634
|
+
ref,
|
|
635
|
+
children
|
|
636
|
+
}));
|
|
453
637
|
FormButtonElement.displayName = "FormButtonElement";
|
|
454
|
-
|
|
638
|
+
|
|
639
|
+
//#endregion
|
|
640
|
+
//#region src/Form/elements/Input.tsx
|
|
641
|
+
const FormInputElement = forwardRef(({ onChange, ...props }, ref) => /* @__PURE__ */ jsx("input", {
|
|
642
|
+
...props,
|
|
643
|
+
onChange: (e) => onChange(e.target.value),
|
|
644
|
+
ref
|
|
645
|
+
}));
|
|
455
646
|
FormInputElement.displayName = "FormInputElement";
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
647
|
+
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/Form/elements/Label.tsx
|
|
650
|
+
const FormLabelElement = ({ name, title, description, error, children }) => {
|
|
651
|
+
return /* @__PURE__ */ jsxs("label", { children: [
|
|
652
|
+
title ?? name,
|
|
653
|
+
children,
|
|
654
|
+
description,
|
|
655
|
+
error && /* @__PURE__ */ jsx("div", {
|
|
656
|
+
style: { color: "red" },
|
|
657
|
+
children: error.message
|
|
658
|
+
})
|
|
659
|
+
] });
|
|
469
660
|
};
|
|
470
661
|
FormLabelElement.displayName = "FormLabelElement";
|
|
471
662
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/Form/elements/index.ts
|
|
665
|
+
const FormDefaultElements = {
|
|
666
|
+
Label: FormLabelElement,
|
|
667
|
+
Input: FormInputElement,
|
|
668
|
+
Button: FormButtonElement
|
|
477
669
|
};
|
|
478
670
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
671
|
+
//#endregion
|
|
672
|
+
//#region src/Form/rules.ts
|
|
673
|
+
/**
|
|
674
|
+
* Default validation rules for a form.
|
|
675
|
+
*/
|
|
676
|
+
const FormDefaultRules = {
|
|
677
|
+
required: async (value, _, lang) => {
|
|
678
|
+
if (value === null || value === void 0 || value === "" || Number.isNaN(value)) throw Error(lang?.required);
|
|
679
|
+
},
|
|
680
|
+
type: async (value, options, lang) => {
|
|
681
|
+
switch (options) {
|
|
682
|
+
case "string":
|
|
683
|
+
if (typeof value !== "string") throw Error(lang?.string);
|
|
684
|
+
break;
|
|
685
|
+
case "number":
|
|
686
|
+
if (Number.isNaN(Number(value))) throw Error(lang?.number);
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
custom: async (value, options) => {
|
|
691
|
+
return options(value);
|
|
692
|
+
}
|
|
499
693
|
};
|
|
500
694
|
async function validValues(rules, items, values, lang) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
return errors;
|
|
695
|
+
const errors = {};
|
|
696
|
+
for (const item of items) {
|
|
697
|
+
const value = values[item.name];
|
|
698
|
+
const rulesOptions = item.rules;
|
|
699
|
+
if (rulesOptions) for (const [name, options] of Object.entries(rulesOptions)) try {
|
|
700
|
+
await rules[name](value, options, lang);
|
|
701
|
+
} catch (error) {
|
|
702
|
+
errors[item.name] = error;
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return errors;
|
|
517
707
|
}
|
|
708
|
+
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/Form/Footer.tsx
|
|
518
711
|
function FormFooter() {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
712
|
+
const { submitting, setSubmitting, onSubmit, valuesRef, Elements, items, setErrors, lang, rules } = useFormContext();
|
|
713
|
+
const handleSubmit = useCallback(async () => {
|
|
714
|
+
setSubmitting(true);
|
|
715
|
+
setErrors({});
|
|
716
|
+
const errors = await validValues(rules, items, valuesRef.current, lang);
|
|
717
|
+
if (Object.keys(errors).length) {
|
|
718
|
+
setErrors(errors);
|
|
719
|
+
setSubmitting(false);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
onSubmit(valuesRef.current).finally(() => setSubmitting(false));
|
|
723
|
+
}, [
|
|
724
|
+
setSubmitting,
|
|
725
|
+
setErrors,
|
|
726
|
+
rules,
|
|
727
|
+
items,
|
|
728
|
+
lang,
|
|
729
|
+
onSubmit
|
|
730
|
+
]);
|
|
731
|
+
return useMemo(() => /* @__PURE__ */ jsx(Elements.Button, {
|
|
732
|
+
submitting,
|
|
733
|
+
submit: handleSubmit,
|
|
734
|
+
children: lang.submit
|
|
735
|
+
}), [
|
|
736
|
+
submitting,
|
|
737
|
+
handleSubmit,
|
|
738
|
+
lang.submit,
|
|
739
|
+
Elements.Button
|
|
740
|
+
]);
|
|
546
741
|
}
|
|
547
742
|
FormFooter.displayName = "FormFooter";
|
|
548
743
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
744
|
+
//#endregion
|
|
745
|
+
//#region src/Form/lang.ts
|
|
746
|
+
const FormDefaultLang = {
|
|
747
|
+
submit: "Submit",
|
|
748
|
+
required: "This field is required",
|
|
749
|
+
string: "This field must be a string",
|
|
750
|
+
number: "This field must be a number"
|
|
555
751
|
};
|
|
752
|
+
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/Form/Container.tsx
|
|
556
755
|
function mergeValues(items, defaultValues = {}) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return values;
|
|
756
|
+
const values = {};
|
|
757
|
+
for (const item of items) values[item.name] = defaultValues[item.name] ?? "";
|
|
758
|
+
return values;
|
|
561
759
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
760
|
+
/**
|
|
761
|
+
* FormContainer component is a wrapper that provides context and state management for form elements.
|
|
762
|
+
* It initializes form states such as values, errors, submitting status, elements, language, and rules.
|
|
763
|
+
*
|
|
764
|
+
* @template Values - The type of form values, defaults to Record<string, any>.
|
|
765
|
+
* @template FormElements - The type of form elements, defaults to FormElementTypes.
|
|
766
|
+
* @template Rules - The type of form rules, defaults to FormDefaultRules.
|
|
767
|
+
*
|
|
768
|
+
* @param {FormProps<Values, FormElements, Rules>} props - The properties for the FormContainer component.
|
|
769
|
+
* @param {Values} props.defaultValues - The default values for the form fields.
|
|
770
|
+
* @param {FormElements} props.Elements - The form elements to be used in the form.
|
|
771
|
+
* @param {Rules} props.rules - The validation rules for the form fields.
|
|
772
|
+
* @param {FormLang} props.lang - The language settings for the form.
|
|
773
|
+
* @param {Partial<FormContextProps>} props - Additional properties for the form context.
|
|
774
|
+
*
|
|
775
|
+
* @returns {JSX.Element} The FormContainer component.
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* ```tsx
|
|
779
|
+
* import { Form } from '@faasjs/react'
|
|
780
|
+
*
|
|
781
|
+
* function MyForm() {
|
|
782
|
+
* return <Form
|
|
783
|
+
* items={[
|
|
784
|
+
* { name: 'name' },
|
|
785
|
+
* ]}
|
|
786
|
+
* />
|
|
787
|
+
* }
|
|
788
|
+
* ```
|
|
789
|
+
*/
|
|
790
|
+
function FormContainer({ defaultValues, Elements, rules, lang, items, ...props }) {
|
|
791
|
+
const [values, setValues, valuesRef] = useStateRef(mergeValues(items, defaultValues));
|
|
792
|
+
return /* @__PURE__ */ jsxs(FormContextProvider, {
|
|
793
|
+
initializeStates: {
|
|
794
|
+
errors: {},
|
|
795
|
+
submitting: false
|
|
796
|
+
},
|
|
797
|
+
value: {
|
|
798
|
+
Elements: Object.assign(FormDefaultElements, Elements),
|
|
799
|
+
lang: Object.assign(FormDefaultLang, lang),
|
|
800
|
+
rules: Object.assign(FormDefaultRules, rules),
|
|
801
|
+
items,
|
|
802
|
+
values,
|
|
803
|
+
setValues,
|
|
804
|
+
valuesRef,
|
|
805
|
+
...props
|
|
806
|
+
},
|
|
807
|
+
memo: true,
|
|
808
|
+
children: [/* @__PURE__ */ jsx(FormBody, {}), /* @__PURE__ */ jsx(FormFooter, {})]
|
|
809
|
+
});
|
|
597
810
|
}
|
|
598
811
|
FormContainer.displayName = "FormContainer";
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
812
|
+
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/OptionalWrapper.tsx
|
|
815
|
+
/**
|
|
816
|
+
* A wrapper component that conditionally wraps its children with a provided wrapper component.
|
|
817
|
+
*
|
|
818
|
+
* @example
|
|
819
|
+
* ```tsx
|
|
820
|
+
* import { OptionalWrapper } from '@faasjs/react'
|
|
821
|
+
*
|
|
822
|
+
* const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
823
|
+
* <div className='wrapper'>{children}</div>
|
|
824
|
+
* )
|
|
825
|
+
*
|
|
826
|
+
* const App = () => (
|
|
827
|
+
* <OptionalWrapper condition={true} Wrapper={Wrapper}>
|
|
828
|
+
* <span>Test</span>
|
|
829
|
+
* </OptionalWrapper>
|
|
830
|
+
* )
|
|
831
|
+
* ```
|
|
832
|
+
*/
|
|
833
|
+
function OptionalWrapper({ condition, Wrapper, wrapperProps, children }) {
|
|
834
|
+
if (condition) return /* @__PURE__ */ jsx(Wrapper, {
|
|
835
|
+
...wrapperProps,
|
|
836
|
+
children
|
|
837
|
+
});
|
|
838
|
+
return children;
|
|
607
839
|
}
|
|
608
840
|
OptionalWrapper.displayName = "OptionalWrapper";
|
|
841
|
+
|
|
842
|
+
//#endregion
|
|
843
|
+
//#region src/useFaasStream.tsx
|
|
844
|
+
/**
|
|
845
|
+
* Stream faas server response with React hook
|
|
846
|
+
*
|
|
847
|
+
* @param action {string} action name
|
|
848
|
+
* @param defaultParams {object} initial action params
|
|
849
|
+
* @returns {UseFaasStreamResult}
|
|
850
|
+
*
|
|
851
|
+
* @example
|
|
852
|
+
* ```tsx
|
|
853
|
+
* function Chat() {
|
|
854
|
+
* const [prompt, setPrompt] = useState('')
|
|
855
|
+
* const { data, loading, reload } = useFaasStream('chat', { prompt })
|
|
856
|
+
*
|
|
857
|
+
* return (
|
|
858
|
+
* <div>
|
|
859
|
+
* <textarea value={prompt} onChange={e => setPrompt(e.target.value)} />
|
|
860
|
+
* <button onClick={reload} disabled={loading}>Send</button>
|
|
861
|
+
* <div>{data}</div>
|
|
862
|
+
* </div>
|
|
863
|
+
* )
|
|
864
|
+
* }
|
|
865
|
+
* ```
|
|
866
|
+
*/
|
|
609
867
|
function useFaasStream(action, defaultParams, options = {}) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
setData: options.setData || setData,
|
|
729
|
-
setLoading,
|
|
730
|
-
setError
|
|
731
|
-
};
|
|
868
|
+
const [loading, setLoading] = useState(true);
|
|
869
|
+
const [data, setData] = useState(options.data || "");
|
|
870
|
+
const [error, setError] = useState();
|
|
871
|
+
const [params, setParams] = useState(defaultParams);
|
|
872
|
+
const [reloadTimes, setReloadTimes] = useState(0);
|
|
873
|
+
const [fails, setFails] = useState(0);
|
|
874
|
+
const [skip, setSkip] = useState(typeof options.skip === "function" ? options.skip(defaultParams) : options.skip);
|
|
875
|
+
const controllerRef = useRef(null);
|
|
876
|
+
const pendingReloadsRef = useRef(/* @__PURE__ */ new Map());
|
|
877
|
+
const reloadCounterRef = useRef(0);
|
|
878
|
+
useEqualEffect(() => {
|
|
879
|
+
setSkip(typeof options.skip === "function" ? options.skip(params) : options.skip);
|
|
880
|
+
}, [typeof options.skip === "function" ? params : options.skip]);
|
|
881
|
+
useEqualEffect(() => {
|
|
882
|
+
if (!equal(defaultParams, params)) setParams(defaultParams);
|
|
883
|
+
}, [defaultParams]);
|
|
884
|
+
useEqualEffect(() => {
|
|
885
|
+
if (!action || skip) {
|
|
886
|
+
setLoading(false);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
setLoading(true);
|
|
890
|
+
setData("");
|
|
891
|
+
const controller = new AbortController();
|
|
892
|
+
controllerRef.current = controller;
|
|
893
|
+
const client = getClient(options.baseUrl);
|
|
894
|
+
const requestParams = options.params ?? params;
|
|
895
|
+
function send() {
|
|
896
|
+
client.browserClient.action(action, requestParams, {
|
|
897
|
+
signal: controller.signal,
|
|
898
|
+
stream: true
|
|
899
|
+
}).then(async (response) => {
|
|
900
|
+
if (!response.body) {
|
|
901
|
+
setError(/* @__PURE__ */ new Error("Response body is null"));
|
|
902
|
+
setLoading(false);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const reader = response.body.getReader();
|
|
906
|
+
const decoder = new TextDecoder();
|
|
907
|
+
let accumulatedText = "";
|
|
908
|
+
try {
|
|
909
|
+
while (true) {
|
|
910
|
+
const { done, value } = await reader.read();
|
|
911
|
+
if (done) break;
|
|
912
|
+
accumulatedText += decoder.decode(value, { stream: true });
|
|
913
|
+
setData(accumulatedText);
|
|
914
|
+
}
|
|
915
|
+
setFails(0);
|
|
916
|
+
setError(null);
|
|
917
|
+
setLoading(false);
|
|
918
|
+
for (const { resolve } of pendingReloadsRef.current.values()) resolve(accumulatedText);
|
|
919
|
+
pendingReloadsRef.current.clear();
|
|
920
|
+
} catch (readError) {
|
|
921
|
+
reader.releaseLock();
|
|
922
|
+
throw readError;
|
|
923
|
+
}
|
|
924
|
+
}).catch(async (e) => {
|
|
925
|
+
if (typeof e?.message === "string" && e.message.toLowerCase().indexOf("aborted") >= 0) return;
|
|
926
|
+
if (!fails && typeof e?.message === "string" && e.message.indexOf("Failed to fetch") >= 0) {
|
|
927
|
+
console.warn(`FaasReactClient: ${e.message} retry...`);
|
|
928
|
+
setFails(1);
|
|
929
|
+
return send();
|
|
930
|
+
}
|
|
931
|
+
let error = e;
|
|
932
|
+
if (client.onError) try {
|
|
933
|
+
await client.onError(action, requestParams)(e);
|
|
934
|
+
} catch (newError) {
|
|
935
|
+
error = newError;
|
|
936
|
+
}
|
|
937
|
+
setError(error);
|
|
938
|
+
setLoading(false);
|
|
939
|
+
for (const { reject } of pendingReloadsRef.current.values()) reject(error);
|
|
940
|
+
pendingReloadsRef.current.clear();
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
if (options.debounce) {
|
|
944
|
+
const timeout = setTimeout(send, options.debounce);
|
|
945
|
+
return () => {
|
|
946
|
+
clearTimeout(timeout);
|
|
947
|
+
controllerRef.current?.abort();
|
|
948
|
+
setLoading(false);
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
send();
|
|
952
|
+
return () => {
|
|
953
|
+
controllerRef.current?.abort();
|
|
954
|
+
setLoading(false);
|
|
955
|
+
};
|
|
956
|
+
}, [
|
|
957
|
+
action,
|
|
958
|
+
options.params || params,
|
|
959
|
+
reloadTimes,
|
|
960
|
+
skip
|
|
961
|
+
]);
|
|
962
|
+
const reload = useEqualCallback((params) => {
|
|
963
|
+
if (skip) setSkip(false);
|
|
964
|
+
if (params) setParams(params);
|
|
965
|
+
const reloadCounter = ++reloadCounterRef.current;
|
|
966
|
+
return new Promise((resolve, reject) => {
|
|
967
|
+
pendingReloadsRef.current.set(reloadCounter, {
|
|
968
|
+
resolve,
|
|
969
|
+
reject
|
|
970
|
+
});
|
|
971
|
+
setReloadTimes((prev) => prev + 1);
|
|
972
|
+
});
|
|
973
|
+
}, [params, skip]);
|
|
974
|
+
return {
|
|
975
|
+
action,
|
|
976
|
+
params,
|
|
977
|
+
loading,
|
|
978
|
+
data: options.data || data,
|
|
979
|
+
reloadTimes,
|
|
980
|
+
error,
|
|
981
|
+
reload,
|
|
982
|
+
setData: options.setData || setData,
|
|
983
|
+
setLoading,
|
|
984
|
+
setError
|
|
985
|
+
};
|
|
732
986
|
}
|
|
987
|
+
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region src/usePrevious.ts
|
|
990
|
+
/**
|
|
991
|
+
* Hook to store the previous value of a state or prop.
|
|
992
|
+
*
|
|
993
|
+
* @template T - The type of the value.
|
|
994
|
+
* @param {T} value - The current value to be stored.
|
|
995
|
+
* @returns {T | undefined} - The previous value, or undefined if there is no previous value.
|
|
996
|
+
*/
|
|
733
997
|
function usePrevious(value) {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
998
|
+
const ref = useRef(void 0);
|
|
999
|
+
useEffect(() => {
|
|
1000
|
+
ref.current = value;
|
|
1001
|
+
});
|
|
1002
|
+
return ref.current;
|
|
739
1003
|
}
|
|
740
1004
|
|
|
741
|
-
|
|
1005
|
+
//#endregion
|
|
1006
|
+
export { ErrorBoundary, FaasDataWrapper, FaasReactClient, FormContainer as Form, FormContextProvider, FormDefaultElements, FormDefaultLang, FormDefaultRules, FormInput, FormItem, OptionalWrapper, createSplittingContext, equal, faas, getClient, useConstant, useEqualCallback, useEqualEffect, useEqualMemo, useEqualMemoize, useFaas, useFaasStream, useFormContext, usePrevious, useSplittingState, useStateRef, validValues, withFaasData };
|