coles-solid-library 0.3.6 → 0.3.8

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.
@@ -1,4 +1,4 @@
1
- import { FormGroupData, ValidationDefault } from "./formHelp/models";
1
+ import { FormGroupData, ValidationDefault, ValidatorResult, ControlMeta } from "./formHelp/models";
2
2
  export { FormArray } from "./formHelp/formArray";
3
3
  export { Validators } from "./formHelp/validators";
4
4
  /**
@@ -11,8 +11,14 @@ export declare class FormGroup<T extends object> {
11
11
  private internalDataSignal;
12
12
  private validators;
13
13
  private errors;
14
+ private meta;
14
15
  private keys;
15
16
  constructor(data: FormGroupData<T>);
17
+ /**
18
+ * INTERNAL: returns the reactive internal store reference (DO NOT MUTATE outside FormGroup).
19
+ * Used for bridging into FormContext without triggering cloning loops.
20
+ */
21
+ _unsafeRaw(): T;
16
22
  /**
17
23
  * Gets the current form data or the value of a specific control.
18
24
  *
@@ -70,6 +76,12 @@ export declare class FormGroup<T extends object> {
70
76
  * @param hasError - Whether the error should be set or cleared
71
77
  */
72
78
  setError(key: keyof T, errKey: string, hasError: boolean): void;
79
+ addValidator<K extends keyof T>(key: K, validator: ValidatorResult<T[K]>): void;
80
+ removeValidator<K extends keyof T>(key: K, errKey: string): void;
81
+ getMeta<K extends keyof T>(key: K): ControlMeta<T[K]>;
82
+ markTouched<K extends keyof T>(key: K): void;
83
+ markDirty<K extends keyof T>(key: K): void;
84
+ reset(): void;
73
85
  /**
74
86
  * Sets a new value for a specified form control.
75
87
  *
@@ -101,6 +113,7 @@ export declare class FormGroup<T extends object> {
101
113
  * @returns `true` if the control(s) pass all validations; otherwise, `false`.
102
114
  */
103
115
  validate<K extends keyof T>(key?: K): boolean;
116
+ validateAsync<K extends keyof T>(key?: K): Promise<boolean>;
104
117
  }
105
118
  /**
106
119
  * Utility type to extract the element type from an array type
@@ -1,8 +1,9 @@
1
1
  import { JSX } from "solid-js";
2
2
  import { FormArray } from "./formArray";
3
3
  export interface ValidatorResult<T> {
4
- revalidate: (val: T) => boolean;
5
4
  errKey: string;
5
+ revalidate: (val: T) => boolean | Promise<boolean>;
6
+ hide?: (val: T) => boolean;
6
7
  }
7
8
  export interface Error {
8
9
  key: string;
@@ -14,18 +15,14 @@ export type ErrorObject<T> = {
14
15
  [P in keyof T]: Error[];
15
16
  };
16
17
  export type ArrayValidation<T> = [(ValidationDefault<T, keyof T>)[], ValidatorResult<T[]>[]];
17
- export interface ValidatorResult<T> {
18
- revalidate: (val: T) => boolean;
19
- errKey: string;
20
- hide?: (val: T) => boolean;
21
- }
22
- export interface Error {
23
- key: string;
24
- hasError: boolean;
25
- }
26
18
  export type ValidatorObject<T> = {
27
19
  [P in keyof T]?: ValidatorResult<T[P]>[];
28
20
  };
29
21
  export type FormGroupData<T extends object> = {
30
- [P in keyof T]: [T[P], ValidatorResult<T[P]>[]] | (T[P] extends object ? FormArray<T[P]> : any);
22
+ [P in keyof T]: [T[P], ValidatorResult<T[P]>[]] | (T[P] extends (infer U)[] ? FormArray<U & object> : T[P] extends object ? FormArray<T[P] & object> : any);
31
23
  };
24
+ export interface ControlMeta<T = any> {
25
+ touched: boolean;
26
+ dirty: boolean;
27
+ initialValue: T;
28
+ }
@@ -67,6 +67,10 @@ export declare class Validators {
67
67
  * @returns A ValidatorResult that includes the error key and a revalidation function.
68
68
  */
69
69
  static custom<T>(errKey: string, validator: (value: T) => boolean, hide?: (val: T) => boolean): ValidatorResult<T>;
