cogsbox-state 0.5.432 → 0.5.435

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/src/Functions.tsx CHANGED
@@ -1,130 +1,8 @@
1
- import {
2
- notifyComponent,
3
- type EffectiveSetState,
4
- type FormElementParams,
5
- type FormOptsType,
6
- type UpdateArg,
7
- type UpdateOpts,
8
- } from "./CogsState";
1
+ import { type FormOptsType } from './CogsState';
9
2
 
10
- import {
11
- getNestedValue,
12
- isFunction,
13
- updateNestedProperty,
14
- updateNestedPropertyIds,
15
- } from "./utility";
16
- import { useEffect, useRef, useState } from "react";
17
- import React from "react";
18
- import { getGlobalStore, formRefStore } from "./store";
19
- import { validateZodPathFunc } from "./useValidateZodPath";
20
- import { ulid } from "ulid";
21
-
22
- export function updateFn<U>(
23
- setState: EffectiveSetState<U>,
24
- payload: UpdateArg<U>,
25
- path: string[],
26
- validationKey?: string
27
- ): void {
28
- setState(
29
- (prevState) => {
30
- if (isFunction<U>(payload)) {
31
- const nestedValue = payload(getNestedValue(prevState, path));
32
- console.group("nestedValue", path, nestedValue);
33
- let value = updateNestedPropertyIds(path, prevState, nestedValue);
34
- console.group("updateFn", value);
35
- if (typeof value == "string") {
36
- value = value.trim();
37
- }
38
- return value;
39
- } else {
40
- let value =
41
- !path || path.length == 0
42
- ? payload
43
- : updateNestedPropertyIds(path, prevState, payload);
44
- if (typeof value == "string") {
45
- value = value.trim();
46
- }
47
- return value;
48
- }
49
- },
50
- path,
51
- { updateType: "update" },
52
- validationKey
53
- );
54
- }
55
- export function pushFunc<U>(
56
- setState: EffectiveSetState<U>,
57
- payload: UpdateArg<U>,
58
- path: string[],
59
- stateKey: string,
60
- index?: number
61
- ): void {
62
- // --- THE FIX ---
63
- // 1. Determine the newItem and its ID BEFORE calling setState.
64
- const arrayBeforeUpdate =
65
- (getGlobalStore.getState().getNestedState(stateKey, path) as any[]) || [];
66
-
67
- const newItem = isFunction<U>(payload)
68
- ? payload(arrayBeforeUpdate as any)
69
- : payload;
70
-
71
- // 2. Ensure it has an ID.
72
- if (typeof newItem === "object" && newItem !== null && !(newItem as any).id) {
73
- (newItem as any).id = ulid();
74
- }
75
- const finalId = (newItem as any).id;
76
- // --- END OF FIX ---
77
-
78
- setState(
79
- (prevState) => {
80
- // The logic inside here is now much simpler.
81
- // We already have the final `newItem`.
82
- const arrayToUpdate = getNestedValue(prevState, [...path]) || [];
83
- const newArray = [...arrayToUpdate];
84
- newArray.splice(index ?? newArray.length, 0, newItem);
85
- return updateNestedPropertyIds([...path], prevState, newArray);
86
- },
87
- [...path, `id:${finalId}`], // Now we use the ID that is guaranteed to be correct.
88
- {
89
- updateType: "insert",
90
- }
91
- );
92
- }
93
- export function cutFunc<U>(
94
- setState: EffectiveSetState<U>,
95
- path: string[],
96
- stateKey: string,
97
- index: number
98
- ): void {
99
- // Get the ordered IDs to find the ID for this index
100
- const arrayKey = [stateKey, ...path].join(".");
101
- const arrayMeta = getGlobalStore.getState().shadowStateStore.get(arrayKey);
102
- const itemId = arrayMeta?.arrayKeys?.[index];
103
-
104
- if (!itemId) {
105
- throw new Error(`No ID found for index ${index} in array`);
106
- }
107
-
108
- setState(
109
- (prevState) => {
110
- const arrayToUpdate = getNestedValue(prevState, [...path]);
111
- if (index < 0 || index >= arrayToUpdate?.length) {
112
- throw new Error(`Index ${index} does not exist in the array.`);
113
- }
114
-
115
- const updatedArray = [
116
- ...arrayToUpdate.slice(0, index),
117
- ...arrayToUpdate.slice(index + 1),
118
- ] as U;
119
-
120
- return path.length == 0
121
- ? updatedArray
122
- : updateNestedPropertyIds([...path], prevState, updatedArray);
123
- },
124
- [...path, itemId], // Use the ID here!
125
- { updateType: "cut" }
126
- );
127
- }
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import React from 'react';
5
+ import { getGlobalStore } from './store';
128
6
 
