cogsbox-state 0.5.470 → 0.5.472

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.
@@ -222,11 +222,17 @@ export function FormElementWrapper({
222
222
  formOpts?: FormOptsType;
223
223
  setState: any;
224
224
  }) {
225
- const [componentId] = useState(() => uuidv4());
225
+ const componentId = useRef(uuidv4()).current;
226
226
  const [, forceUpdate] = useState({});
227
227
 
228
228
  const stateKeyPathKey = [stateKey, ...path].join('.');
229
229
  useRegisterComponent(stateKey, componentId, forceUpdate);
230
+
231
+ // Get the shadow node to access typeInfo and schema
232
+ const shadowNode = getGlobalStore.getState().getShadowNode(stateKey, path);
233
+ const typeInfo = shadowNode?._meta?.typeInfo;
234
+ const fieldSchema = typeInfo?.schema; // The actual Zod schema for this field
235
+
230
236
  const globalStateValue = getShadowValue(stateKey, path);
231
237
  const [localValue, setLocalValue] = useState<any>(globalStateValue);
232
238
  const isCurrentlyDebouncing = useRef(false);
@@ -258,13 +264,204 @@ export function FormElementWrapper({
258
264
  };
259
265
  }, []);
260
266
 
267
+ // Separate validation function that uses the field's schema
268
+ const validateField = useCallback(
269
+ (value: any, trigger: 'onChange' | 'onBlur') => {
270
+ const rootMeta = getGlobalStore
271
+ .getState()
272
+ .getShadowMetadata(stateKey, []);
273
+ if (!rootMeta?.features?.validationEnabled) return;
274
+
275
+ const validationOptions = getInitialOptions(stateKey)?.validation;
276
+
277
+ if (!validationOptions) return;
278
+
279
+ const currentMeta = getShadowMetadata(stateKey, path) || {};
280
+ const currentStatus = currentMeta?.validation?.status;
281
+
282
+ let shouldValidate = false;
283
+ let severity: 'error' | 'warning' | undefined;
284
+ console.log('trigger', trigger, validationOptions);
285
+ if (trigger === 'onBlur' && validationOptions.onBlur) {
286
+ shouldValidate = true;
287
+ severity = validationOptions.onBlur ?? 'error';
288
+ } else if (trigger === 'onChange') {
289
+ if (validationOptions.onChange) {
290
+ shouldValidate = true;
291
+ severity = validationOptions.onChange;
292
+ } else if (currentStatus === 'INVALID') {
293
+ shouldValidate = true;
294
+ severity = 'warning';
295
+ }
296
+ }
297
+
298
+ if (!shouldValidate) return;
299
+
300
+ let validationResult: { success: boolean; message?: string } | null =
301
+ null;
302
+ console.log(
303
+ 'shouldValidate 33',
304
+ path,
305
+ fieldSchema,
306
+ shouldValidate,
307
+ value,
308
+ typeof value
309
+ );
310
+ if (fieldSchema && shouldValidate) {
311
+ // Direct field validation using its own schema
312
+ const result = fieldSchema.safeParse(value);
313
+
314
+ if (!result.success) {
315
+ const errors =
316
+ 'issues' in result.error
317
+ ? result.error.issues
318
+ : (result.error as any).errors;
319
+
320
+ validationResult = {
321
+ success: false,
322
+ message: errors[0]?.message || 'Invalid value',
323
+ };
324
+ } else {
325
+ validationResult = { success: true };
326
+ }
327
+ } else {
328
+ // Fallback: validate using the entire schema
329
+ const zodSchema =
330
+ validationOptions.zodSchemaV4 || validationOptions.zodSchemaV3;
331
+ if (!zodSchema) return;
332
+
333
+ // Create a test state with the new value at the correct path
334
+ const fullState = getShadowValue(stateKey, []);
335
+ const testState = JSON.parse(JSON.stringify(fullState)); // Deep clone
336
+
337
+ // Set the value at the correct path
338
+ let current = testState;
339
+ for (let i = 0; i < path.length - 1; i++) {
340
+ if (!current[path[i]!]) current[path[i]!] = {};
341
+ current = current[path[i]!];
342
+ }
343
+ if (path.length > 0) {
344
+ current[path[path.length - 1]!] = value;
345
+ } else {
346
+ // Root level update
347
+ Object.assign(testState, value);
348
+ }
349
+
350
+ const result = zodSchema.safeParse(testState);
351
+
352
+ if (!result.success) {
353
+ const errors =
354
+ 'issues' in result.error
355
+ ? result.error.issues
356
+ : (result.error as any).errors;
357
+
358
+ // Find errors for this specific path
359
+ const pathErrors = errors.filter((error: any) => {
360
+ // Handle array paths with id: prefixes
361
+ if (path.some((p) => p.startsWith('id:'))) {
362
+ const parentPath = path[0]!.startsWith('id:')
363
+ ? []
364
+ : path.slice(0, -1);
365
+ const arrayMeta = getGlobalStore
366
+ .getState()
367
+ .getShadowMetadata(stateKey, parentPath);
368
+
369
+ if (arrayMeta?.arrayKeys) {
370
+ const itemKey = path.slice(0, -1).join('.');
371
+ const itemIndex = arrayMeta.arrayKeys.findIndex(
372
+ (k) => k === path[path.length - 2]
373
+ );
374
+ const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
375
+ return JSON.stringify(error.path) === JSON.stringify(zodPath);
376
+ }
377
+ }
378
+
379
+ return JSON.stringify(error.path) === JSON.stringify(path);
380
+ });
381
+
382
+ if (pathErrors.length > 0) {
383
+ validationResult = {
384
+ success: false,
385
+ message: pathErrors[0]?.message,
386
+ };
387
+ } else {
388
+ validationResult = { success: true };
389
+ }
390
+ } else {
391
+ validationResult = { success: true };
392
+ }
393
+ }
394
+
395
+ // Update validation state based on result
396
+ if (validationResult) {
397
+ if (!validationResult.success) {
398
+ setShadowMetadata(stateKey, path, {
399
+ ...currentMeta,
400
+ validation: {
401
+ status: 'INVALID',
402
+ errors: [
403
+ {
404
+ source: 'client' as const,
405
+ message: validationResult.message!,
406
+ severity: severity!,
407
+ },
408
+ ],
409
+ lastValidated: Date.now(),
410
+ validatedValue: value,
411
+ },
412
+ });
413
+ } else {
414
+ setShadowMetadata(stateKey, path, {
415
+ ...currentMeta,
416
+ validation: {
417
+ status: 'VALID',
418
+ errors: [],
419
+ lastValidated: Date.now(),
420
+ validatedValue: value,
421
+ },
422
+ });
423
+ }
424
+ }
425
+ forceUpdate({});
426
+ },
427
+ [stateKey, path, fieldSchema]
428
+ );
429
+
261
430
  const debouncedUpdate = useCallback(
262
431
  (newValue: any) => {
263
- const currentType = typeof globalStateValue;
264
- if (currentType === 'number' && typeof newValue === 'string') {
265
- newValue = newValue === '' ? 0 : Number(newValue);
432
+ // Use typeInfo to properly convert the value
433
+
434
+ if (typeInfo) {
435
+ if (typeInfo.type === 'number' && typeof newValue === 'string') {
436
+ newValue =
437
+ newValue === ''
438
+ ? typeInfo.nullable
439
+ ? null
440
+ : (typeInfo.default ?? 0)
441
+ : Number(newValue);
442
+ } else if (
443
+ typeInfo.type === 'boolean' &&
444
+ typeof newValue === 'string'
445
+ ) {
446
+ newValue = newValue === 'true' || newValue === '1';
447
+ } else if (typeInfo.type === 'date' && typeof newValue === 'string') {
448
+ newValue = new Date(newValue);
449
+ }
450
+ } else {
451
+ // Fallback to old behavior if no typeInfo
452
+
453
+ const currentType = typeof globalStateValue;
454
+
455
+ if (currentType === 'number' && typeof newValue === 'string') {
456
+ newValue = newValue === '' ? 0 : Number(newValue);
457
+ }
266
458
  }
459
+
267
460
  setLocalValue(newValue);
461
+
462
+ // Validate immediately on change (will only run if configured or clearing errors)
463
+ validateField(newValue, 'onChange');
464
+
268
465
  isCurrentlyDebouncing.current = true;
269
466
 
270
467
  if (debounceTimeoutRef.current) {
@@ -273,184 +470,34 @@ export function FormElementWrapper({
273
470
 
274
471
  const debounceTime = formOpts?.debounceTime ?? 200;
275
472
 
473
+ // Debounce only the state update, not the validation
276
474
  debounceTimeoutRef.current = setTimeout(() => {
277
475
  isCurrentlyDebouncing.current = false;
278
476
  setState(newValue, path, { updateType: 'update' });
279
-
280
- // NEW: Check if validation is enabled via features
281
- const rootMeta = getGlobalStore
282
- .getState()
283
- .getShadowMetadata(stateKey, []);
284
- if (!rootMeta?.features?.validationEnabled) return;
285
-
286
- const validationOptions = getInitialOptions(stateKey)?.validation;
287
- const zodSchema =
288
- validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
289
-
290
- if (zodSchema) {
291
- const fullState = getShadowValue(stateKey, []);
292
- const result = zodSchema.safeParse(fullState);
293
- const currentMeta = getShadowMetadata(stateKey, path) || {};
294
-
295
- if (!result.success) {
296
- const errors =
297
- 'issues' in result.error
298
- ? result.error.issues
299
- : (result.error as any).errors;
300
-
301
- const pathErrors = errors.filter(
302
- (error: any) =>
303
- JSON.stringify(error.path) === JSON.stringify(path)
304
- );
305
-
306
- if (pathErrors.length > 0) {
307
- setShadowMetadata(stateKey, path, {
308
- ...currentMeta,
309
- validation: {
310
- status: 'INVALID',
311
- errors: [
312
- {
313
- source: 'client',
314
- message: pathErrors[0]?.message,
315
- severity: 'warning', // Gentle error during typing
316
- },
317
- ],
318
- lastValidated: Date.now(),
319
- validatedValue: newValue,
320
- },
321
- });
322
- } else {
323
- setShadowMetadata(stateKey, path, {
324
- ...currentMeta,
325
- validation: {
326
- status: 'VALID',
327
- errors: [],
328
- lastValidated: Date.now(),
329
- validatedValue: newValue,
330
- },
331
- });
332
- }
333
- } else {
334
- setShadowMetadata(stateKey, path, {
335
- ...currentMeta,
336
- validation: {
337
- status: 'VALID',
338
- errors: [],
339
- lastValidated: Date.now(),
340
- validatedValue: newValue,
341
- },
342
- });
343
- }
344
- }
345
477
  }, debounceTime);
346
- forceUpdate({});
347
478
  },
348
- [setState, path, formOpts?.debounceTime, stateKey]
479
+ [
480
+ setState,
481
+ path,
482
+ formOpts?.debounceTime,
483
+ validateField,
484
+ typeInfo,
485
+ globalStateValue,
486
+ ]
349
487
  );
350
488
 
351
- const handleBlur = useCallback(async () => {
352
- console.log('handleBlur triggered');
353
-
354
- // Commit any pending changes
489
+ const handleBlur = useCallback(() => {
490
+ // Commit any pending changes immediately
355
491
  if (debounceTimeoutRef.current) {
356
492
  clearTimeout(debounceTimeoutRef.current);
357
493
  debounceTimeoutRef.current = null;
358
494
  isCurrentlyDebouncing.current = false;
359
495
  setState(localValue, path, { updateType: 'update' });
360
496
  }
361
- const rootMeta = getShadowMetadata(stateKey, []);
362
- if (!rootMeta?.features?.validationEnabled) return;
363
- const { getInitialOptions } = getGlobalStore.getState();
364
- const validationOptions = getInitialOptions(stateKey)?.validation;
365
- const zodSchema =
366
- validationOptions?.zodSchemaV4 || validationOptions?.zodSchemaV3;
367
-
368
- if (!zodSchema) return;
369
-
370
- // Get the full path including stateKey
371
-
372
- // Update validation state to "validating"
373
- const currentMeta = getShadowMetadata(stateKey, path);
374
-
375
- setShadowMetadata(stateKey, path, {
376
- ...currentMeta,
377
- validation: {
378
- status: 'VALIDATING',
379
- errors: [],
380
- lastValidated: Date.now(),
381
- validatedValue: localValue,
382
- },
383
- });
384
497
 
385
- // Validate full state
386
- const fullState = getShadowValue(stateKey, []);
387
- const result = zodSchema.safeParse(fullState);
388
-
389
- if (!result.success) {
390
- const errors =
391
- 'issues' in result.error
392
- ? result.error.issues
393
- : (result.error as any).errors;
394
-
395
- // Find errors for this specific path
396
- const pathErrors = errors.filter((error: any) => {
397
- // For array paths, we need to translate indices to ULIDs
398
- if (path.some((p) => p.startsWith('id:'))) {
399
- // This is an array item path like ["id:xyz", "name"]
400
- const parentPath = path[0]!.startsWith('id:')
401
- ? []
402
- : path.slice(0, -1);
403
-
404
- const arrayMeta = getGlobalStore
405
- .getState()
406
- .getShadowMetadata(stateKey, parentPath);
407
-
408
- if (arrayMeta?.arrayKeys) {
409
- const itemKey = [stateKey, ...path.slice(0, -1)].join('.');
410
- const itemIndex = arrayMeta.arrayKeys.indexOf(itemKey);
411
-
412
- // Compare with Zod path
413
- const zodPath = [...parentPath, itemIndex, ...path.slice(-1)];
414
- const match =
415
- JSON.stringify(error.path) === JSON.stringify(zodPath);
416
-
417
- return match;
418
- }
419
- }
420
-
421
- const directMatch = JSON.stringify(error.path) === JSON.stringify(path);
422
-
423
- return directMatch;
424
- });
425
-
426
- // Update shadow metadata with validation result
427
- setShadowMetadata(stateKey, path, {
428
- ...currentMeta,
429
- validation: {
430
- status: 'INVALID',
431
- errors: pathErrors.map((err: any) => ({
432
- source: 'client' as const,
433
- message: err.message,
434
- severity: 'error' as const, // Hard error on blur
435
- })),
436
- lastValidated: Date.now(),
437
- validatedValue: localValue,
438
- },
439
- });
440
- } else {
441
- // Validation passed
442
- setShadowMetadata(stateKey, path, {
443
- ...currentMeta,
444
- validation: {
445
- status: 'VALID',
446
- errors: [],
447
- lastValidated: Date.now(),
448
- validatedValue: localValue,
449
- },
450
- });
451
- }
452
- forceUpdate({});
453
- }, [stateKey, path, localValue, setState]);
498
+ // Validate on blur
499
+ validateField(localValue, 'onBlur');
500
+ }, [localValue, setState, path, validateField]);
454
501
 
455
502
  const baseState = rebuildStateShape({
456
503
  path: path,
@@ -466,7 +513,6 @@ export function FormElementWrapper({
466
513
  onChange: (e: any) => {
467
514
  debouncedUpdate(e.target.value);
468
515
  },
469
- // 5. Wire the new onBlur handler to the input props.
470
516
  onBlur: handleBlur,
471
517
  ref: formRefStore
472
518
  .getState()