coles-solid-library 0.3.6 → 0.3.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.
@@ -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,6 +11,7 @@ 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>);
16
17
  /**
@@ -70,6 +71,12 @@ export declare class FormGroup<T extends object> {
70
71
  * @param hasError - Whether the error should be set or cleared
71
72
  */
72
73
  setError(key: keyof T, errKey: string, hasError: boolean): void;
74
+ addValidator<K extends keyof T>(key: K, validator: ValidatorResult<T[K]>): void;
75
+ removeValidator<K extends keyof T>(key: K, errKey: string): void;
76
+ getMeta<K extends keyof T>(key: K): ControlMeta<T[K]>;
77
+ markTouched<K extends keyof T>(key: K): void;
78
+ markDirty<K extends keyof T>(key: K): void;
79
+ reset(): void;
73
80
  /**
74
81
  * Sets a new value for a specified form control.
75
82
  *
@@ -101,6 +108,7 @@ export declare class FormGroup<T extends object> {
101
108
  * @returns `true` if the control(s) pass all validations; otherwise, `false`.
102
109
  */
103
110
  validate<K extends keyof T>(key?: K): boolean;
111
+ validateAsync<K extends keyof T>(key?: K): Promise<boolean>;
104
112
  }
105
113
  /**
106
114
  * 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>;
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
  },
@@ -1498,21 +1501,21 @@ const Input = props => {
1498
1501
  }
1499
1502
  if (props.onChange) props.onChange(e);
1500
1503
  };
1501
- createEffect(() => {
1502
- if (isRequired()) {
1503
- context?.setName?.(old => `${old} *`);
1504
- } else {
1505
- context?.setName?.(old => old);
1506
- }
1507
- });
1504
+ // Removed name mutation adding '*' to avoid duplicating required indicator; legend handles display.
1508
1505
  onMount(() => {
1509
1506
  if (!isNullish(context.getName)) {
1510
1507
  // Force a non-checkbox field type
1511
1508
  context.setFieldType(props.type === "checkbox" ? "text" : props.type ?? "text");
1512
1509
  if (!isNullish(formContext?.data)) {
1513
- const formValue = formContext.data[context.formName ?? ""]?.trim();
1514
- if (formValue) {
1515
- context.setValue(formValue);
1510
+ const raw = formContext.data[context.formName ?? ""];
1511
+ if (typeof raw === 'string') {
1512
+ const formValue = raw.trim();
1513
+ if (formValue) {
1514
+ context.setValue(formValue);
1515
+ context.setTextInside(true);
1516
+ }
1517
+ } else if (raw !== undefined && raw !== null) {
1518
+ context.setValue(raw);
1516
1519
  context.setTextInside(true);
1517
1520
  }
1518
1521
  }
@@ -2122,7 +2125,6 @@ function Select(props) {
2122
2125
  selectStyle: currStyle
2123
2126
  }));
2124
2127
  const [dropTop, setDropTop] = createSignal(false);
2125
- console.log("Select rendered with options:", options);
2126
2128
  return (() => {
2127
2129
  var _el$7 = _tmpl$5$1(),
2128
2130
  _el$8 = _el$7.firstChild,
@@ -2299,7 +2301,6 @@ function Option(props) {
2299
2301
  const contextSuccess = selectFormContextValue(props.value);
2300
2302
  !contextSuccess ? selectFormFieldValue(props.value) : true;
2301
2303
  if (!formField?.getName?.()) {
2302
- console.log('selected!', props.value, select);
2303
2304
  select.selectValue?.(props.value);
2304
2305
  formField?.setFocused?.(true);
2305
2306
  }
@@ -2406,7 +2407,7 @@ const TextArea = props => {
2406
2407
  if (!!context?.getName && !!e.currentTarget.value.trim()) {
2407
2408
  context?.setValue(e.currentTarget.value);
2408
2409
  context?.setTextInside(true);
2409
- } else if (!!context.getName && !e.currentTarget.value.trim()) {
2410
+ } else if (!!context?.getName && !e.currentTarget.value.trim()) {
2410
2411
  context?.setValue("");
2411
2412
  context?.setTextInside(false);
2412
2413
  }
@@ -3000,11 +3001,21 @@ function RadioGroup(props) {
3000
3001
  radioGroupCount++;
3001
3002
  const groupName = props.name ?? `radio-group-${radioGroupCount}`;
3002
3003
  const [internalValue, setInternalValue] = createSignal(props.defaultValue);
3004
+ const formField = useFormProvider();
3005
+ const formContext = useFormContext();
3003
3006
  const selectedValue = () => props.value !== undefined ? props.value : internalValue();
3004
3007
  const setSelectedValue = val => {
3005
3008
  if (props.value === undefined) {
3006
3009
  setInternalValue(val);
3007
3010
  }
3011
+ // Bridge to FormGroup if inside a FormField with formName
3012
+ if (formField?.formName && formContext?.formGroup?.set) {
3013
+ formContext.formGroup.set(formField.formName, val);
3014
+ formContext.setData(old => ({
3015
+ ...old,
3016
+ [formField.formName]: val
3017
+ }));
3018
+ }
3008
3019
  props.onChange?.(val);
3009
3020
  };
3010
3021
  const radioRefs = [];
@@ -3450,39 +3461,47 @@ class FormArray {
3450
3461
  }
3451
3462
  // Validate all controls in the form array.
3452
3463
  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]) => {
3464
+ // Always evaluate array-level validators even if the array is empty
3465
+ const arrayLevelErrors = this.internalArrayValidation.map(validator => ({
3466
+ key: validator.errKey,
3467
+ hasError: !validator.revalidate(values)
3468
+ }));
3469
+ if (values.length === 0) {
3470
+ this.errors = [arrayLevelErrors]; // store array-level errors at index 0 placeholder
3471
+ return this.errors.flat().every(error => !error.hasError);
3472
+ }
3473
+ this.errors = values.map((value, i) => {
3474
+ const controlErrors = this.internalValidation.map(([propKey, validators]) => {
3475
+ const currentVal = value[propKey];
3464
3476
  return validators.map(validator => ({
3465
3477
  key: validator.errKey,
3466
- hasError: !validator.revalidate(value)
3478
+ hasError: !validator.revalidate(currentVal)
3467
3479
  }));
3468
3480
  });
3469
3481
  const arrayErrors = this.internalArrayValidation.map(validator => ({
3470
3482
  key: validator.errKey,
3471
3483
  hasError: !validator.revalidate(values)
3472
3484
  }));
3473
- this.errors[i] = [...errors.flat(), ...arrayErrors];
3474
- return [...errors.flat(), ...arrayErrors];
3485
+ const merged = [...controlErrors.flat(), ...arrayErrors];
3486
+ this.errors[i] = merged;
3487
+ return merged;
3475
3488
  });
3476
3489
  return this.errors.flat().every(error => !error.hasError);
3477
3490
  }
3478
3491
  // Validate a specific control in the form array.
3479
- const errors = this.internalValidation.map(([_, validators]) => {
3492
+ const value = values[index];
3493
+ const controlErrors = this.internalValidation.map(([propKey, validators]) => {
3494
+ const currentVal = value[propKey];
3480
3495
  return validators.map(validator => ({
3481
3496
  key: validator.errKey,
3482
- hasError: !validator.revalidate(values[index])
3497
+ hasError: !validator.revalidate(currentVal)
3483
3498
  }));
3484
3499
  });
3485
- this.errors[index] = errors.flat();
3500
+ const arrayErrors = this.internalArrayValidation.map(validator => ({
3501
+ key: validator.errKey,
3502
+ hasError: !validator.revalidate(values)
3503
+ }));
3504
+ this.errors[index] = [...controlErrors.flat(), ...arrayErrors];
3486
3505
  return this.errors[index].every(error => !error.hasError);
3487
3506
  }
3488
3507
  }
@@ -3529,15 +3548,16 @@ const FormField2 = props => {
3529
3548
  });
3530
3549
  const theChildren = children(() => props.children);
3531
3550
  const formErrors = () => {
3532
- const allErrors = (formContext?.formGroup.getErrors(local?.formName ?? '') ?? []).filter(error => error.hasError);
3551
+ if (!local?.formName) return [];
3552
+ const allErrors = (formContext?.formGroup.getErrors(local.formName) ?? []).filter(e => e.hasError);
3533
3553
  if (allErrors.length === 0) return [];
3534
- let errKeys = allErrors.map(error => error.key);
3535
- // test just require
3554
+ let errKeys = allErrors.map(e => e.key);
3536
3555
  if (errKeys.includes('required')) {
3537
- errKeys = errKeys.filter(err => err !== 'minLength' && err !== 'maxLength');
3556
+ errKeys = errKeys.filter(k => k !== 'minLength' && k !== 'maxLength');
3538
3557
  }
3539
- //---
3540
- return context?.getErrors?.().err.filter(err => errKeys.includes(err.key));
3558
+ // Map to displays stored in context errors (ColeError registered displays) if available
3559
+ const displayMap = context?.getErrors?.().err ?? [];
3560
+ return displayMap.filter(e => errKeys.includes(e.key));
3541
3561
  };
3542
3562
  const hasRequired = createMemo(() => {
3543
3563
  if (isNullish(local?.formName)) return false;
@@ -3746,7 +3766,13 @@ class Validators {
3746
3766
  * @returns A ValidatorResult that includes the error key and a revalidation function.
3747
3767
  */