70
+ /**
71
+ * Creates an async validator; resolves promise to boolean.
72
+ */
73
+ static asyncCustom<T>(errKey: string, validator: (value: T) => Promise<boolean>, hide?: (val: T) => boolean): ValidatorResult<T>;
70
74
  /**
71
75
  * A helper function that creates a ValidatorResult object.
72
76
  *
@@ -0,0 +1,14 @@
1
+ import { Accessor } from 'solid-js';
2
+ export interface FieldBinding<T, K extends keyof T> {
3
+ value: Accessor<T[K]>;
4
+ setValue: (v: T[K]) => void;
5
+ errors: () => {
6
+ key: string;
7
+ hasError: boolean;
8
+ }[];
9
+ hasError: () => boolean;
10
+ touched: () => boolean;
11
+ dirty: () => boolean;
12
+ validate: () => boolean;
13
+ }
14
+ export declare function useFormFieldBinding<T extends object, K extends keyof T>(key: K): FieldBinding<T, K>;
@@ -1,7 +1,9 @@
1
1
  import { Accessor, Component, JSX, Setter } from "solid-js";
2
2
  interface Props extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement> {
3
- text: Accessor<string>;
4
- setText: Setter<string>;
3
+ /** Optional external accessor (legacy). If omitted and inside a FormField+Form, form data is used. */
4
+ text?: Accessor<string>;
5
+ /** Optional external setter (legacy). */
6
+ setText?: Setter<string>;
5
7
  class?: string;
6
8
  tooltip?: string;
7
9
  transparent?: boolean;
package/dist/index.esm.js CHANGED
@@ -1400,10 +1400,13 @@ const useFormContext = () => {
1400
1400
  };
