@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.
Files changed (109) hide show
  1. package/.scaffold-cache.json +537 -0
  2. package/package.json +42 -0
  3. package/src/.scaffold-cache.json +544 -0
  4. package/src/adapters/axios.ts +117 -0
  5. package/src/adapters/index.ts +91 -0
  6. package/src/adapters/inertia.ts +187 -0
  7. package/src/core/adapter-registry.ts +87 -0
  8. package/src/core/bound/bind-host.ts +14 -0
  9. package/src/core/bound/observe-bound-field.ts +172 -0
  10. package/src/core/bound/wait-for-bound-field.ts +57 -0
  11. package/src/core/context.ts +23 -0
  12. package/src/core/core-provider.tsx +818 -0
  13. package/src/core/core-root.tsx +72 -0
  14. package/src/core/core-shell.tsx +44 -0
  15. package/src/core/errors/error-strip.tsx +71 -0
  16. package/src/core/errors/index.ts +2 -0
  17. package/src/core/errors/map-error-bag.ts +51 -0
  18. package/src/core/errors/map-zod.ts +39 -0
  19. package/src/core/hooks/use-button.ts +220 -0
  20. package/src/core/hooks/use-core-context.ts +20 -0
  21. package/src/core/hooks/use-core-utility.ts +0 -0
  22. package/src/core/hooks/use-core.ts +13 -0
  23. package/src/core/hooks/use-field.ts +497 -0
  24. package/src/core/hooks/use-optional-field.ts +28 -0
  25. package/src/core/index.ts +0 -0
  26. package/src/core/registry/binder-registry.ts +82 -0
  27. package/src/core/registry/field-registry.ts +187 -0
  28. package/src/core/test.tsx +17 -0
  29. package/src/global.d.ts +14 -0
  30. package/src/index.ts +68 -0
  31. package/src/input/index.ts +4 -0
  32. package/src/input/input-field.tsx +854 -0
  33. package/src/input/input-layout-graph.ts +230 -0
  34. package/src/input/input-props.ts +190 -0
  35. package/src/lib/get-global-countries.ts +87 -0
  36. package/src/lib/utils.ts +6 -0
  37. package/src/presets/index.ts +0 -0
  38. package/src/presets/shadcn-preset.ts +0 -0
  39. package/src/presets/shadcn-variants/checkbox.tsx +849 -0
  40. package/src/presets/shadcn-variants/chips.tsx +756 -0
  41. package/src/presets/shadcn-variants/color.tsx +284 -0
  42. package/src/presets/shadcn-variants/custom.tsx +227 -0
  43. package/src/presets/shadcn-variants/date.tsx +796 -0
  44. package/src/presets/shadcn-variants/file.tsx +764 -0
  45. package/src/presets/shadcn-variants/keyvalue.tsx +556 -0
  46. package/src/presets/shadcn-variants/multiselect.tsx +1132 -0
  47. package/src/presets/shadcn-variants/number.tsx +176 -0
  48. package/src/presets/shadcn-variants/password.tsx +737 -0
  49. package/src/presets/shadcn-variants/phone.tsx +628 -0
  50. package/src/presets/shadcn-variants/radio.tsx +578 -0
  51. package/src/presets/shadcn-variants/select.tsx +956 -0
  52. package/src/presets/shadcn-variants/slider.tsx +622 -0
  53. package/src/presets/shadcn-variants/text.tsx +343 -0
  54. package/src/presets/shadcn-variants/textarea.tsx +66 -0
  55. package/src/presets/shadcn-variants/toggle.tsx +218 -0
  56. package/src/presets/shadcn-variants/treeselect.tsx +784 -0
  57. package/src/presets/ui/badge.tsx +46 -0
  58. package/src/presets/ui/button.tsx +60 -0
  59. package/src/presets/ui/calendar.tsx +214 -0
  60. package/src/presets/ui/checkbox.tsx +115 -0
  61. package/src/presets/ui/custom.tsx +0 -0
  62. package/src/presets/ui/dialog.tsx +141 -0
  63. package/src/presets/ui/field.tsx +246 -0
  64. package/src/presets/ui/input-mask.tsx +739 -0
  65. package/src/presets/ui/input-otp.tsx +77 -0
  66. package/src/presets/ui/input.tsx +1011 -0
  67. package/src/presets/ui/label.tsx +22 -0
  68. package/src/presets/ui/number.tsx +1370 -0
  69. package/src/presets/ui/popover.tsx +46 -0
  70. package/src/presets/ui/radio-group.tsx +43 -0
  71. package/src/presets/ui/scroll-area.tsx +56 -0
  72. package/src/presets/ui/select.tsx +190 -0
  73. package/src/presets/ui/separator.tsx +28 -0
  74. package/src/presets/ui/slider.tsx +61 -0
  75. package/src/presets/ui/switch.tsx +32 -0
  76. package/src/presets/ui/textarea.tsx +634 -0
  77. package/src/presets/ui/time-dropdowns.tsx +350 -0
  78. package/src/schema/adapter.ts +217 -0
  79. package/src/schema/core.ts +429 -0
  80. package/src/schema/field-map.ts +0 -0
  81. package/src/schema/field.ts +224 -0
  82. package/src/schema/index.ts +0 -0
  83. package/src/schema/input-field.ts +260 -0
  84. package/src/schema/presets.ts +0 -0
  85. package/src/schema/variant.ts +216 -0
  86. package/src/variants/core/checkbox.tsx +54 -0
  87. package/src/variants/core/chips.tsx +22 -0
  88. package/src/variants/core/color.tsx +16 -0
  89. package/src/variants/core/custom.tsx +18 -0
  90. package/src/variants/core/date.tsx +25 -0
  91. package/src/variants/core/file.tsx +9 -0
  92. package/src/variants/core/keyvalue.tsx +12 -0
  93. package/src/variants/core/multiselect.tsx +28 -0
  94. package/src/variants/core/number.tsx +115 -0
  95. package/src/variants/core/password.tsx +35 -0
  96. package/src/variants/core/phone.tsx +16 -0
  97. package/src/variants/core/radio.tsx +38 -0
  98. package/src/variants/core/select.tsx +15 -0
  99. package/src/variants/core/slider.tsx +55 -0
  100. package/src/variants/core/text.tsx +114 -0
  101. package/src/variants/core/textarea.tsx +22 -0
  102. package/src/variants/core/toggle.tsx +50 -0
  103. package/src/variants/core/treeselect.tsx +11 -0
  104. package/src/variants/helpers/selection-summary.tsx +236 -0
  105. package/src/variants/index.ts +75 -0
  106. package/src/variants/registry.ts +38 -0
  107. package/src/variants/select-shared.ts +0 -0
  108. package/src/variants/shared.ts +126 -0
  109. 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
+ }