@astroapps/forms-core 1.2.3 → 2.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.
@@ -0,0 +1,807 @@
1
+ import { FormNode, lookupDataNode } from "./formNode";
2
+ import {
3
+ hideDisplayOnly,
4
+ SchemaDataNode,
5
+ validDataNode,
6
+ } from "./schemaDataNode";
7
+ import { SchemaInterface } from "./schemaInterface";
8
+ import { FieldOption } from "./schemaField";
9
+ import {
10
+ ChangeListenerFunc,
11
+ CleanupScope,
12
+ Control,
13
+ createScopedEffect,
14
+ createSyncEffect,
15
+ newControl,
16
+ updateComputedValue,
17
+ updateElements,
18
+ } from "@astroapps/controls";
19
+ import { createEvalExpr, ExpressionEvalContext } from "./evalExpression";
20
+ import { EntityExpression } from "./entityExpression";
21
+ import { createScoped } from "./util";
22
+ import {
23
+ AnyControlDefinition,
24
+ ControlAdornmentType,
25
+ ControlDefinition,
26
+ ControlDisableType,
27
+ DataGroupRenderOptions,
28
+ DataRenderType,
29
+ DynamicPropertyType,
30
+ getGroupRendererOptions,
31
+ GridRendererOptions,
32
+ HtmlDisplay,
33
+ isActionControl,
34
+ isControlDisabled,
35
+ isControlReadonly,
36
+ isDataControl,
37
+ isDataGroupRenderer,
38
+ isDisplayControl,
39
+ isGroupControl,
40
+ isHtmlDisplay,
41
+ isTextDisplay,
42
+ TextDisplay,
43
+ } from "./controlDefinition";
44
+ import { createOverrideProxy, KeysOfUnion, NoOverride } from "./overrideProxy";
45
+ import { ChildNodeSpec, ChildResolverFunc } from "./resolveChildren";
46
+ import { setupValidation } from "./validators";
47
+ import { groupedControl } from "./controlBuilder";
48
+
49
+ export type EvalExpr = <A>(
50
+ scope: CleanupScope,
51
+ init: A,
52
+ nk: Control<A>,
53
+ e: EntityExpression | undefined,
54
+ coerce: (t: unknown) => any,
55
+ ) => boolean;
56
+
57
+ export type VariablesFunc = (
58
+ changes: ChangeListenerFunc<any>,
59
+ ) => Record<string, any>;
60
+ export interface FormNodeOptions {
61
+ forceReadonly?: boolean;
62
+ forceDisabled?: boolean;
63
+ forceHidden?: boolean;
64
+ variables?: VariablesFunc;
65
+ }
66
+ export interface FormGlobalOptions {
67
+ schemaInterface: SchemaInterface;
68
+ evalExpression: (e: EntityExpression, ctx: ExpressionEvalContext) => void;
69
+ resolveChildren(c: FormStateNode): ChildNodeSpec[];
70
+ runAsync: (af: () => void) => void;
71
+ clearHidden: boolean;
72
+ }
73
+
74
+ export interface ResolvedDefinition {
75
+ definition: ControlDefinition;
76
+ display?: string;
77
+ stateId?: string;
78
+ style?: object;
79
+ layoutStyle?: object;
80
+ fieldOptions?: FieldOption[];
81
+ }
82
+
83
+ export interface FormStateBase {
84
+ parent: SchemaDataNode;
85
+ dataNode?: SchemaDataNode | undefined;
86
+ readonly: boolean;
87
+ visible: boolean | null;
88
+ disabled: boolean;
89
+ resolved: ResolvedDefinition;
90
+ childIndex: number;
91
+ busy: boolean;
92
+ }
93
+
94
+ export interface FormNodeUi {
95
+ ensureVisible(): void;
96
+ ensureChildVisible(childIndex: number): void;
97
+ getDisabler(type: ControlDisableType): () => () => void;
98
+ }
99
+
100
+ export interface FormStateNode extends FormStateBase, FormNodeOptions {
101
+ childKey: string | number;
102
+ uniqueId: string;
103
+ definition: ControlDefinition;
104
+ schemaInterface: SchemaInterface;
105
+ valid: boolean;
106
+ touched: boolean;
107
+ clearHidden: boolean;
108
+ variables?: (changes: ChangeListenerFunc<any>) => Record<string, any>;
109
+ meta: Record<string, any>;
110
+ form: FormNode | undefined | null;
111
+ children: FormStateNode[];
112
+ parentNode: FormStateNode | undefined;
113
+ setTouched(b: boolean, notChildren?: boolean): void;
114
+ validate(): boolean;
115
+ getChildCount(): number;
116
+ getChild(index: number): FormStateNode | undefined;
117
+ ensureMeta<A>(key: string, init: (scope: CleanupScope) => A): A;
118
+ cleanup(): void;
119
+ ui: FormNodeUi;
120
+ attachUi(f: FormNodeUi): void;
121
+ setBusy(busy: boolean): void;
122
+ setForceDisabled(forceDisable: boolean): void;
123
+ }
124
+ export function createEvaluatedDefinition(
125
+ def: ControlDefinition,
126
+ evalExpr: EvalExpr,
127
+ scope: CleanupScope,
128
+ display: Control<string | undefined>,
129
+ ): ControlDefinition {
130
+ const definitionOverrides = createScoped<Record<string, any>>(scope, {});
131
+ const displayOverrides = createScoped<Record<string, any>>(scope, {});
132
+ const groupOptionsOverrides = createScoped<Record<string, any>>(scope, {});
133
+ const renderOptionsOverrides = createScoped<Record<string, any>>(scope, {});
134
+
135
+ const {
136
+ hidden,
137
+ displayData,
138
+ readonly,
139
+ disabled,
140
+ defaultValue,
141
+ actionData,
142
+ title,
143
+ groupOptions,
144
+ renderOptions,
145
+ } = definitionOverrides.fields as Record<
146
+ KeysOfUnion<AnyControlDefinition>,
147
+ Control<any>
148
+ >;
149
+
150
+ const { columns } = groupOptionsOverrides.fields as Record<
151
+ KeysOfUnion<GridRendererOptions>,
152
+ Control<any>
153
+ >;
154
+
155
+ const { groupOptions: dataGroupRenderOptions } =
156
+ renderOptionsOverrides.fields as Record<
157
+ KeysOfUnion<DataGroupRenderOptions>,
158
+ Control<any>
159
+ >;
160
+
161
+ const { html, text } = displayOverrides.fields as Record<
162
+ KeysOfUnion<TextDisplay | HtmlDisplay>,
163
+ Control<any>
164
+ >;
165
+
166
+ updateComputedValue(dataGroupRenderOptions, () =>
167
+ isDataControl(def) && isDataGroupRenderer(def.renderOptions)
168
+ ? createOverrideProxy(
169
+ (def.renderOptions.groupOptions as GridRendererOptions) ?? {},
170
+ groupOptionsOverrides,
171
+ )
172
+ : undefined,
173
+ );
174
+
175
+ updateComputedValue(displayData, () =>
176
+ isDisplayControl(def)
177
+ ? createOverrideProxy(def.displayData, displayOverrides)
178
+ : undefined,
179
+ );
180
+
181
+ updateComputedValue(groupOptions, () => {
182
+ const groupOptions = getGroupRendererOptions(def);
183
+ return groupOptions
184
+ ? createOverrideProxy(groupOptions, groupOptionsOverrides)
185
+ : undefined;
186
+ });
187
+
188
+ updateComputedValue(renderOptions, () =>
189
+ isDataControl(def)
190
+ ? createOverrideProxy(def.renderOptions ?? {}, renderOptionsOverrides)
191
+ : undefined,
192
+ );
193
+
194
+ evalDynamic(
195
+ hidden,
196
+ DynamicPropertyType.Visible,
197
+ // Make sure it's not null if no scripting
198
+ (x) => (x ? def.hidden : !!def.hidden),
199
+ (r) => !r,
200
+ );
201
+
202
+ evalDynamic(
203
+ readonly,
204
+ DynamicPropertyType.Readonly,
205
+ () => isControlReadonly(def),
206
+ (r) => !!r,
207
+ );
208
+
209
+ createScopedEffect((c) => {
210
+ evalExpr(
211
+ c,
212
+ isControlDisabled(def),
213
+ disabled,
214
+ firstExpr(DynamicPropertyType.Disabled),
215
+ (r) => !!r,
216
+ );
217
+ }, definitionOverrides);
218
+
219
+ createScopedEffect((c) => {
220
+ const groupOptions = getGroupRendererOptions(def);
221
+ evalExpr(
222
+ c,
223
+ (groupOptions as GridRendererOptions)?.columns,
224
+ columns,
225
+ groupOptions ? firstExpr(DynamicPropertyType.GridColumns) : undefined,
226
+ (r) => (typeof r === "number" ? r : undefined),
227
+ );
228
+ }, groupOptionsOverrides);
229
+
230
+ createScopedEffect((c) => {
231
+ evalExpr(
232
+ c,
233
+ isDataControl(def) ? def.defaultValue : undefined,
234
+ defaultValue,
235
+ isDataControl(def)
236
+ ? firstExpr(DynamicPropertyType.DefaultValue)
237
+ : undefined,
238
+ (r) => r,
239
+ );
240
+ }, definitionOverrides);
241
+
242
+ createScopedEffect((c) => {
243
+ evalExpr(
244
+ c,
245
+ isActionControl(def) ? def.actionData : undefined,
246
+ actionData,
247
+ isActionControl(def)
248
+ ? firstExpr(DynamicPropertyType.ActionData)
249
+ : undefined,
250
+ (r) => r,
251
+ );
252
+ }, definitionOverrides);
253
+
254
+ createScopedEffect((c) => {
255
+ evalExpr(
256
+ c,
257
+ def.title,
258
+ title,
259
+ firstExpr(DynamicPropertyType.Label),
260
+ coerceString,
261
+ );
262
+ }, definitionOverrides);
263
+
264
+ createSyncEffect(() => {
265
+ if (isDisplayControl(def)) {
266
+ if (display.value !== undefined) {
267
+ text.value = isTextDisplay(def.displayData)
268
+ ? display.value
269
+ : NoOverride;
270
+ html.value = isHtmlDisplay(def.displayData)
271
+ ? display.value
272
+ : NoOverride;
273
+ } else {
274
+ text.value = NoOverride;
275
+ html.value = NoOverride;
276
+ }
277
+ }
278
+ }, displayOverrides);
279
+
280
+ return createOverrideProxy(def, definitionOverrides);
281
+
282
+ function firstExpr(
283
+ property: DynamicPropertyType,
284
+ ): EntityExpression | undefined {
285
+ return def.dynamic?.find((x) => x.type === property && x.expr.type)?.expr;
286
+ }
287
+
288
+ function evalDynamic<A>(
289
+ control: Control<A>,
290
+ property: DynamicPropertyType,
291
+ init: (ex: EntityExpression | undefined) => A,
292
+ coerce: (v: unknown) => any,
293
+ ) {
294
+ createScopedEffect((c) => {
295
+ const x = firstExpr(property);
296
+ evalExpr(c, init(x), control, x, coerce);
297
+ }, scope);
298
+ }
299
+ }
300
+
301
+ export function coerceStyle(v: unknown): any {
302
+ return typeof v === "object" ? v : undefined;
303
+ }
304
+
305
+ export function coerceString(v: unknown): string {
306
+ if (typeof v === "string") return v;
307
+ if (v == null) return "";
308
+ switch (typeof v) {
309
+ case "number":
310
+ case "boolean":
311
+ return v.toString();
312
+ default:
313
+ return JSON.stringify(v);
314
+ }
315
+ }
316
+
317
+ export function createFormStateNode(
318
+ formNode: FormNode,
319
+ parent: SchemaDataNode,
320
+ options: FormGlobalOptions,
321
+ nodeOptions: FormNodeOptions,
322
+ ): FormStateNodeImpl {
323
+ const globals = newControl<FormGlobalOptions>({
324
+ schemaInterface: options.schemaInterface,
325
+ evalExpression: options.evalExpression,
326
+ runAsync: options.runAsync,
327
+ resolveChildren: options.resolveChildren,
328
+ clearHidden: options.clearHidden,
329
+ });
330
+ return new FormStateNodeImpl(
331
+ "ROOT",
332
+ {},
333
+ formNode.definition,
334
+ formNode,
335
+ nodeOptions,
336
+ globals,
337
+ parent,
338
+ undefined,
339
+ 0,
340
+ options.resolveChildren,
341
+ );
342
+ }
343
+
344
+ export interface FormStateBaseImpl extends FormStateBase {
345
+ children: FormStateBaseImpl[];
346
+ allowedOptions?: unknown;
347
+ nodeOptions: FormNodeOptions;
348
+ busy: boolean;
349
+ }
350
+
351
+ export const noopUi: FormNodeUi = {
352
+ ensureChildVisible(childIndex: number) {},
353
+ ensureVisible() {},
354
+ getDisabler(type: ControlDisableType): () => () => void {
355
+ return () => () => {};
356
+ },
357
+ };
358
+
359
+ class FormStateNodeImpl implements FormStateNode {
360
+ readonly base: Control<FormStateBaseImpl>;
361
+ readonly options: Control<FormNodeOptions>;
362
+ readonly resolveChildren: ChildResolverFunc;
363
+
364
+ ui = noopUi;
365
+
366
+ constructor(
367
+ public childKey: string | number,
368
+ public meta: Record<string, any>,
369
+ definition: ControlDefinition,
370
+ public form: FormNode | undefined | null,
371
+ nodeOptions: FormNodeOptions,
372
+ public readonly globals: Control<FormGlobalOptions>,
373
+ public parent: SchemaDataNode,
374
+ public parentNode: FormStateNode | undefined,
375
+ childIndex: number,
376
+ resolveChildren?: ChildResolverFunc,
377
+ ) {
378
+ const base = newControl<FormStateBaseImpl>(
379
+ {
380
+ readonly: false,
381
+ visible: null,
382
+ disabled: false,
383
+ children: [],
384
+ resolved: { definition } as ResolvedDefinition,
385
+ parent,
386
+ allowedOptions: undefined,
387
+ childIndex,
388
+ nodeOptions,
389
+ busy: false,
390
+ },
391
+ { dontClearError: true },
392
+ );
393
+ this.base = base;
394
+ this.options = base.fields.nodeOptions;
395
+ base.meta["$FormState"] = this;
396
+ this.resolveChildren =
397
+ resolveChildren ?? globals.fields.resolveChildren.value;
398
+ initFormState(definition, this, parentNode);
399
+ }
400
+
401
+ get busy() {
402
+ return this.base.fields.busy.value;
403
+ }
404
+
405
+ setBusy(busy: boolean) {
406
+ this.base.fields.busy.value = busy;
407
+ }
408
+
409
+ get evalExpression(): (
410
+ e: EntityExpression,
411
+ ctx: ExpressionEvalContext,
412
+ ) => void {
413
+ return this.globals.fields.evalExpression.value;
414
+ }
415
+
416
+ get runAsync() {
417
+ return this.globals.fields.runAsync.value;
418
+ }
419
+
420
+ get schemaInterface(): SchemaInterface {
421
+ return this.globals.fields.schemaInterface.value;
422
+ }
423
+
424
+ get forceDisabled() {
425
+ return this.options.fields.forceDisabled.value;
426
+ }
427
+
428
+ setForceDisabled(value: boolean) {
429
+ return (this.options.fields.forceDisabled.value = value);
430
+ }
431
+
432
+ get forceReadonly() {
433
+ return this.options.fields.forceReadonly.value;
434
+ }
435
+ get forceHidden() {
436
+ return this.options.fields.forceHidden.value;
437
+ }
438
+
439
+ attachUi(f: FormNodeUi) {
440
+ this.ui = f;
441
+ }
442
+
443
+ get childIndex() {
444
+ return this.base.fields.childIndex.value;
445
+ }
446
+
447
+ get children() {
448
+ return this.base.fields.children.elements.map(
449
+ (x) => x.meta["$FormState"] as FormStateNode,
450
+ );
451
+ }
452
+
453
+ get uniqueId() {
454
+ return this.base.uniqueId.toString();
455
+ }
456
+ get valid(): boolean {
457
+ return this.base.valid;
458
+ }
459
+
460
+ get touched(): boolean {
461
+ return this.base.touched;
462
+ }
463
+
464
+ setTouched(touched: boolean, notChildren?: boolean) {
465
+ this.base.setTouched(touched, notChildren);
466
+ }
467
+
468
+ validate(): boolean {
469
+ this.children.forEach((child) => {
470
+ child.validate();
471
+ });
472
+ if (this.dataNode) {
473
+ this.dataNode.control.validate();
474
+ }
475
+ return this.valid;
476
+ }
477
+
478
+ get readonly() {
479
+ return this.base.fields.readonly.value;
480
+ }
481
+
482
+ get visible() {
483
+ return this.base.fields.visible.value;
484
+ }
485
+
486
+ get disabled() {
487
+ return this.base.fields.disabled.value;
488
+ }
489
+
490
+ get clearHidden() {
491
+ return this.globals.fields.clearHidden.value;
492
+ }
493
+
494
+ get variables() {
495
+ return this.options.fields.variables.value;
496
+ }
497
+
498
+ get definition() {
499
+ return this.resolved.definition;
500
+ }
501
+
502
+ getChild(index: number) {
503
+ return this.base.fields.children.elements[index]?.meta[
504
+ "$FormState"
505
+ ] as FormStateNode;
506
+ }
507
+
508
+ getChildCount(): number {
509
+ return this.base.fields.children.elements.length;
510
+ }
511
+
512
+ cleanup() {
513
+ this.base.cleanup();
514
+ }
515
+
516
+ get resolved() {
517
+ return this.base.fields.resolved.value;
518
+ }
519
+
520
+ get dataNode() {
521
+ return this.base.fields.dataNode.value;
522
+ }
523
+
524
+ ensureMeta<A>(key: string, init: (scope: CleanupScope) => A): A {
525
+ if (key in this.meta) return this.meta[key];
526
+ const res = init(this.base);
527
+ this.meta[key] = res;
528
+ return res;
529
+ }
530
+ }
531
+
532
+ function initFormState(
533
+ def: ControlDefinition,
534
+ impl: FormStateNodeImpl,
535
+ parentNode: FormStateNode | undefined,
536
+ ) {
537
+ const {
538
+ base,
539
+ options,
540
+ schemaInterface,
541
+ runAsync,
542
+ evalExpression,
543
+ parent,
544
+ variables,
545
+ } = impl;
546
+
547
+ const evalExpr = createEvalExpr(evalExpression, {
548
+ schemaInterface,
549
+ variables,
550
+ dataNode: parent,
551
+ runAsync,
552
+ });
553
+
554
+ const scope = base;
555
+
556
+ const { forceReadonly, forceDisabled, forceHidden } = options.fields;
557
+ const resolved = base.fields.resolved.as<ResolvedDefinition>();
558
+ const {
559
+ style,
560
+ layoutStyle,
561
+ fieldOptions,
562
+ display,
563
+ definition: rd,
564
+ } = resolved.fields;
565
+
566
+ evalDynamic(display, DynamicPropertyType.Display, undefined, coerceString);
567
+
568
+ const { dataNode, readonly, disabled, visible, children, allowedOptions } =
569
+ base.fields;
570
+
571
+ const definition = createEvaluatedDefinition(def, evalExpr, scope, display);
572
+ rd.value = definition;
573
+
574
+ evalDynamic(style, DynamicPropertyType.Style, undefined, coerceStyle);
575
+ evalDynamic(
576
+ layoutStyle,
577
+ DynamicPropertyType.LayoutStyle,
578
+ undefined,
579
+ coerceStyle,
580
+ );
581
+ evalDynamic(
582
+ allowedOptions,
583
+ DynamicPropertyType.AllowedOptions,
584
+ undefined,
585
+ (x) => x,
586
+ );
587
+
588
+ updateComputedValue(dataNode, () => lookupDataNode(definition, parent));
589
+
590
+ updateComputedValue(visible, () => {
591
+ if (forceHidden.value) return false;
592
+ if (parentNode && !parentNode.visible) return parentNode.visible;
593
+ const dn = dataNode.value;
594
+ if (
595
+ dn &&
596
+ (!validDataNode(dn) || hideDisplayOnly(dn, schemaInterface, definition))
597
+ )
598
+ return false;
599
+ return definition.hidden == null ? null : !definition.hidden;
600
+ });
601
+
602
+ updateComputedValue(
603
+ readonly,
604
+ () =>
605
+ parentNode?.readonly ||
606
+ forceReadonly.value ||
607
+ isControlReadonly(definition),
608
+ );
609
+ updateComputedValue(
610
+ disabled,
611
+ () =>
612
+ parentNode?.disabled ||
613
+ forceDisabled.value ||
614
+ isControlDisabled(definition),
615
+ );
616
+
617
+ updateComputedValue(fieldOptions, () => {
618
+ const dn = dataNode.value;
619
+ if (!dn) return undefined;
620
+ const fieldOptions = schemaInterface.getDataOptions(dn);
621
+ const _allowed = allowedOptions.value ?? [];
622
+ const allowed = Array.isArray(_allowed) ? _allowed : [_allowed];
623
+
624
+ return allowed.length > 0
625
+ ? allowed
626
+ .map((x) =>
627
+ typeof x === "object"
628
+ ? x
629
+ : (fieldOptions?.find((y) => y.value == x) ?? {
630
+ name: x.toString(),
631
+ value: x,
632
+ }),
633
+ )
634
+ .filter((x) => x != null)
635
+ : fieldOptions;
636
+ });
637
+
638
+ createSyncEffect(() => {
639
+ const dn = dataNode.value;
640
+ if (dn) {
641
+ dn.control.disabled = disabled.value;
642
+ }
643
+ }, scope);
644
+
645
+ createSyncEffect(() => {
646
+ const dn = dataNode.value;
647
+ if (dn) {
648
+ dn.control.touched = base.touched;
649
+ }
650
+ }, scope);
651
+
652
+ createSyncEffect(() => {
653
+ const dn = dataNode.value;
654
+ if (dn) {
655
+ base.touched = dn.control.touched;
656
+ }
657
+ }, scope);
658
+
659
+ createSyncEffect(() => {
660
+ const dn = dataNode.value;
661
+ base.setErrors(dn?.control.errors);
662
+ }, scope);
663
+
664
+ setupValidation(
665
+ scope,
666
+ impl.variables,
667
+ definition,
668
+ dataNode,
669
+ schemaInterface,
670
+ parent,
671
+ visible,
672
+ runAsync,
673
+ );
674
+
675
+ createSyncEffect(() => {
676
+ const dn = dataNode.value?.control;
677
+ if (dn && isDataControl(definition)) {
678
+ if (impl.visible == false) {
679
+ if (impl.clearHidden && !definition.dontClearHidden) {
680
+ // console.log("Clearing hidden");
681
+ dn.value = undefined;
682
+ }
683
+ } else if (
684
+ impl.visible &&
685
+ dn.value === undefined &&
686
+ definition.defaultValue != null &&
687
+ !definition.adornments?.some(
688
+ (x) => x.type === ControlAdornmentType.Optional,
689
+ ) &&
690
+ definition.renderOptions?.type != DataRenderType.NullToggle
691
+ ) {
692
+ // console.log(
693
+ // "Setting to default",
694
+ // definition.defaultValue,
695
+ // definition.field,
696
+ // );
697
+ // const [required, dcv] = isDataControl(definition)
698
+ // ? [definition.required, definition.defaultValue]
699
+ // : [false, undefined];
700
+ // const field = ctx.dataNode?.schema.field;
701
+ // return (
702
+ // dcv ??
703
+ // (field
704
+ // ? ctx.dataNode!.elementIndex != null
705
+ // ? elementValueForField(field)
706
+ // : defaultValueForField(field, required)
707
+ // : undefined)
708
+ // );
709
+ dn.value = definition.defaultValue;
710
+ }
711
+ }
712
+ }, scope);
713
+
714
+ initChildren(impl);
715
+
716
+ function firstExpr(
717
+ property: DynamicPropertyType,
718
+ ): EntityExpression | undefined {
719
+ return def.dynamic?.find((x) => x.type === property && x.expr.type)?.expr;
720
+ }
721
+
722
+ function evalDynamic<A>(
723
+ control: Control<A>,
724
+ property: DynamicPropertyType,
725
+ init: A,
726
+ coerce: (v: unknown) => any,
727
+ ) {
728
+ createScopedEffect(
729
+ (c) => evalExpr(c, init, control, firstExpr(property), coerce),
730
+ scope,
731
+ );
732
+ }
733
+ }
734
+
735
+ export function combineVariables(
736
+ v1?: VariablesFunc,
737
+ v2?: VariablesFunc,
738
+ ): VariablesFunc | undefined {
739
+ if (!v1) return v2;
740
+ if (!v2) return v1;
741
+ return (c) => ({ ...v1(c), ...v2(c) });
742
+ }
743
+
744
+ function initChildren(formImpl: FormStateNodeImpl) {
745
+ const childMap = new Map<any, Control<FormStateBaseImpl>>();
746
+ createSyncEffect(() => {
747
+ const { base, resolveChildren } = formImpl;
748
+ const children = base.fields.children;
749
+ const kids = resolveChildren(formImpl);
750
+ const scope = base;
751
+ const detached = updateElements(children, () =>
752
+ kids.map(({ childKey, create }, childIndex) => {
753
+ let child = childMap.get(childKey);
754
+ if (child) {
755
+ child.fields.childIndex.value = childIndex;
756
+ } else {
757
+ const meta: Record<string, any> = {};
758
+ const cc = create(scope, meta);
759
+ const newOptions: FormNodeOptions = {
760
+ forceHidden: false,
761
+ forceDisabled: false,
762
+ forceReadonly: false,
763
+ variables: combineVariables(formImpl.variables, cc.variables),
764
+ };
765
+ const fsChild = new FormStateNodeImpl(
766
+ childKey,
767
+ meta,
768
+ cc.definition ?? groupedControl([]),
769
+ cc.node === undefined ? formImpl.form : cc.node,
770
+ newOptions,
771
+ formImpl.globals,
772
+ cc.parent ?? formImpl.parent,
773
+ formImpl,
774
+ childIndex,
775
+ cc.resolveChildren,
776
+ );
777
+ child = fsChild.base;
778
+ childMap.set(childKey, child);
779
+ }
780
+ return child;
781
+ }),
782
+ );
783
+ detached.forEach((child) => child.cleanup());
784
+ }, formImpl.base);
785
+ }
786
+
787
+ /**
788
+ * Interface representing the form context data.
789
+ */
790
+ export interface FormContextData {
791
+ option?: FieldOption;
792
+ optionSelected?: boolean;
793
+ }
794
+
795
+ export function visitFormState<A>(
796
+ node: FormStateNode,
797
+ visitFn: (node: FormStateNode) => A | undefined,
798
+ ): A | undefined {
799
+ const v = visitFn(node);
800
+ if (v !== undefined) return v;
801
+ const childCount = node.getChildCount();
802
+ for (let i = 0; i < childCount; i++) {
803
+ const res = visitFormState(node.getChild(i)!, visitFn);
804
+ if (res !== undefined) return res;
805
+ }
806
+ return undefined;
807
+ }