3748
3768
  static custom(errKey, validator, hide) {
3749
- return this.createValidatorResult(errKey, validator);
3769
+ return this.createValidatorResult(errKey, validator, hide);
3770
+ }
3771
+ /**
3772
+ * Creates an async validator; resolves promise to boolean.
3773
+ */
3774
+ static asyncCustom(errKey, validator, hide) {
3775
+ return this.createValidatorResult(errKey, validator, hide);
3750
3776
  }
3751
3777
  /**
3752
3778
  * A helper function that creates a ValidatorResult object.
@@ -3777,6 +3803,7 @@ class FormGroup {
3777
3803
  internalDataSignal;
3778
3804
  validators = {};
3779
3805
  errors;
3806
+ meta = {};
3780
3807
  keys = [];
3781
3808
  constructor(data) {
3782
3809
  this.data = data;
@@ -3787,11 +3814,8 @@ class FormGroup {
3787
3814
  const value = data[key];
3788
3815
  this.keys.push(key);
3789
3816
  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();
3817
+ newData[key] = value;
3793
3818
  } else {
3794
- // Otherwise, initialize the data, validators, and errors for the control.
3795
3819
  newData[key] = value[0];
3796
3820
  newValidators[key] = value[1];
3797
3821
  newErrors[key] = value[1].map(validator => ({
@@ -3799,13 +3823,31 @@ class FormGroup {
3799
3823
  hasError: false
3800
3824
  }));
3801
3825
  }
3826
+ const initialVal = value instanceof FormArray ? value.get() : CloneStore(newData[key]);
3827
+ this.meta[key] = {
3828
+ touched: false,
3829
+ dirty: false,
3830
+ initialValue: initialVal
3831
+ };
3802
3832
  }
3803
3833
  this.internalDataSignal = createStore(newData);
3804
3834
  this.validators = newValidators;
3805
3835
  this.errors = createSignal(newErrors);
3806
3836
  }
3807
3837
  get(key) {
3808
- if (!key) return CloneStore(this.internalDataSignal[0]);
3838
+ if (!key) {
3839
+ // Custom clone that preserves FormArray instances (structuredClone fails on functions inside)
3840
+ const clone = {};
3841
+ for (const k of this.keys) {
3842
+ const val = this.internalDataSignal[0][k];
3843
+ if (val instanceof FormArray) {
3844
+ clone[k] = val; // keep reference; consumer methods operate on instance
3845
+ } else {
3846
+ clone[k] = CloneStore(val);
3847
+ }
3848
+ }
3849
+ return clone;
3850
+ }
3809
3851
  // If the control is a FormArray, use its get method
3810
3852
  if (this.internalDataSignal[0][key] instanceof FormArray) {
3811
3853
  // Return the array from FormArray.get() as the expected type T[K]
@@ -3885,16 +3927,56 @@ class FormGroup {
3885
3927
  * @param hasError - Whether the error should be set or cleared
3886
3928
  */