1401
1401
  const Form = props => {
1402
1402
  const startData = props.data.get() ?? {};
1403
+ // Custom shallow clone preserving FormArray instances (already handled in FormGroup.get)
1404
+ const initialValue = {};
1405
+ Object.keys(startData).forEach(k => {
1406
+ initialValue[k] = startData[k];
1407
+ });
1403
1408
  return createComponent(Provider, {
1404
- get value() {
1405
- return CloneStore(startData);
1406
- },
1409
+ value: initialValue,
1407
1410
  get formGroup() {
1408
1411
  return props.data;
1409
1412
  },
@@ -1438,6 +1441,17 @@ const FormInner = props => {
1438
1441
  const Provider = props => {
1439
1442
  const defaultData = props.value ?? {};
1440
1443
  const [data, setData] = createStore(defaultData);
1444
+ // Bridge: keep FormContext store in sync with underlying FormGroup reactive store
1445
+ // This allows programmatic calls to formGroup.set(...) to propagate into bound fields.
1446
+ createEffect(() => {
1447
+ const raw = props.formGroup._unsafeRaw(); // reactive read of underlying store
1448
+ for (const k in raw) {
1449
+ const nextVal = raw[k];
1450
+ if (data[k] !== nextVal) {
1451
+ setData(k, nextVal);
1452
+ }
1453
+ }
1454
+ });
1441
1455
  return createComponent(FormContext.Provider, {
1442
1456
  get value() {
1443
1457
  return {
@@ -1498,21 +1512,21 @@ const Input = props => {
1498
1512
  }
1499
1513
  if (props.onChange) props.onChange(e);
1500
1514
  };
1501
- createEffect(() => {
1502
- if (isRequired()) {
1503
- context?.setName?.(old => `${old} *`);
1504
- } else {
1505
- context?.setName?.(old => old);
1506
- }
1507
- });
1515
+ // Removed name mutation adding '*' to avoid duplicating required indicator; legend handles display.
1508
1516
  onMount(() => {
1509
1517
  if (!isNullish(context.getName)) {
1510
1518
  // Force a non-checkbox field type
1511
1519
  context.setFieldType(props.type === "checkbox" ? "text" : props.type ?? "text");
1512
1520
  if (!isNullish(formContext?.data)) {
1513
- const formValue = formContext.data[context.formName ?? ""]?.trim();
1514
- if (formValue) {
1515
- context.setValue(formValue);
1521
+ const raw = formContext.data[context.formName ?? ""];
1522
+ if (typeof raw === 'string') {
1523
+ const formValue = raw.trim();
1524
+ if (formValue) {
1525
+ context.setValue(formValue);
1526
+ context.setTextInside(true);
1527
+ }
1528
+ } else if (raw !== undefined && raw !== null) {
1529
+ context.setValue(raw);
1516
1530
  context.setTextInside(true);
1517
1531
  }
1518
1532
  }
@@ -1585,23 +1599,52 @@ styleInject(css_248z$d);
1585
1599
  var _tmpl$$h = /*#__PURE__*/template(`<label><input type=checkbox><span>`),
1586
1600
  _tmpl$2$9 = /*#__PURE__*/template(`<span>`);
1587
1601
  function Checkbox(props) {
1588
- // Handle controlled vs. uncontrolled state
1602
+ const field = useFormProvider();
1603
+ const formCtx = useFormContext();
1604
+ const formName = field?.formName;
1605
+ // Internal state for uncontrolled usage outside form context
1589
1606
  const [internalChecked, setInternalChecked] = createSignal(props.defaultChecked ?? false);
1590
- const checkedState = createMemo(() => props.checked !== undefined ? props.checked : internalChecked());
1607
+ // Derive current checked state with priority: controlled prop -> form context -> field local value -> internal state
1608
+ const checkedState = createMemo(() => {
1609
+ if (props.checked !== undefined) return !!props.checked;
1610
+ if (formName && formCtx?.data) return !!formCtx.data[formName];
1611
+ if (field?.getValue) return !!field.getValue();
1612
+ return internalChecked();
1613
+ });
1614
+ // Keep field/form floating states in sync if programmatic changes occur
1615
+ createEffect(() => {
1616
+ const c = checkedState();
1617
+ if (formName && formCtx?.data) {
1618
+ field?.setTextInside?.(c);
1619
+ field?.setValue?.(c);
1620
+ }
1621
+ });
1622
+ const commitValue = next => {
1623
+ if (props.checked === undefined) {
1624
+ if (formName && formCtx?.formGroup) {
1625
+ formCtx.formGroup.set(formName, next);
1626
+ formCtx.setData?.(formName, next);
1627
+ } else if (field?.setValue) {
1628
+ field.setValue(next);
1629
+ } else {
1630
+ setInternalChecked(next);
1631
+ }
1632
+ }
1633
+ field?.setTextInside?.(next);
1634
+ props.onChange?.(next);
1635
+ };
1591
1636
  const handleChange = e => {
1592
1637
  const target = e.currentTarget;
1593
1638
  const newChecked = target.checked;
1594
- if (props.checked === undefined) {
1595
- setInternalChecked(newChecked);
1596
- props.onChange?.(newChecked);
1597
- } else {
1598
- // Controlled: prevent native state flip from persisting
1639
+ if (props.checked !== undefined) {
1640
+ // controlled: revert DOM change and just emit
1599
1641
  e.preventDefault();
1600
- // Re-sync DOM to prop (in case browser already toggled visually before preventDefault took effect)
1601
1642
  queueMicrotask(() => {
1602
1643
  target.checked = !!props.checked;
1603
1644
  });
1604
1645
  props.onChange?.(newChecked);
1646
+ } else {
1647
+ commitValue(newChecked);
1605
1648
  }
1606
1649
  };
1607
1650
  const handleClick = e => {
@@ -2003,6 +2046,9 @@ function Select(props) {
2003
2046
  }
2004
2047
  // store orientation for class assignment
2005
2048
  setDropTop(placeAbove);
2049
+ // Compute available vertical space for dropdown and clamp to a sensible minimum
2050
+ const availableSpace = (placeAbove ? spaceAbove : spaceBelow) - VIEWPORT_MARGIN;
2051
+ const maxHeight = Math.max(160, availableSpace); // ensure at least 160px so a few options are visible
2006
2052
  let newY;
2007
2053
  if (!placeAbove) {
2008
2054
  newY = baseRect.bottom + window.scrollY; // default below
@@ -2024,6 +2070,7 @@ function Select(props) {
2024
2070
  dropdown.style.left = `${newX}px`;
2025
2071
  dropdown.style.top = `${newY}px`;
2026
2072
  dropdown.style.width = `${baseRect.width}px`;
2073
+ dropdown.style.maxHeight = `${maxHeight}px`;
2027
2074
  };
2028
2075
  // Update width of select to match option text width
2029
2076
  createEffect(() => {
@@ -2122,7 +2169,6 @@ function Select(props) {
2122
2169
  selectStyle: currStyle
2123
2170
  }));
2124
2171
  const [dropTop, setDropTop] = createSignal(false);
2125
- console.log("Select rendered with options:", options);
2126
2172
  return (() => {
2127
2173
  var _el$7 = _tmpl$5$1(),
2128
2174
  _el$8 = _el$7.firstChild,
@@ -2176,7 +2222,6 @@ function Select(props) {
2176
2222
  get children() {
2177
2223
  var _el$12 = _tmpl$4$1();
2178
2224
  use(setDropdownRef, _el$12);
2179
- _el$12.style.setProperty("max-height", "calc(100vh - 8px)");
2180
2225
  insert(_el$12, () => props.children);
2181
2226
  createRenderEffect(_p$ => {
2182
2227
  var _v$ = `${styles$7['solid_select__dropdown']} ${dropTop() ? styles$7.dropTop : styles$7.dropBottom} ${open() ? styles$7.open : ''} ${props.dropdownClass || ""}`,
@@ -2299,7 +2344,6 @@ function Option(props) {
2299
2344
  const contextSuccess = selectFormContextValue(props.value);
2300
2345
  !contextSuccess ? selectFormFieldValue(props.value) : true;
2301
2346
  if (!formField?.getName?.()) {
2302
- console.log('selected!', props.value, select);
2303
2347
  select.selectValue?.(props.value);
2304
2348
  formField?.setFocused?.(true);
2305
2349
  }
@@ -2344,76 +2388,101 @@ var _tmpl$$e = /*#__PURE__*/template(`<textarea>`);
2344
2388
  const TextArea = props => {
2345
2389
  let myElement;
2346
2390
  const [customProps, normalProps] = splitProps(props, ["minSize", "text", "setText", "class", "tooltip", "transparent"]);
2347
- const context = useFormProvider();
2348
- /**
2349
- * Function to set the height of the textarea based on the content
2350
- */
2351
- function OnInput() {
2352
- if (myElement) {
2353
- myElement.style.height = 'auto';
2354
- const minHeight = customProps.minSize?.height ?? 100;
2355
- const currentHeight = myElement.scrollHeight < minHeight ? minHeight : myElement.scrollHeight;
2356
- myElement.style.height = `${currentHeight}px`;
2357
- myElement.setAttribute("style", "height:" + currentHeight + "px;overflow-y:hidden;");
2358
- }
2391
+ const fieldCtx = useFormProvider();
2392
+ const formCtx = useFormContext();
2393
+ const formName = fieldCtx?.formName;
2394
+ // Internal state only used when not provided externally and not in form context.
2395
+ const [internal, setInternal] = createSignal(customProps.text ? customProps.text() : "");
2396
+ // Determine current value priority: FormGroup -> external accessor -> internal state
2397
+ const areaValue = createMemo(() => {
2398
+ if (formName && formCtx?.data && !isNullish(formCtx.data[formName])) {
2399
+ return formCtx.data[formName];
2400
+ }
2401
+ if (customProps.text) return customProps.text();
2402
+ return internal();
2403
+ });
2404
+ function resizeToContent() {
2405
+ if (!myElement) return;
2406
+ myElement.style.height = 'auto';
2407
+ const minHeight = customProps.minSize?.height ?? 100;
2408
+ const currentHeight = Math.max(minHeight, myElement.scrollHeight);
2409
+ myElement.style.height = `${currentHeight}px`;
2410
+ myElement.style.overflowY = 'hidden';
2359
2411
  }
2360
- // sets the field type on mount and sets the height of the textarea
2412
+ // Set field type & initial floating state
2361
2413
  onMount(() => {
2362
- OnInput();
2363
- context?.setFieldType("textarea");
2414
+ resizeToContent();
2415
+ fieldCtx?.setFieldType?.('textarea');
2416
+ if (formName && formCtx?.data) {
2417
+ const v = formCtx.data[formName];
2418
+ if (!isNullish(v) && String(v).trim() !== '') {
2419
+ fieldCtx?.setValue?.(v);
2420
+ fieldCtx?.setTextInside?.(true);
2421
+ } else {
2422
+ fieldCtx?.setTextInside?.(false);
2423
+ }
2424
+ } else if (customProps.text) {
2425
+ const v = customProps.text();
2426
+ fieldCtx?.setTextInside?.(!(v === undefined || v === null || v.trim() === ''));
2427
+ }
2364
2428
  });
2365
- // updates height of textarea when text is changed
2366
- createRenderEffect(() => {
2367
- OnInput();
2368
- customProps.text();
2429
+ // React to programmatic FormGroup.set changes
2430
+ createEffect(() => {
2431
+ if (formName && formCtx?.data) {
2432
+ const v = formCtx.data[formName];
2433
+ // keep internal state aligned if unmanaged
2434
+ if (!customProps.text && internal() !== v) setInternal(String(v ?? ''));
2435
+ if (fieldCtx) {
2436
+ const has = !(v === undefined || v === null || typeof v === 'string' && v.trim() === '');
2437
+ fieldCtx.setTextInside(has);
2438
+ fieldCtx.setValue?.(v);
2439
+ }
2440
+ }
2369
2441
  });
2442
+ // Resize whenever value changes (user or programmatic)
2370
2443
  createEffect(() => {
2371
- if (Object.keys(props).includes("required") || props?.required === true) {
2372
- context?.setName(old => `${old} *`);
2444
+ areaValue();
2445
+ queueMicrotask(resizeToContent);
2446
+ });
2447
+ const handleInput = e => {
2448
+ const newVal = e.currentTarget.value;
2449
+ // Update whichever source is active
2450
+ if (formName && formCtx?.formGroup) {
2451
+ formCtx.formGroup.set(formName, newVal);
2452
+ formCtx.setData?.(formName, newVal);
2453
+ } else if (customProps.setText) {
2454
+ customProps.setText(newVal);
2373
2455
  } else {
2374
- context?.setName(old => old);
2456
+ setInternal(newVal);
2375
2457
  }
2376
- });
2458
+ if (fieldCtx) {
2459
+ fieldCtx.setValue?.(newVal);
2460
+ fieldCtx.setTextInside?.(newVal.trim().length > 0);
2461
+ }
2462
+ resizeToContent();
2463
+ };
2377
2464
  return (() => {
2378
2465
  var _el$ = _tmpl$$e();
2379
2466
  use(el => {
2380
2467
  myElement = el;
2381
- OnInput();
2468
+ resizeToContent();
2382
2469
  }, _el$);
2383
2470
  spread(_el$, mergeProps(normalProps, {
2384
- "onFocus": e => {
2385
- if (!isNullish(context?.getName)) {
2386
- context?.setFocused(true);
2387
- }
2388
- },
2389
- "onBlur": e => {
2390
- if (context?.setFocused) {
2391
- context?.setFocused(false);
2392
- }
2393
- },
2394
- get placeholder() {
2395
- return !!context?.getName && context?.getTextInside() && !context?.getFocused() ? "" : props.placeholder;
2471
+ get value() {
2472
+ return areaValue();
2396
2473
  },
2397
2474
  get ["class"]() {
2398
- return `${style$4.areaStyle} ${customProps.class ?? ""} ${customProps.transparent ? customProps.transparent : ""}`;
2475
+ return `${style$4.areaStyle} ${customProps.class ?? ''} ${customProps.transparent ? customProps.transparent : ''}`;
2399
2476
  },
2400
- get value() {
2401
- return customProps.text();
2402
- },
2403
- "onInput": e => {
2404
- customProps.setText(e.currentTarget.value);
2405
- OnInput();
2406
- if (!!context?.getName && !!e.currentTarget.value.trim()) {
2407
- context?.setValue(e.currentTarget.value);
2408
- context?.setTextInside(true);
2409
- } else if (!!context.getName && !e.currentTarget.value.trim()) {
2410
- context?.setValue("");
2411
- context?.setTextInside(false);
2412
- }
2477
+ get placeholder() {
2478
+ return fieldCtx?.getTextInside?.() && !fieldCtx?.getFocused?.() ? '' : props.placeholder;
2413
2479
  },
2414
2480
  get title() {
2415
2481
  return customProps.tooltip;
2416
- }
2482
+ },
2483
+ "onInput": handleInput,
2484
+ "onFocus": () => fieldCtx?.setFocused?.(true),
2485
+ "onBlur": () => fieldCtx?.setFocused?.(false)
2417
2486
  }), false, false);
2418
2487
  return _el$;
2419
2488
  })();
@@ -3000,11 +3069,21 @@ function RadioGroup(props) {
3000
3069
  radioGroupCount++;
3001
3070
  const groupName = props.name ?? `radio-group-${radioGroupCount}`;
3002
3071
  const [internalValue, setInternalValue] = createSignal(props.defaultValue);
3072
+ const formField = useFormProvider();
3073
+ const formContext = useFormContext();
3003
3074
  const selectedValue = () => props.value !== undefined ? props.value : internalValue();
3004
3075
  const setSelectedValue = val => {
3005
3076
  if (props.value === undefined) {
3006
3077
  setInternalValue(val);
3007
3078
  }
3079
+ // Bridge to FormGroup if inside a FormField with formName
3080
+ if (formField?.formName && formContext?.formGroup?.set) {
3081
+ formContext.formGroup.set(formField.formName, val);
3082
+ formContext.setData(old => ({
3083
+ ...old,
3084
+ [formField.formName]: val
3085
+ }));
3086
+ }
3008
3087
  props.onChange?.(val);
3009
3088
  };
3010
3089
  const radioRefs = [];
@@ -3450,39 +3529,47 @@ class FormArray {
3450
3529
  }
3451
3530
  // Validate all controls in the form array.
3452
3531
  if (isNullish(index)) {
3453
- const cleanValue = val => {
3454
- if (isNullish(val)) {
3455
- return [];
3456
- } else if (Array.isArray(val)) {
3457
- return val;
3458
- } else {
3459
- return [val];
3460
- }
3461
- };
3462
- this.errors = cleanValue(values).map((value, i) => {
3463
- const errors = this.internalValidation.map(([_, validators]) => {
3532
+ // Always evaluate array-level validators even if the array is empty
3533
+ const arrayLevelErrors = this.internalArrayValidation.map(validator => ({
3534
+ key: validator.errKey,
3535
+ hasError: !validator.revalidate(values)
3536
+ }));
3537
+ if (values.length === 0) {
3538
+ this.errors = [arrayLevelErrors]; // store array-level errors at index 0 placeholder
3539
+ return this.errors.flat().every(error => !error.hasError);
3540
+ }
3541
+ this.errors = values.map((value, i) => {
3542
+ const controlErrors = this.internalValidation.map(([propKey, validators]) => {
3543
+ const currentVal = value[propKey];
3464
3544
  return validators.map(validator => ({
3465
3545
  key: validator.errKey,
3466
- hasError: !validator.revalidate(value)
3546
+ hasError: !validator.revalidate(currentVal)
3467
3547
  }));
3468
3548
  });
3469
3549
  const arrayErrors = this.internalArrayValidation.map(validator => ({
3470
3550
  key: validator.errKey,
3471
3551
  hasError: !validator.revalidate(values)
3472
3552
  }));
3473
- this.errors[i] = [...errors.flat(), ...arrayErrors];
3474
- return [...errors.flat(), ...arrayErrors];
3553
+ const merged = [...controlErrors.flat(), ...arrayErrors];
3554
+ this.errors[i] = merged;
3555
+ return merged;
3475
3556
  });
3476
3557
  return this.errors.flat().every(error => !error.hasError);
3477
3558
  }
3478
3559
  // Validate a specific control in the form array.
3479
- const errors = this.internalValidation.map(([_, validators]) => {
3560
+ const value = values[index];
3561
+ const controlErrors = this.internalValidation.map(([propKey, validators]) => {
3562
+ const currentVal = value[propKey];
3480
3563
  return validators.map(validator => ({
3481
3564
  key: validator.errKey,
3482
- hasError: !validator.revalidate(values[index])
3565
+ hasError: !validator.revalidate(currentVal)
3483
3566
  }));
3484
3567
  });
3485
- this.errors[index] = errors.flat();
3568
+ const arrayErrors = this.internalArrayValidation.map(validator => ({
3569
+ key: validator.errKey,
3570
+ hasError: !validator.revalidate(values)
3571
+ }));
3572
+ this.errors[index] = [...controlErrors.flat(), ...arrayErrors];
3486
3573
  return this.errors[index].every(error => !error.hasError);
3487
3574
  }
3488
3575
  }
@@ -3529,15 +3616,16 @@ const FormField2 = props => {
3529
3616
  });
3530
3617
  const theChildren = children(() => props.children);
3531
3618
  const formErrors = () => {
3532
- const allErrors = (formContext?.formGroup.getErrors(local?.formName ?? '') ?? []).filter(error => error.hasError);
3619
+ if (!local?.formName) return [];
3620
+ const allErrors = (formContext?.formGroup.getErrors(local.formName) ?? []).filter(e => e.hasError);
3533
3621
  if (allErrors.length === 0) return [];
3534
- let errKeys = allErrors.map(error => error.key);
3535
- // test just require
3622
+ let errKeys = allErrors.map(e => e.key);
3536
3623
  if (errKeys.includes('required')) {
3537
- errKeys = errKeys.filter(err => err !== 'minLength' && err !== 'maxLength');
3624
+ errKeys = errKeys.filter(k => k !== 'minLength' && k !== 'maxLength');
3538
3625
  }
3539
- //---
3540
- return context?.getErrors?.().err.filter(err => errKeys.includes(err.key));
3626
+ // Map to displays stored in context errors (ColeError registered displays) if available
3627
+ const displayMap = context?.getErrors?.().err ?? [];
3628
+ return displayMap.filter(e => errKeys.includes(e.key));
3541
3629
  };
3542
3630
  const hasRequired = createMemo(() => {
3543
3631
  if (isNullish(local?.formName)) return false;
@@ -3746,7 +3834,13 @@ class Validators {
3746
3834
  * @returns A ValidatorResult that includes the error key and a revalidation function.
3747
3835
  */
3748
3836
  static custom(errKey, validator, hide) {
3749
- return this.createValidatorResult(errKey, validator);
3837
+ return this.createValidatorResult(errKey, validator, hide);
3838
+ }
3839
+ /**
3840
+ * Creates an async validator; resolves promise to boolean.
3841
+ */
3842
+ static asyncCustom(errKey, validator, hide) {
3843
+ return this.createValidatorResult(errKey, validator, hide);
3750
3844
  }
3751
3845
  /**
3752
3846
  * A helper function that creates a ValidatorResult object.
@@ -3777,6 +3871,7 @@ class FormGroup {
3777
3871
  internalDataSignal;
3778
3872
  validators = {};
3779
3873
  errors;
3874
+ meta = {};
3780
3875
  keys = [];
3781
3876
  constructor(data) {
3782
3877
  this.data = data;
@@ -3787,11 +3882,8 @@ class FormGroup {
3787
3882
  const value = data[key];
3788
3883
  this.keys.push(key);
3789
3884
  if (value instanceof FormArray) {
3790
- // If the value is a FormArray, store it directly.
3791
- // We need to ensure that when T[key] is an array type, we store the array returned by FormArray
3792
- newData[key] = value.get();
3885
+ newData[key] = value;
3793
3886
  } else {
3794
- // Otherwise, initialize the data, validators, and errors for the control.
3795
3887
  newData[key] = value[0];
3796
3888
  newValidators[key] = value[1];
3797
3889
  newErrors[key] = value[1].map(validator => ({
@@ -3799,13 +3891,38 @@ class FormGroup {
3799
3891
  hasError: false
3800
3892
  }));
3801
3893
  }
3894
+ const initialVal = value instanceof FormArray ? value.get() : CloneStore(newData[key]);
3895
+ this.meta[key] = {
3896
+ touched: false,
3897
+ dirty: false,
3898
+ initialValue: initialVal
3899
+ };
3802
3900
  }
3803
3901
  this.internalDataSignal = createStore(newData);
3804
3902
  this.validators = newValidators;
3805
3903
  this.errors = createSignal(newErrors);
3806
3904
  }
3905
+ /**
3906
+ * INTERNAL: returns the reactive internal store reference (DO NOT MUTATE outside FormGroup).
3907
+ * Used for bridging into FormContext without triggering cloning loops.
3908
+ */
3909
+ _unsafeRaw() {
3910
+ return this.internalDataSignal[0];
3911
+ }
3807
3912
  get(key) {
3808
- if (!key) return CloneStore(this.internalDataSignal[0]);
3913
+ if (!key) {
3914
+ // Custom clone that preserves FormArray instances (structuredClone fails on functions inside)
3915
+ const clone = {};
3916
+ for (const k of this.keys) {
3917
+ const val = this.internalDataSignal[0][k];
3918
+ if (val instanceof FormArray) {
3919
+ clone[k] = val; // keep reference; consumer methods operate on instance
3920
+ } else {
3921
+ clone[k] = CloneStore(val);
3922
+ }
3923
+ }
3924
+ return clone;
3925
+ }
3809
3926
  // If the control is a FormArray, use its get method
3810
3927
  if (this.internalDataSignal[0][key] instanceof FormArray) {
3811
3928
  // Return the array from FormArray.get() as the expected type T[K]
@@ -3885,16 +4002,56 @@ class FormGroup {
3885
4002
  * @param hasError - Whether the error should be set or cleared
3886
4003
  */
3887
4004
  setError(key, errKey, hasError) {
3888
- if (this.internalDataSignal[0][key] instanceof FormArray) {
3889
- // Not supported for FormArray - errors are managed internally
3890
- return;
3891
- }
3892
- const errorIndex = this.errors[0]()[key]?.findIndex(error => error.key === errKey);
3893
- if (errorIndex !== -1) {
3894
- this.errors[1](old => {
3895
- old[key][errorIndex].hasError = hasError;
3896
- return Clone(old);
4005
+ if (this.internalDataSignal[0][key] instanceof FormArray) return;
4006
+ const current = this.errors[0]();
4007
+ const list = current[key] ?? [];
4008
+ let index = list.findIndex(e => e.key === errKey);
4009
+ if (index === -1) {
4010
+ list.push({
4011
+ key: errKey,
4012
+ hasError
3897
4013
  });
4014
+ } else {
4015
+ list[index].hasError = hasError;
4016
+ }
4017
+ current[key] = list;
4018
+ this.errors[1](() => Clone(current));
4019
+ }
4020
+ addValidator(key, validator) {
4021
+ if (!this.validators[key]) this.validators[key] = [];
4022
+ this.validators[key].push(validator);
4023
+ this.setError(key, validator.errKey, false);
4024
+ }
4025
+ removeValidator(key, errKey) {
4026
+ if (!this.validators[key]) return;
4027
+ this.validators[key] = this.validators[key].filter(v => v.errKey !== errKey);
4028
+ const current = this.errors[0]();
4029
+ if (current[key]) {
4030
+ current[key] = current[key].filter(e => e.key !== errKey);
4031
+ this.errors[1](() => Clone(current));
4032
+ }
4033
+ }
4034
+ getMeta(key) {
4035
+ return this.meta[key];
4036
+ }
4037
+ markTouched(key) {
4038
+ this.meta[key].touched = true;
4039
+ }
4040
+ markDirty(key) {
4041
+ this.meta[key].dirty = true;
4042
+ }
4043
+ reset() {
4044
+ for (const k of this.keys) {
4045
+ const m = this.meta[k];
4046
+ this.set(k, CloneStore(m.initialValue));
4047
+ m.touched = false;
4048
+ m.dirty = false;
4049
+ const errs = this.errors[0]();
4050
+ errs[k] = errs[k]?.map(e => ({
4051
+ ...e,
4052
+ hasError: false
4053
+ }));
4054
+ this.errors[1](() => Clone(errs));
3898
4055
  }
3899
4056
  }
3900
4057
  /**
@@ -3915,6 +4072,11 @@ class FormGroup {
3915
4072
  ...old,
3916
4073
  [key]: value
3917
4074
  }));
4075
+ const m = this.meta[key];
4076
+ if (m) {
4077
+ if (!m.touched) m.touched = true;
4078
+ if (!m.dirty && JSON.stringify(m.initialValue) !== JSON.stringify(value)) m.dirty = true;
4079
+ }
3918
4080
  }
3919
4081
  /**
3920
4082
  * Adds an item to a FormArray control
@@ -3949,28 +4111,62 @@ class FormGroup {
3949
4111
  */
3950
4112
  validate(key) {
3951
4113
  if (isNullish(this.internalDataSignal?.[0])) {
3952
- console.error('Data is null');
4114
+ if (process?.env?.NODE_ENV !== 'production') console.error('Data is null');
3953
4115
  return false;
3954
4116
  }
3955
4117
  if (!key) {
3956
4118
  const results = this.keys.map(k => this.validate(k));
3957
- return results.every(result => result);
4119
+ return results.every(Boolean);
3958
4120
  }
3959
4121
  if (this.internalDataSignal?.[0]?.[key] instanceof FormArray) {
3960
4122
  return this.internalDataSignal[0][key].validateCurrent();
3961
4123
  }
3962
4124
  const validators = this.validators[key];
3963
4125
  if (!validators) return true;
3964
- return !validators.map(validator => {
3965
- const hasError = !validator.revalidate(this.internalDataSignal[0][key]);
3966
- const toHide = validator.hide?.(this.internalDataSignal[0][key]);
3967
- if (toHide) {
3968
- this.setError(key, validator.errKey, false);
3969
- return true;
4126
+ let allValid = true;
4127
+ for (const validator of validators) {
4128
+ try {
4129
+ const result = validator.revalidate(this.internalDataSignal[0][key]);
4130
+ const isPromise = typeof result?.then === 'function';
4131
+ const resolved = isPromise ? true : result; // optimistic until async handled via validateAsync
4132
+ const toHide = validator.hide?.(this.internalDataSignal[0][key]);
4133
+ if (toHide) {
4134
+ this.setError(key, validator.errKey, false);
4135
+ } else {
4136
+ this.setError(key, validator.errKey, !resolved);
4137
+ if (!resolved) allValid = false;
4138
+ }
4139
+ } catch (e) {
4140
+ if (process?.env?.NODE_ENV !== 'production') console.warn('Validator threw', validator.errKey, e);
4141
+ this.setError(key, validator.errKey, true);
4142
+ allValid = false;
3970
4143
  }
3971
- this.setError(key, validator.errKey, hasError);
3972
- return !hasError;
3973
- }).includes(false);
4144
+ }
4145
+ return allValid;
4146
+ }
4147
+ async validateAsync(key) {
4148
+ if (!key) {
4149
+ const results = await Promise.all(this.keys.map(k => this.validateAsync(k)));
4150
+ return results.every(Boolean);
4151
+ }
4152
+ if (this.internalDataSignal?.[0]?.[key] instanceof FormArray) {
4153
+ return this.internalDataSignal[0][key].validateCurrent();
4154
+ }
4155
+ const validators = this.validators[key];
4156
+ if (!validators) return true;
4157
+ let allValid = true;
4158
+ for (const validator of validators) {
4159
+ try {
4160
+ const res = await validator.revalidate(this.internalDataSignal[0][key]);
4161
+ const toHide = validator.hide?.(this.internalDataSignal[0][key]);
4162
+ if (toHide) this.setError(key, validator.errKey, false);else this.setError(key, validator.errKey, !res);
4163
+ if (!res && !toHide) allValid = false;
4164
+ } catch {
4165
+ this.setError(key, validator.errKey, true);
4166
+ allValid = false;
4167
+ }
4168
+ }
4169
+ return allValid;
3974
4170
  }
3975
4171
  }
3976
4172
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coles-solid-library",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "A SolidJS mostly UI library",
5
5
  "module": "dist/index.esm.js",
6
6
  "types": "dist/index.d.ts",