@timeax/form-palette 0.0.1
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/.scaffold-cache.json +537 -0
- package/package.json +42 -0
- package/src/.scaffold-cache.json +544 -0
- package/src/adapters/axios.ts +117 -0
- package/src/adapters/index.ts +91 -0
- package/src/adapters/inertia.ts +187 -0
- package/src/core/adapter-registry.ts +87 -0
- package/src/core/bound/bind-host.ts +14 -0
- package/src/core/bound/observe-bound-field.ts +172 -0
- package/src/core/bound/wait-for-bound-field.ts +57 -0
- package/src/core/context.ts +23 -0
- package/src/core/core-provider.tsx +818 -0
- package/src/core/core-root.tsx +72 -0
- package/src/core/core-shell.tsx +44 -0
- package/src/core/errors/error-strip.tsx +71 -0
- package/src/core/errors/index.ts +2 -0
- package/src/core/errors/map-error-bag.ts +51 -0
- package/src/core/errors/map-zod.ts +39 -0
- package/src/core/hooks/use-button.ts +220 -0
- package/src/core/hooks/use-core-context.ts +20 -0
- package/src/core/hooks/use-core-utility.ts +0 -0
- package/src/core/hooks/use-core.ts +13 -0
- package/src/core/hooks/use-field.ts +497 -0
- package/src/core/hooks/use-optional-field.ts +28 -0
- package/src/core/index.ts +0 -0
- package/src/core/registry/binder-registry.ts +82 -0
- package/src/core/registry/field-registry.ts +187 -0
- package/src/core/test.tsx +17 -0
- package/src/global.d.ts +14 -0
- package/src/index.ts +68 -0
- package/src/input/index.ts +4 -0
- package/src/input/input-field.tsx +854 -0
- package/src/input/input-layout-graph.ts +230 -0
- package/src/input/input-props.ts +190 -0
- package/src/lib/get-global-countries.ts +87 -0
- package/src/lib/utils.ts +6 -0
- package/src/presets/index.ts +0 -0
- package/src/presets/shadcn-preset.ts +0 -0
- package/src/presets/shadcn-variants/checkbox.tsx +849 -0
- package/src/presets/shadcn-variants/chips.tsx +756 -0
- package/src/presets/shadcn-variants/color.tsx +284 -0
- package/src/presets/shadcn-variants/custom.tsx +227 -0
- package/src/presets/shadcn-variants/date.tsx +796 -0
- package/src/presets/shadcn-variants/file.tsx +764 -0
- package/src/presets/shadcn-variants/keyvalue.tsx +556 -0
- package/src/presets/shadcn-variants/multiselect.tsx +1132 -0
- package/src/presets/shadcn-variants/number.tsx +176 -0
- package/src/presets/shadcn-variants/password.tsx +737 -0
- package/src/presets/shadcn-variants/phone.tsx +628 -0
- package/src/presets/shadcn-variants/radio.tsx +578 -0
- package/src/presets/shadcn-variants/select.tsx +956 -0
- package/src/presets/shadcn-variants/slider.tsx +622 -0
- package/src/presets/shadcn-variants/text.tsx +343 -0
- package/src/presets/shadcn-variants/textarea.tsx +66 -0
- package/src/presets/shadcn-variants/toggle.tsx +218 -0
- package/src/presets/shadcn-variants/treeselect.tsx +784 -0
- package/src/presets/ui/badge.tsx +46 -0
- package/src/presets/ui/button.tsx +60 -0
- package/src/presets/ui/calendar.tsx +214 -0
- package/src/presets/ui/checkbox.tsx +115 -0
- package/src/presets/ui/custom.tsx +0 -0
- package/src/presets/ui/dialog.tsx +141 -0
- package/src/presets/ui/field.tsx +246 -0
- package/src/presets/ui/input-mask.tsx +739 -0
- package/src/presets/ui/input-otp.tsx +77 -0
- package/src/presets/ui/input.tsx +1011 -0
- package/src/presets/ui/label.tsx +22 -0
- package/src/presets/ui/number.tsx +1370 -0
- package/src/presets/ui/popover.tsx +46 -0
- package/src/presets/ui/radio-group.tsx +43 -0
- package/src/presets/ui/scroll-area.tsx +56 -0
- package/src/presets/ui/select.tsx +190 -0
- package/src/presets/ui/separator.tsx +28 -0
- package/src/presets/ui/slider.tsx +61 -0
- package/src/presets/ui/switch.tsx +32 -0
- package/src/presets/ui/textarea.tsx +634 -0
- package/src/presets/ui/time-dropdowns.tsx +350 -0
- package/src/schema/adapter.ts +217 -0
- package/src/schema/core.ts +429 -0
- package/src/schema/field-map.ts +0 -0
- package/src/schema/field.ts +224 -0
- package/src/schema/index.ts +0 -0
- package/src/schema/input-field.ts +260 -0
- package/src/schema/presets.ts +0 -0
- package/src/schema/variant.ts +216 -0
- package/src/variants/core/checkbox.tsx +54 -0
- package/src/variants/core/chips.tsx +22 -0
- package/src/variants/core/color.tsx +16 -0
- package/src/variants/core/custom.tsx +18 -0
- package/src/variants/core/date.tsx +25 -0
- package/src/variants/core/file.tsx +9 -0
- package/src/variants/core/keyvalue.tsx +12 -0
- package/src/variants/core/multiselect.tsx +28 -0
- package/src/variants/core/number.tsx +115 -0
- package/src/variants/core/password.tsx +35 -0
- package/src/variants/core/phone.tsx +16 -0
- package/src/variants/core/radio.tsx +38 -0
- package/src/variants/core/select.tsx +15 -0
- package/src/variants/core/slider.tsx +55 -0
- package/src/variants/core/text.tsx +114 -0
- package/src/variants/core/textarea.tsx +22 -0
- package/src/variants/core/toggle.tsx +50 -0
- package/src/variants/core/treeselect.tsx +11 -0
- package/src/variants/helpers/selection-summary.tsx +236 -0
- package/src/variants/index.ts +75 -0
- package/src/variants/registry.ts +38 -0
- package/src/variants/select-shared.ts +0 -0
- package/src/variants/shared.ts +126 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// src/core/hooks/use-field.ts
|
|
2
|
+
// noinspection JSUnusedGlobalSymbols
|
|
3
|
+
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { useCoreContext } from "@/core/hooks/use-core-context";
|
|
7
|
+
import type { CoreContext, Dict } from "@/schema/core";
|
|
8
|
+
import type { Field } from "@/schema/field";
|
|
9
|
+
|
|
10
|
+
export type UseFieldValidate<T> = (
|
|
11
|
+
value: T,
|
|
12
|
+
report: boolean
|
|
13
|
+
) => boolean | string;
|
|
14
|
+
|
|
15
|
+
export interface UseFieldOptions<T = unknown> {
|
|
16
|
+
/**
|
|
17
|
+
* Primary field name.
|
|
18
|
+
*
|
|
19
|
+
* This is the key that will show up in the values snapshot and
|
|
20
|
+
* error bags (unless mapped via `shared` or `alias`).
|
|
21
|
+
*/
|
|
22
|
+
name?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Optional internal binding identifier.
|
|
26
|
+
*
|
|
27
|
+
* Used by the bound helpers (observeBoundField, waitForBoundField)
|
|
28
|
+
* and the binder registry.
|
|
29
|
+
*/
|
|
30
|
+
bindId?: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optional external binding key – a semantic identifier for this
|
|
34
|
+
* field’s binding group.
|
|
35
|
+
*
|
|
36
|
+
* Example:
|
|
37
|
+
* bind="shipping"
|
|
38
|
+
*/
|
|
39
|
+
bind?: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Shared key for nested grouping, e.g:
|
|
43
|
+
*
|
|
44
|
+
* shared="profile", name="first_name"
|
|
45
|
+
* → values.profile.first_name
|
|
46
|
+
*/
|
|
47
|
+
shared?: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Optional grouping identifier used to group related controls
|
|
51
|
+
* (e.g. radio groups, segmented inputs).
|
|
52
|
+
*/
|
|
53
|
+
groupId?: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Optional alias for error / mapping purposes.
|
|
57
|
+
*
|
|
58
|
+
* Example:
|
|
59
|
+
* alias="email" but name="contact.email"
|
|
60
|
+
*/
|
|
61
|
+
alias?: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Marks this field as the "main" one in a group.
|
|
65
|
+
*/
|
|
66
|
+
main?: boolean;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* If true, this field is ignored by snapshot / some validation
|
|
70
|
+
* flows, but may still exist in the registry.
|
|
71
|
+
*/
|
|
72
|
+
ignore?: boolean;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether the field is required.
|
|
76
|
+
*/
|
|
77
|
+
required?: boolean;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Initial/default value for this field.
|
|
81
|
+
*/
|
|
82
|
+
defaultValue?: T;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Initial disabled flag.
|
|
86
|
+
*/
|
|
87
|
+
disabled?: boolean;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Initial readOnly flag.
|
|
91
|
+
*/
|
|
92
|
+
readOnly?: boolean;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Custom validation hook.
|
|
96
|
+
*
|
|
97
|
+
* Return:
|
|
98
|
+
* - `true` → valid
|
|
99
|
+
* - `false` → invalid (no message)
|
|
100
|
+
* - `"message"` → invalid with explicit message
|
|
101
|
+
*/
|
|
102
|
+
validate?: UseFieldValidate<T>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Optional projector to derive an "original" value from the
|
|
106
|
+
* initial default.
|
|
107
|
+
*/
|
|
108
|
+
getOriginalValue?(value: T | undefined): unknown;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Local change hook for the field.
|
|
112
|
+
*
|
|
113
|
+
* This is in addition to the form-level `onChange`.
|
|
114
|
+
*/
|
|
115
|
+
onValueChange?(next: T, prev: T, variant: string): void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface UseFieldReturn<T = unknown> {
|
|
119
|
+
/** Ref to the underlying DOM element */
|
|
120
|
+
ref: React.RefObject<HTMLElement>;
|
|
121
|
+
key: string;
|
|
122
|
+
/** Current value */
|
|
123
|
+
value: T | undefined;
|
|
124
|
+
setValue(next: T | undefined, variant?: string): void;
|
|
125
|
+
|
|
126
|
+
/** Current error message */
|
|
127
|
+
error: string;
|
|
128
|
+
setError(message: string): void;
|
|
129
|
+
|
|
130
|
+
/** Async-loading flag (e.g. remote validation) */
|
|
131
|
+
loading: boolean;
|
|
132
|
+
setLoading(loading: boolean): void;
|
|
133
|
+
|
|
134
|
+
/** Required flag */
|
|
135
|
+
required: boolean;
|
|
136
|
+
setRequired(required: boolean): void;
|
|
137
|
+
|
|
138
|
+
/** Disabled flag */
|
|
139
|
+
disabled: boolean;
|
|
140
|
+
setDisabled(disabled: boolean): void;
|
|
141
|
+
|
|
142
|
+
/** Readonly flag */
|
|
143
|
+
readOnly: boolean;
|
|
144
|
+
setReadOnly(readOnly: boolean): void;
|
|
145
|
+
|
|
146
|
+
/** Metadata / wiring */
|
|
147
|
+
name: string;
|
|
148
|
+
bindId: string;
|
|
149
|
+
bind?: string;
|
|
150
|
+
shared?: string;
|
|
151
|
+
groupId?: string;
|
|
152
|
+
alias?: string;
|
|
153
|
+
main?: boolean;
|
|
154
|
+
ignore?: boolean;
|
|
155
|
+
|
|
156
|
+
/** Snapshots */
|
|
157
|
+
readonly defaultValue: T | undefined;
|
|
158
|
+
readonly originalValue: unknown;
|
|
159
|
+
|
|
160
|
+
/** Owning core context */
|
|
161
|
+
form: CoreContext<Dict>;
|
|
162
|
+
|
|
163
|
+
/** Run validation (optionally reporting errors) */
|
|
164
|
+
validate(report?: boolean): boolean | undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Strict field hook.
|
|
169
|
+
*
|
|
170
|
+
* - Registers the field with the core provider / registry.
|
|
171
|
+
* - Exposes value/error/loading and lifecycle helpers.
|
|
172
|
+
* - Wires into:
|
|
173
|
+
* - core-level `onChange`
|
|
174
|
+
* - `controlButton()` dirty logic
|
|
175
|
+
*/
|
|
176
|
+
export function useField<T = unknown>(
|
|
177
|
+
options: UseFieldOptions<T>
|
|
178
|
+
): UseFieldReturn<T> {
|
|
179
|
+
const form = useCoreContext<Dict>();
|
|
180
|
+
|
|
181
|
+
const {
|
|
182
|
+
name: rawName,
|
|
183
|
+
bindId: rawBindId,
|
|
184
|
+
bind,
|
|
185
|
+
shared,
|
|
186
|
+
groupId,
|
|
187
|
+
alias,
|
|
188
|
+
main,
|
|
189
|
+
ignore,
|
|
190
|
+
required: requiredProp = false,
|
|
191
|
+
defaultValue,
|
|
192
|
+
disabled: disabledProp = false,
|
|
193
|
+
readOnly: readOnlyProp = false,
|
|
194
|
+
validate,
|
|
195
|
+
getOriginalValue,
|
|
196
|
+
onValueChange,
|
|
197
|
+
} = options;
|
|
198
|
+
|
|
199
|
+
const ref = React.useRef<HTMLElement>(null);
|
|
200
|
+
|
|
201
|
+
// Core state (value, error, loading, original) lives in a ref
|
|
202
|
+
const stateRef = React.useRef<{
|
|
203
|
+
value: T | undefined;
|
|
204
|
+
error: string;
|
|
205
|
+
loading: boolean;
|
|
206
|
+
original: unknown;
|
|
207
|
+
}>({
|
|
208
|
+
value: defaultValue,
|
|
209
|
+
error: "",
|
|
210
|
+
loading: false,
|
|
211
|
+
original: getOriginalValue
|
|
212
|
+
? getOriginalValue(defaultValue)
|
|
213
|
+
: defaultValue,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// React state mirrors (used for rerenders)
|
|
217
|
+
const [value, setValueState] = React.useState<T | undefined>(
|
|
218
|
+
stateRef.current.value
|
|
219
|
+
);
|
|
220
|
+
const [error, setErrorState] = React.useState<string>(
|
|
221
|
+
stateRef.current.error
|
|
222
|
+
);
|
|
223
|
+
const [loading, setLoadingState] = React.useState<boolean>(
|
|
224
|
+
stateRef.current.loading
|
|
225
|
+
);
|
|
226
|
+
const [required, setRequired] = React.useState<boolean>(
|
|
227
|
+
Boolean(requiredProp)
|
|
228
|
+
);
|
|
229
|
+
const [disabled, setDisabled] = React.useState<boolean>(
|
|
230
|
+
Boolean(disabledProp)
|
|
231
|
+
);
|
|
232
|
+
const [readOnly, setReadOnly] = React.useState<boolean>(
|
|
233
|
+
Boolean(readOnlyProp)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const id = React.useId();
|
|
237
|
+
// Stable wiring keys
|
|
238
|
+
// @ts-ignore
|
|
239
|
+
const keyRef = React.useRef<string>((() => {
|
|
240
|
+
if (rawName && rawName.trim()) return `${rawName.trim()}-${id}`;
|
|
241
|
+
if (rawBindId && rawBindId.trim()) return `${rawBindId.trim()}-${id}`;
|
|
242
|
+
return `field-${Math.random().toString(36).slice(2)}-${id}`;
|
|
243
|
+
})()) as React.MutableRefObject<string>;
|
|
244
|
+
|
|
245
|
+
const bindIdRef = React.useRef<string>(
|
|
246
|
+
(rawBindId && rawBindId.trim()) || keyRef.current
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const fieldRef = React.useRef<Field | null>(null);
|
|
250
|
+
|
|
251
|
+
// Build the Field object once
|
|
252
|
+
if (!fieldRef.current) {
|
|
253
|
+
const key = keyRef.current;
|
|
254
|
+
const bindId = bindIdRef.current;
|
|
255
|
+
const trimmedName = rawName?.trim() ?? "";
|
|
256
|
+
|
|
257
|
+
const validateFn = (report?: boolean): boolean => {
|
|
258
|
+
const formDisabled = false; // core-level disable could be added later
|
|
259
|
+
const curDisabled = formDisabled || disabled || readOnly;
|
|
260
|
+
|
|
261
|
+
if (curDisabled && !report) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const current = stateRef.current.value as T;
|
|
266
|
+
let ok = true;
|
|
267
|
+
let message = "";
|
|
268
|
+
|
|
269
|
+
if (
|
|
270
|
+
required &&
|
|
271
|
+
(current === undefined ||
|
|
272
|
+
current === null ||
|
|
273
|
+
(typeof current === "string" && current.trim() === "") ||
|
|
274
|
+
(Array.isArray(current) && current.length === 0))
|
|
275
|
+
) {
|
|
276
|
+
ok = false;
|
|
277
|
+
message = "This field is required.";
|
|
278
|
+
} else if (validate) {
|
|
279
|
+
const result = validate(current, !!report);
|
|
280
|
+
if (typeof result === "string") {
|
|
281
|
+
ok = false;
|
|
282
|
+
message = result;
|
|
283
|
+
} else if (result === false) {
|
|
284
|
+
ok = false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!report) {
|
|
289
|
+
return ok;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Report mode → set/clear error
|
|
293
|
+
stateRef.current.error = ok ? "" : message;
|
|
294
|
+
setErrorState(ok ? "" : message);
|
|
295
|
+
return ok;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const f: Field = {
|
|
299
|
+
key,
|
|
300
|
+
bindId,
|
|
301
|
+
bind,
|
|
302
|
+
name: trimmedName,
|
|
303
|
+
shared,
|
|
304
|
+
groupId,
|
|
305
|
+
alias,
|
|
306
|
+
main,
|
|
307
|
+
ignore,
|
|
308
|
+
required,
|
|
309
|
+
ref: ref as React.RefObject<HTMLElement>,
|
|
310
|
+
get defaultValue() {
|
|
311
|
+
return stateRef.current.original;
|
|
312
|
+
},
|
|
313
|
+
get value() {
|
|
314
|
+
return stateRef.current.value;
|
|
315
|
+
},
|
|
316
|
+
set value(v: unknown) {
|
|
317
|
+
stateRef.current.value = v as T | undefined;
|
|
318
|
+
setValueState(v as T | undefined);
|
|
319
|
+
},
|
|
320
|
+
get originalValue() {
|
|
321
|
+
return stateRef.current.original;
|
|
322
|
+
},
|
|
323
|
+
get error() {
|
|
324
|
+
return stateRef.current.error;
|
|
325
|
+
},
|
|
326
|
+
set error(msg: string) {
|
|
327
|
+
stateRef.current.error = msg;
|
|
328
|
+
setErrorState(msg);
|
|
329
|
+
},
|
|
330
|
+
get loading() {
|
|
331
|
+
return stateRef.current.loading;
|
|
332
|
+
},
|
|
333
|
+
set loading(v: boolean) {
|
|
334
|
+
stateRef.current.loading = v;
|
|
335
|
+
setLoadingState(v);
|
|
336
|
+
},
|
|
337
|
+
validate: validateFn,
|
|
338
|
+
onChange(value: unknown, old: unknown, variant: string) {
|
|
339
|
+
if (onValueChange) {
|
|
340
|
+
onValueChange(value as T, old as T, variant);
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
// Flags not directly on the Field interface but used via `as any`
|
|
344
|
+
// in core-provider (getValue/setValue/reset).
|
|
345
|
+
} as Field & {
|
|
346
|
+
getValue(): T | undefined;
|
|
347
|
+
setValue(next: T | undefined): void;
|
|
348
|
+
reset(): void;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Imperative helpers used by the core
|
|
352
|
+
(f as any).getValue = () => stateRef.current.value;
|
|
353
|
+
(f as any).setValue = (next: T | undefined) => {
|
|
354
|
+
stateRef.current.value = next;
|
|
355
|
+
setValueState(next);
|
|
356
|
+
};
|
|
357
|
+
(f as any).reset = () => {
|
|
358
|
+
stateRef.current.value = defaultValue;
|
|
359
|
+
stateRef.current.error = "";
|
|
360
|
+
stateRef.current.loading = false;
|
|
361
|
+
|
|
362
|
+
setValueState(defaultValue);
|
|
363
|
+
setErrorState("");
|
|
364
|
+
setLoadingState(false);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
fieldRef.current = f;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const field = fieldRef.current;
|
|
371
|
+
|
|
372
|
+
// Sync prop-driven flags when they change
|
|
373
|
+
React.useEffect(() => {
|
|
374
|
+
setRequired(!!requiredProp);
|
|
375
|
+
if (field) {
|
|
376
|
+
field.required = !!requiredProp;
|
|
377
|
+
}
|
|
378
|
+
}, [requiredProp, field]);
|
|
379
|
+
|
|
380
|
+
React.useEffect(() => {
|
|
381
|
+
setDisabled(!!disabledProp);
|
|
382
|
+
}, [disabledProp]);
|
|
383
|
+
|
|
384
|
+
React.useEffect(() => {
|
|
385
|
+
setReadOnly(!!readOnlyProp);
|
|
386
|
+
}, [readOnlyProp]);
|
|
387
|
+
|
|
388
|
+
// Register field with the core
|
|
389
|
+
React.useEffect(() => {
|
|
390
|
+
if (!field) return;
|
|
391
|
+
|
|
392
|
+
form.addField(field);
|
|
393
|
+
|
|
394
|
+
return () => {
|
|
395
|
+
// Remove from registry directly
|
|
396
|
+
const registry = form.inputs as any;
|
|
397
|
+
if (registry && typeof registry.remove === "function") {
|
|
398
|
+
registry.remove(field.key);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
402
|
+
}, [form, field]);
|
|
403
|
+
|
|
404
|
+
// Value setter that wires into form-level change + button control
|
|
405
|
+
function setValue(next: T | undefined, variant: string = "direct") {
|
|
406
|
+
const prev = stateRef.current.value as T | undefined;
|
|
407
|
+
if (Object.is(prev, next)) return;
|
|
408
|
+
|
|
409
|
+
const runFormOnChange = () => {
|
|
410
|
+
const props: any = form.props ?? {};
|
|
411
|
+
const fn = props.onChange as
|
|
412
|
+
| ((
|
|
413
|
+
form: CoreContext<Dict>,
|
|
414
|
+
current: Field,
|
|
415
|
+
options: Dict
|
|
416
|
+
) => void)
|
|
417
|
+
| undefined;
|
|
418
|
+
|
|
419
|
+
if (!fn) return;
|
|
420
|
+
|
|
421
|
+
fn(form as any, field, {
|
|
422
|
+
variant,
|
|
423
|
+
value: next,
|
|
424
|
+
previous: prev,
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const props: any = form.props ?? {};
|
|
429
|
+
const changeBefore = !!props.changeBefore;
|
|
430
|
+
|
|
431
|
+
if (changeBefore) {
|
|
432
|
+
runFormOnChange();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
stateRef.current.value = next;
|
|
436
|
+
setValueState(next);
|
|
437
|
+
|
|
438
|
+
// Local field-level onChange
|
|
439
|
+
if (field.onChange) {
|
|
440
|
+
field.onChange(next, prev, variant);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!changeBefore) {
|
|
444
|
+
runFormOnChange();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Let the core adjust the active button’s disabled state
|
|
448
|
+
form.controlButton();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function setError(message: string) {
|
|
452
|
+
stateRef.current.error = message;
|
|
453
|
+
setErrorState(message);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function setLoading(loading: boolean) {
|
|
457
|
+
stateRef.current.loading = loading;
|
|
458
|
+
setLoadingState(loading);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
ref,
|
|
463
|
+
get key() {
|
|
464
|
+
return keyRef.current
|
|
465
|
+
},
|
|
466
|
+
value,
|
|
467
|
+
setValue,
|
|
468
|
+
error,
|
|
469
|
+
setError,
|
|
470
|
+
loading,
|
|
471
|
+
setLoading,
|
|
472
|
+
required,
|
|
473
|
+
setRequired,
|
|
474
|
+
disabled,
|
|
475
|
+
setDisabled,
|
|
476
|
+
readOnly,
|
|
477
|
+
setReadOnly,
|
|
478
|
+
name: field.name!,
|
|
479
|
+
bindId: field.bindId!,
|
|
480
|
+
bind: field.bind,
|
|
481
|
+
shared: field.shared,
|
|
482
|
+
groupId: field.groupId,
|
|
483
|
+
alias: field.alias,
|
|
484
|
+
main: field.main,
|
|
485
|
+
ignore: field.ignore,
|
|
486
|
+
get defaultValue() {
|
|
487
|
+
return stateRef.current.original as T | undefined;
|
|
488
|
+
},
|
|
489
|
+
get originalValue() {
|
|
490
|
+
return stateRef.current.original;
|
|
491
|
+
},
|
|
492
|
+
form,
|
|
493
|
+
validate(report?: boolean) {
|
|
494
|
+
return field.validate?.(report);
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/core/hooks/use-optional-field.ts
|
|
2
|
+
// noinspection GrazieInspection
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
useField,
|
|
6
|
+
UseFieldOptions,
|
|
7
|
+
UseFieldReturn,
|
|
8
|
+
} from "@/core/hooks/use-field";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Optional variant of `useField`.
|
|
12
|
+
*
|
|
13
|
+
* - If there is a CoreProvider, behaves like `useField`.
|
|
14
|
+
* - If not, it fails gracefully and returns `undefined`.
|
|
15
|
+
*
|
|
16
|
+
* This is handy for inputs that should degrade gracefully when
|
|
17
|
+
* rendered outside of a form context.
|
|
18
|
+
*/
|
|
19
|
+
export function useOptionalField<T = unknown>(
|
|
20
|
+
options: UseFieldOptions<T>
|
|
21
|
+
): UseFieldReturn<T> | undefined {
|
|
22
|
+
try {
|
|
23
|
+
return useField<T>(options);
|
|
24
|
+
} catch {
|
|
25
|
+
// Most likely: no CoreProvider / context not available.
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/core/registry/binder-registry.ts
|
|
2
|
+
// noinspection JSUnusedGlobalSymbols
|
|
3
|
+
|
|
4
|
+
import type { Dict } from "@/schema/core";
|
|
5
|
+
import type { Field } from "@/schema/field";
|
|
6
|
+
import type { BindHost } from "@/core/bound/bind-host";
|
|
7
|
+
import {
|
|
8
|
+
getBoundField,
|
|
9
|
+
hasBoundField,
|
|
10
|
+
readBoundValue,
|
|
11
|
+
setBoundValue,
|
|
12
|
+
setBoundError,
|
|
13
|
+
validateBoundField,
|
|
14
|
+
observeBoundField,
|
|
15
|
+
} from "@/core/bound/observe-bound-field";
|
|
16
|
+
import { waitForBoundField } from "@/core/bound/wait-for-bound-field";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* BinderRegistry: bound-field utilities for a given host (CoreContext or FieldRegistry).
|
|
20
|
+
*
|
|
21
|
+
* - Hosts must satisfy BindHost (getBind + optional controlButton).
|
|
22
|
+
* - FieldRegistry already does (via getBind() we added).
|
|
23
|
+
* - CoreContext also does.
|
|
24
|
+
*
|
|
25
|
+
* You typically access this via:
|
|
26
|
+
* form.inputs.binding // where inputs is a FieldRegistry
|
|
27
|
+
*/
|
|
28
|
+
export class BinderRegistry<V extends Dict = Dict> {
|
|
29
|
+
constructor(private readonly host: BindHost<V>) {}
|
|
30
|
+
|
|
31
|
+
/** Raw field access. */
|
|
32
|
+
get(bindId: string): Field | undefined {
|
|
33
|
+
return getBoundField(this.host, bindId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
has(bindId: string): boolean {
|
|
37
|
+
return hasBoundField(this.host, bindId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read current value. */
|
|
41
|
+
value<T = unknown>(bindId: string): T | undefined {
|
|
42
|
+
return readBoundValue<T, V>(this.host, bindId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Set value (and trigger controlButton / onChange). */
|
|
46
|
+
set<T = unknown>(
|
|
47
|
+
bindId: string,
|
|
48
|
+
value: T,
|
|
49
|
+
variant: string = "util"
|
|
50
|
+
): boolean {
|
|
51
|
+
return setBoundValue<T, V>(this.host, bindId, value, variant);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Set error message on the bound field. */
|
|
55
|
+
error(bindId: string, msg: string): boolean {
|
|
56
|
+
return setBoundError<V>(this.host, bindId, msg);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Run the field’s own validate(). */
|
|
60
|
+
validate(bindId: string, report = true): boolean {
|
|
61
|
+
return validateBoundField<V>(this.host, bindId, report);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Observe a bound field’s value/error and liveness. */
|
|
65
|
+
observe<T = unknown>(
|
|
66
|
+
bindId: string,
|
|
67
|
+
handler: (evt: {
|
|
68
|
+
exists: boolean;
|
|
69
|
+
field?: Field;
|
|
70
|
+
value?: T;
|
|
71
|
+
error?: string;
|
|
72
|
+
}) => void,
|
|
73
|
+
pollMs = 300
|
|
74
|
+
): () => void {
|
|
75
|
+
return observeBoundField<T, V>(this.host, bindId, handler, pollMs);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Wait for a bound field to appear. */
|
|
79
|
+
wait(bindId: string, timeoutMs = 5000): Promise<Field> {
|
|
80
|
+
return waitForBoundField<V>(this.host, bindId, timeoutMs);
|
|
81
|
+
}
|
|
82
|
+
}
|