3887
3929
  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);
3930
+ if (this.internalDataSignal[0][key] instanceof FormArray) return;
3931
+ const current = this.errors[0]();
3932
+ const list = current[key] ?? [];
3933
+ let index = list.findIndex(e => e.key === errKey);
3934
+ if (index === -1) {
3935
+ list.push({
3936
+ key: errKey,
3937
+ hasError
3897
3938
  });
3939
+ } else {
3940
+ list[index].hasError = hasError;
3941
+ }
3942
+ current[key] = list;
3943
+ this.errors[1](() => Clone(current));
3944
+ }
3945
+ addValidator(key, validator) {
3946
+ if (!this.validators[key]) this.validators[key] = [];
3947
+ this.validators[key].push(validator);
3948
+ this.setError(key, validator.errKey, false);
3949
+ }
3950
+ removeValidator(key, errKey) {
3951
+ if (!this.validators[key]) return;
3952
+ this.validators[key] = this.validators[key].filter(v => v.errKey !== errKey);
3953
+ const current = this.errors[0]();
3954
+ if (current[key]) {
3955
+ current[key] = current[key].filter(e => e.key !== errKey);
3956
+ this.errors[1](() => Clone(current));
3957
+ }
3958
+ }
3959
+ getMeta(key) {
3960
+ return this.meta[key];
3961
+ }
3962
+ markTouched(key) {
3963
+ this.meta[key].touched = true;
3964
+ }
3965
+ markDirty(key) {
3966
+ this.meta[key].dirty = true;
3967
+ }
3968
+ reset() {
3969
+ for (const k of this.keys) {
3970
+ const m = this.meta[k];
3971
+ this.set(k, CloneStore(m.initialValue));
3972
+ m.touched = false;
3973
+ m.dirty = false;
3974
+ const errs = this.errors[0]();
3975
+ errs[k] = errs[k]?.map(e => ({
3976
+ ...e,
3977
+ hasError: false
3978
+ }));
3979
+ this.errors[1](() => Clone(errs));
3898
3980
  }
3899
3981
  }
