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.
- package/dist/components/Form/formGroup.d.ts +14 -1
- package/dist/components/Form/formHelp/models.d.ts +8 -11
- package/dist/components/Form/formHelp/validators.d.ts +4 -0
- package/dist/components/Form/useFormFieldBinding.d.ts +14 -0
- package/dist/components/TextArea/TextArea.d.ts +4 -2
- package/dist/index.esm.js +320 -124
- package/package.json +1 -1
|
@@ -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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1514
|
-
if (
|
|
1515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1595
|
-
|
|
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
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
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
|
-
//
|
|
2412
|
+
// Set field type & initial floating state
|
|
2361
2413
|
onMount(() => {
|
|
2362
|
-
|
|
2363
|
-
|
|
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
|
-
//
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
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
|
-
|
|
2372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2468
|
+
resizeToContent();
|
|
2382
2469
|
}, _el$);
|
|
2383
2470
|
spread(_el$, mergeProps(normalProps, {
|
|
2384
|
-
|
|
2385
|
-
|
|
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 ??
|
|
2475
|
+
return `${style$4.areaStyle} ${customProps.class ?? ''} ${customProps.transparent ? customProps.transparent : ''}`;
|
|
2399
2476
|
},
|
|
2400
|
-
get
|
|
2401
|
-
return
|
|
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
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
}
|
|
3462
|
-
this.errors =
|
|
3463
|
-
const
|
|
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(
|
|
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
|
-
|
|
3474
|
-
|
|
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
|
|
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(
|
|
3565
|
+
hasError: !validator.revalidate(currentVal)
|
|
3483
3566
|
}));
|
|
3484
3567
|
});
|
|
3485
|
-
|
|
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
|
-
|
|
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(
|
|
3535
|
-
// test just require
|
|
3622
|
+
let errKeys = allErrors.map(e => e.key);
|
|
3536
3623
|
if (errKeys.includes('required')) {
|
|
3537
|
-
errKeys = errKeys.filter(
|
|
3624
|
+
errKeys = errKeys.filter(k => k !== 'minLength' && k !== 'maxLength');
|
|
3538
3625
|
}
|
|
3539
|
-
|
|
3540
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
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(
|
|
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
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
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
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
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
|
|