129
7
  export const useStoreSubscription = <T,>(
130
8
  fullPath: string,
@@ -167,9 +45,9 @@ export const useGetValidationErrors = (
167
45
  ) => {
168
46
  const fullPath =
169
47
  validationKey +
170
- "." +
171
- (path.length > 0 ? [path.join(".")] : []) +
172
- (validIndices && validIndices.length > 0 ? "." + validIndices : "");
48
+ '.' +
49
+ (path.length > 0 ? [path.join('.')] : []) +
50
+ (validIndices && validIndices.length > 0 ? '.' + validIndices : '');
173
51
 
174
52
  const returnresult = useStoreSubscription(
175
53
  fullPath,
@@ -179,179 +57,165 @@ export const useGetValidationErrors = (
179
57
  return returnresult;
180
58
  };
181
59
 
182
- export const useGetSyncInfo = (key: string, path: string[]) => {
183
- const syncKey = `${key}:${path.join(".")}`;
184
- return useStoreSubscription(syncKey, (store, path) =>
185
- store.getSyncInfo(path)
186
- );
187
- };
188
- export const useGetKeyState = (key: string, path: string[]) => {
189
- return useStoreSubscription(`${key}:${path.join(".")}`, (store, fullPath) =>
190
- store.getNestedState(key, path)
191
- );
192
- };
193
- interface FormControlComponentProps<TStateObject> {
194
- setState: EffectiveSetState<TStateObject>;
195
-
196
- path: string[];
197
- child: (obj: FormElementParams<TStateObject>) => JSX.Element;
198
- formOpts?: FormOptsType;
199
- stateKey: string;
200
- }
201
60
  // Find FormControlComponent in your Functions.ts or equivalent file
202
61
 
203
- export const FormControlComponent = <TStateObject,>({
204
- setState, // This is the real effectiveSetState from the hook
205
- path,
206
- child,
207
- formOpts,
208
- stateKey,
209
- }: FormControlComponentProps<TStateObject>) => {
210
- const { registerFormRef, getFormRef } = formRefStore.getState();
211
- const {
212
- getValidationErrors,
213
- addValidationError,
214
- getInitialOptions,
215
- removeValidationError,
216
- } = getGlobalStore.getState();
217
-
218
- const refKey = stateKey + "." + path.join(".");
219
- const localFormRef = useRef<HTMLInputElement>(null);
220
- const existingRef = getFormRef(refKey);
221
- if (!existingRef) {
222
- registerFormRef(refKey, localFormRef);
223
- }
224
- const formRef = existingRef || localFormRef;
225
-
226
- // --- START CHANGES ---
227
-
228
- const globalStateValue = useGetKeyState(stateKey, path);
229
- const [localValue, setLocalValue] = useState<any>(globalStateValue);
230
- const isCurrentlyDebouncing = useRef(false);
231
- const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
232
-
233
- // Effect to sync local state if global state changes externally
234
- useEffect(() => {
235
- // Only update local if not actively debouncing a local change
236
- if (!isCurrentlyDebouncing.current && globalStateValue !== localValue) {
237
- setLocalValue(globalStateValue);
238
- }
239
- }, [globalStateValue]); // Removed localValue dependency
240
-
241
- // Effect for cleanup
242
- useEffect(() => {
243
- return () => {
244
- if (debounceTimeoutRef.current) {
245
- clearTimeout(debounceTimeoutRef.current);
246
- debounceTimeoutRef.current = null; // Explicitly nullify
247
- isCurrentlyDebouncing.current = false;
248
- }
249
- };
250
- }, []);
251
-
252
- const debouncedUpdater = (payload: UpdateArg<TStateObject>) => {
253
- setLocalValue(payload); // Update local state immediately
254
- isCurrentlyDebouncing.current = true;
255
-
256
- if (payload === "") {
257
- if (debounceTimeoutRef.current) {
258
- clearTimeout(debounceTimeoutRef.current); // Clear pending timer
259
- debounceTimeoutRef.current = null;
260
- }
261
- updateFn(setState, payload, path, validationKey); // Update global state NOW
262
- isCurrentlyDebouncing.current = false; // No longer debouncing
263
- return; // Don't proceed to set another timeout
264
- }
265
-
266
- // If not empty, proceed with normal debouncing
267
- if (debounceTimeoutRef.current) {
268
- clearTimeout(debounceTimeoutRef.current);
269
- }
270
-
271
- debounceTimeoutRef.current = setTimeout(
272
- () => {
273
- isCurrentlyDebouncing.current = false;
274
- updateFn(setState, payload, path, validationKey);
275
- },
276
- formOpts?.debounceTime ??
277
- (typeof globalStateValue == "boolean" ? 20 : 200)
278
- );
279
- };
280
-
281
- const initialOptions = getInitialOptions(stateKey);
282
- if (!initialOptions?.validation?.key) {
283
- throw new Error("Validation key not found.");
284
- }
285
- const validationKey = initialOptions.validation.key;
286
- const validateOnBlur = initialOptions.validation.onBlur === true;
287
-
288
- const handleBlur = async () => {
289
- // --- Ensure latest value is flushed if debouncing ---
290
- if (debounceTimeoutRef.current) {
291
- clearTimeout(debounceTimeoutRef.current); // Clear pending timer
292
- debounceTimeoutRef.current = null;
293
- isCurrentlyDebouncing.current = false;
294
- // Ensure the absolute latest local value is committed on blur
295
- updateFn(setState, localValue, path, validationKey);
296
- }
297
- // --- End modification ---
298
-
299
- if (!initialOptions.validation?.zodSchema || !validateOnBlur) return;
300
- removeValidationError(validationKey + "." + path.join("."));
301
- try {
302
- // Use the potentially just flushed value
303
- const fieldValue = getGlobalStore
304
- .getState()
305
- .getNestedState(stateKey, path);
306
- await validateZodPathFunc(
307
- validationKey,
308
- initialOptions.validation.zodSchema,
309
- path,
310
- fieldValue
311
- );
312
- // forceUpdate might be needed if validation state update doesn't trigger render
313
- // Consider using useGetValidationErrors hook result directly for validation display
314
- } catch (error) {
315
- console.error("Validation error on blur:", error);
316
- }
317
- };
318
-
319
- const rawSyncStatus = useGetSyncInfo(stateKey, path);
320
- const syncStatus = rawSyncStatus
321
- ? { ...rawSyncStatus, date: new Date(rawSyncStatus.timeStamp) }
322
- : null;
323
-
324
- const childElement = child({
325
- // --- START CHANGES ---
326
- get: () => localValue, // Get should return the immediate local value
327
- set: debouncedUpdater, // Use the new debounced updater
328
- // --- END CHANGES ---
329
- syncStatus,
330
- path: path,
331
- validationErrors: () =>
332
- getValidationErrors(validationKey + "." + path.join(".")),
333
- addValidationError: (message?: string) => {
334
- removeValidationError(validationKey + "." + path.join("."));
335
- addValidationError(validationKey + "." + path.join("."), message ?? "");
336
- },
337
- inputProps: {
338
- // --- START CHANGES ---
339
- value: localValue ?? "", // Input value is always the local state
340
- onChange: (e: any) => debouncedUpdater(e.target.value), // Use debounced updater
341
- // --- END CHANGES ---
342
- onBlur: handleBlur,
343
- ref: formRef,
344
- },
345
- });
346
-
347
- return (
348
- <>
349
- <ValidationWrapper {...{ formOpts, path, stateKey }}>
350
- {childElement}
351
- </ValidationWrapper>
352
- </>
353
- );
354
- };
62
+ // export const FormControlComponent = <TStateObject,>({
63
+ // setState, // This is the real effectiveSetState from the hook
64
+ // path,
65
+ // child,
66
+ // formOpts,
67
+ // stateKey,
68
+ // rebuildStateShape,
69
+ // }: FormControlComponentProps<TStateObject>) => {
70
+ // const { registerFormRef, getFormRef } = formRefStore.getState();
71
+ // const {
72
+ // getValidationErrors,
73
+ // addValidationError,
74
+ // getInitialOptions,
75
+ // removeValidationError,
76
+ // } = getGlobalStore.getState();
77
+ // const stateKeyPathKey = [stateKey, ...path].join('.');
78
+ // const [, forceUpdate] = useState<any>();
79
+ // getGlobalStore.getState().subscribeToPath(stateKeyPathKey, () => {
80
+ // forceUpdate({});
81
+ // });
82
+
83
+ // const refKey = stateKey + '.' + path.join('.');
84
+ // const localFormRef = useRef<HTMLInputElement>(null);
85
+ // const existingRef = getFormRef(refKey);
86
+ // if (!existingRef) {
87
+ // registerFormRef(refKey, localFormRef);
88
+ // }
89
+ // const formRef = existingRef || localFormRef;
90
+
91
+ // // --- START CHANGES ---
92
+
93
+ // const globalStateValue = getGlobalStore
94
+ // .getState()
95
+ // .getShadowValue(stateKeyPathKey);
96
+ // const [localValue, setLocalValue] = useState<any>(globalStateValue);
97
+ // const isCurrentlyDebouncing = useRef(false);
98
+ // const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
99
+
100
+ // // Effect to sync local state if global state changes externally
101
+ // useEffect(() => {
102
+ // // Only update local if not actively debouncing a local change
103
+ // if (!isCurrentlyDebouncing.current && globalStateValue !== localValue) {
104
+ // setLocalValue(globalStateValue);
105
+ // }
106
+ // }, [globalStateValue]); // Removed localValue dependency
107
+
108
+ // // Effect for cleanup
109
+ // useEffect(() => {
110
+ // return () => {
111
+ // if (debounceTimeoutRef.current) {
112
+ // clearTimeout(debounceTimeoutRef.current);
113
+ // debounceTimeoutRef.current = null; // Explicitly nullify
114
+ // isCurrentlyDebouncing.current = false;
115
+ // }
116
+ // };
117
+ // }, []);
118
+
119
+ // const debouncedUpdater = (payload: UpdateArg<TStateObject>) => {
120
+ // setLocalValue(payload); // Update local state immediately
121
+ // isCurrentlyDebouncing.current = true;
122
+
123
+ // if (payload === '') {
124
+ // if (debounceTimeoutRef.current) {
125
+ // clearTimeout(debounceTimeoutRef.current); // Clear pending timer
126
+ // debounceTimeoutRef.current = null;
127
+ // }
128
+
129
+ // setState(payload, path, { updateType: 'update' });
130
+ // isCurrentlyDebouncing.current = false; // No longer debouncing
131
+ // return; // Don't proceed to set another timeout
132
+ // }
133
+
134
+ // // If not empty, proceed with normal debouncing
135
+ // if (debounceTimeoutRef.current) {
136
+ // clearTimeout(debounceTimeoutRef.current);
137
+ // }
138
+
139
+ // debounceTimeoutRef.current = setTimeout(
140
+ // () => {
141
+ // isCurrentlyDebouncing.current = false;
142
+ // console.log('debouncedUpdater', payload);
143
+ // setState(payload, path, { updateType: 'update' });
144
+ // },
145
+ // formOpts?.debounceTime ??
146
+ // (typeof globalStateValue == 'boolean' ? 20 : 200)
147
+ // );
148
+ // };
149
+
150
+ // const initialOptions = getInitialOptions(stateKey);
151
+
152
+ // const validationKey = initialOptions?.validation?.key;
153
+ // const validateOnBlur = initialOptions?.validation?.onBlur === true;
154
+
155
+ // const handleBlur = async () => {
156
+ // // --- Ensure latest value is flushed if debouncing ---
157
+ // if (debounceTimeoutRef.current) {
158
+ // clearTimeout(debounceTimeoutRef.current); // Clear pending timer
159
+ // debounceTimeoutRef.current = null;
160
+ // isCurrentlyDebouncing.current = false;
161
+ // // Ensure the absolute latest local value is committed on blur
162
+ // setState(localValue, path, { updateType: 'update' });
163
+ // }
164
+ // // --- End modification ---
165
+
166
+ // if (!initialOptions?.validation?.zodSchema || !validateOnBlur) return;
167
+ // removeValidationError(validationKey + '.' + path.join('.'));
168
+ // try {
169
+ // // Use the potentially just flushed value
170
+ // if (!validationKey) return;
171
+ // const fieldValue = getGlobalStore
172
+ // .getState()
173
+ // .getShadowValue(stateKeyPathKey);
174
+ // await validateZodPathFunc(
175
+ // validationKey,
176
+ // initialOptions.validation.zodSchema,
177
+ // path,
178
+ // fieldValue
179
+ // );
180
+ // // forceUpdate might be needed if validation state update doesn't trigger render
181
+ // // Consider using useGetValidationErrors hook result directly for validation display
182
+ // } catch (error) {
183
+ // console.error('Validation error on blur:', error);
184
+ // }
185
+ // };
186
+
187
+ // const childElement = child({
188
+ // state: setter,
189
+ // // --- START CHANGES ---
190
+ // get: () => localValue, // Get should return the immediate local value
191
+ // set: debouncedUpdater, // Use the new debounced updater
192
+ // // --- END CHANGES ---
193
+
194
+ // path: path,
195
+ // validationErrors: () =>
196
+ // getValidationErrors(validationKey + '.' + path.join('.')),
197
+ // addValidationError: (message?: string) => {
198
+ // removeValidationError(validationKey + '.' + path.join('.'));
199
+ // addValidationError(validationKey + '.' + path.join('.'), message ?? '');
200
+ // },
201
+ // inputProps: {
202
+ // // --- START CHANGES ---
203
+ // value: localValue ?? '', // Input value is always the local state
204
+ // onChange: (e: any) => debouncedUpdater(e.target.value), // Use debounced updater
205
+ // // --- END CHANGES ---
206
+ // onBlur: handleBlur,
207
+ // ref: formRef,
208
+ // },
209
+ // });
210
+
211
+ // return (
212
+ // <>
213
+ // <ValidationWrapper {...{ formOpts, path, stateKey }}>
214
+ // {childElement}
215
+ // </ValidationWrapper>
216
+ // </>
217
+ // );
218
+ // };
355
219
  export type ValidationWrapperProps = {
356
220
  formOpts?: FormOptsType;
357
221
  path: string[];
@@ -385,7 +249,7 @@ export function ValidationWrapper({
385
249
  const thesMessages: string[] = [];
386
250
 
387
251
  if (validationErrors) {
388
- const newMessage = validationErrors!.join(", ");
252
+ const newMessage = validationErrors!.join(', ');
389
253
  if (!thesMessages.includes(newMessage)) {
390
254
  thesMessages.push(newMessage);
391
255
  }
@@ -401,10 +265,10 @@ export function ValidationWrapper({
401
265
  ),
402
266
  active: validationErrors.length > 0 ? true : false,
403
267
  message: formOpts?.validation?.hideMessage
404
- ? ""
268
+ ? ''
405
269
  : formOpts?.validation?.message
406
270
  ? formOpts?.validation?.message
407
- : thesMessages.map((m) => m).join(", "),
271
+ : thesMessages.map((m) => m).join(', '),
408
272
  path: path,
409
273
  })
410
274
  ) : (