3900
3982
  /**
@@ -3915,6 +3997,11 @@ class FormGroup {
3915
3997
  ...old,
3916
3998
  [key]: value
3917
3999
  }));
4000
+ const m = this.meta[key];
4001
+ if (m) {
4002
+ if (!m.touched) m.touched = true;
4003
+ if (!m.dirty && JSON.stringify(m.initialValue) !== JSON.stringify(value)) m.dirty = true;
4004
+ }
3918
4005
  }
3919
4006
  /**
3920
4007
  * Adds an item to a FormArray control
@@ -3949,28 +4036,62 @@ class FormGroup {
3949
4036
  */
3950
4037
  validate(key) {
3951
4038
  if (isNullish(this.internalDataSignal?.[0])) {
3952
- console.error('Data is null');
4039
+ if (process?.env?.NODE_ENV !== 'production') console.error('Data is null');
3953
4040
  return false;
3954
4041
  }
3955
4042
  if (!key) {
3956
4043
  const results = this.keys.map(k => this.validate(k));
3957
- return results.every(result => result);
4044
+ return results.every(Boolean);
3958
4045
  }
3959
4046
  if (this.internalDataSignal?.[0]?.[key] instanceof FormArray) {
3960
4047
  return this.internalDataSignal[0][key].validateCurrent();
3961
4048
  }
3962
4049
  const validators = this.validators[key];
3963
4050
  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;
4051
+ let allValid = true;
4052
+ for (const validator of validators) {
4053
+ try {
4054
+ const result = validator.revalidate(this.internalDataSignal[0][key]);
4055
+ const isPromise = typeof result?.then === 'function';
4056
+ const resolved = isPromise ? true : result; // optimistic until async handled via validateAsync
4057
+ const toHide = validator.hide?.(this.internalDataSignal[0][key]);
4058
+ if (toHide) {
4059
+ this.setError(key, validator.errKey, false);
4060
+ } else {
4061
+ this.setError(key, validator.errKey, !resolved);
4062
+ if (!resolved) allValid = false;
4063
+ }
4064
+ } catch (e) {
4065
+ if (process?.env?.NODE_ENV !== 'production') console.warn('Validator threw', validator.errKey, e);
4066
+ this.setError(key, validator.errKey, true);
4067
+ allValid = false;
3970
4068
  }
3971
- this.setError(key, validator.errKey, hasError);
3972
- return !hasError;
3973
- }).includes(false);
4069
+ }
4070
+ return allValid;
4071
+ }
4072
+ async validateAsync(key) {
4073
+ if (!key) {
4074
+ const results = await Promise.all(this.keys.map(k => this.validateAsync(k)));
4075
+ return results.every(Boolean);
4076
+ }
4077
+ if (this.internalDataSignal?.[0]?.[key] instanceof FormArray) {
4078
+ return this.internalDataSignal[0][key].validateCurrent();
4079
+ }
4080
+ const validators = this.validators[key];
4081
+ if (!validators) return true;
4082
+ let allValid = true;
4083
+ for (const validator of validators) {
4084
+ try {
4085
+ const res = await validator.revalidate(this.internalDataSignal[0][key]);
4086
+ const toHide = validator.hide?.(this.internalDataSignal[0][key]);
4087
+ if (toHide) this.setError(key, validator.errKey, false);else this.setError(key, validator.errKey, !res);
4088
+ if (!res && !toHide) allValid = false;
4089
+ } catch {
4090
+ this.setError(key, validator.errKey, true);
4091
+ allValid = false;
4092
+ }
4093
+ }
4094
+ return allValid;
3974
4095
  }
3975
4096
  }
3976
4097
 
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.7",
4
4
  "description": "A SolidJS mostly UI library",
5
5
  "module": "dist/index.esm.js",
6
6
  "types": "dist/index.d.ts",