@astroapps/forms-core 1.0.0

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,491 @@
1
+ import { FormNode, lookupDataNode } from "./formNode";
2
+ import {
3
+ hideDisplayOnly,
4
+ SchemaDataNode,
5
+ validDataNode,
6
+ } from "./schemaDataNode";
7
+ import {
8
+ AnyControlDefinition,
9
+ ControlAdornmentType,
10
+ ControlDefinition,
11
+ DataRenderType,
12
+ DynamicPropertyType,
13
+ HtmlDisplay,
14
+ isActionControl,
15
+ isControlDisabled,
16
+ isControlReadonly,
17
+ isDataControl,
18
+ isDisplayControl,
19
+ isHtmlDisplay,
20
+ isTextDisplay,
21
+ TextDisplay,
22
+ } from "./controlDefinition";
23
+ import { SchemaInterface } from "./schemaInterface";
24
+ import { FieldOption } from "./schemaField";
25
+ import {
26
+ CleanupScope,
27
+ clearMetaValue,
28
+ Control,
29
+ createScopedEffect,
30
+ createSyncEffect,
31
+ ensureMetaValue,
32
+ getControlPath,
33
+ getCurrentFields,
34
+ newControl,
35
+ trackedValue,
36
+ unsafeRestoreControl,
37
+ updateComputedValue,
38
+ } from "@astroapps/controls";
39
+ import {
40
+ defaultEvaluators,
41
+ ExpressionEval,
42
+ ExpressionEvalContext,
43
+ } from "./evalExpression";
44
+ import { EntityExpression } from "./entityExpression";
45
+ import { createScoped, jsonPathString } from "./util";
46
+ import { setupValidation } from "./validators";
47
+
48
+ export interface ControlState {
49
+ definition: ControlDefinition;
50
+ schemaInterface: SchemaInterface;
51
+ dataNode?: SchemaDataNode | undefined;
52
+ stateId?: string;
53
+ style?: object;
54
+ layoutStyle?: object;
55
+ allowedOptions?: any[];
56
+ readonly: boolean;
57
+ hidden: boolean;
58
+ disabled: boolean;
59
+ clearHidden: boolean;
60
+ variables: Record<string, any>;
61
+ }
62
+
63
+ export interface FormContextOptions {
64
+ readonly?: boolean | null;
65
+ hidden?: boolean | null;
66
+ disabled?: boolean | null;
67
+ clearHidden?: boolean;
68
+ stateKey?: string;
69
+ variables?: Record<string, any>;
70
+ }
71
+
72
+ /**
73
+ * Interface representing the form context data.
74
+ */
75
+ export interface FormContextData {
76
+ option?: FieldOption;
77
+ optionSelected?: boolean;
78
+ }
79
+
80
+ export interface FormState {
81
+ getControlState(
82
+ parent: SchemaDataNode,
83
+ formNode: FormNode,
84
+ context: FormContextOptions,
85
+ ): ControlState;
86
+
87
+ cleanup(): void;
88
+
89
+ evalExpression(expr: EntityExpression, context: ExpressionEvalContext): void;
90
+ }
91
+
92
+ const formStates: FormState[] = [];
93
+
94
+ export function createFormState(
95
+ schemaInterface: SchemaInterface,
96
+ evaluators: Record<string, ExpressionEval<any>> = defaultEvaluators,
97
+ ): FormState {
98
+ // console.log("createFormState");
99
+ const controlStates = newControl<Record<string, FormContextOptions>>({});
100
+
101
+ function evalExpression(
102
+ e: EntityExpression,
103
+ context: ExpressionEvalContext,
104
+ ): void {
105
+ const x = evaluators[e.type];
106
+ x?.(e, context);
107
+ }
108
+
109
+ return {
110
+ evalExpression,
111
+ cleanup: () => {
112
+ // console.log("Cleanup form state");
113
+ controlStates.cleanup();
114
+ },
115
+ getControlState(
116
+ parent: SchemaDataNode,
117
+ formNode: FormNode,
118
+ context: FormContextOptions,
119
+ ): ControlState {
120
+ const stateId = parent.id + "$" + formNode.id + (context.stateKey ?? "");
121
+ const controlImpl = controlStates.fields[stateId];
122
+ controlImpl.value = context;
123
+ function evalExpr<A>(
124
+ scope: CleanupScope,
125
+ init: A,
126
+ nk: Control<A>,
127
+ e: EntityExpression | undefined,
128
+ coerce: (t: unknown) => any,
129
+ ): boolean {
130
+ nk.value = init;
131
+ if (e?.type) {
132
+ evalExpression(e, {
133
+ returnResult: (r) => {
134
+ nk.value = coerce(r);
135
+ },
136
+ scope,
137
+ dataNode: parent,
138
+ variables: controlImpl.fields.variables,
139
+ schemaInterface,
140
+ });
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+
146
+ return createScopedMetaValue(formNode, controlImpl, "impl", (scope) => {
147
+ const cf = controlImpl.fields;
148
+ const definitionOverrides = createScoped<Record<string, any>>(
149
+ controlImpl,
150
+ {},
151
+ );
152
+ const displayControl = createScoped<string | undefined>(
153
+ controlImpl,
154
+ undefined,
155
+ );
156
+
157
+ const displayOverrides = createScoped<Record<string, any>>(
158
+ controlImpl,
159
+ {},
160
+ );
161
+ const def = formNode.definition;
162
+ const definition = createOverrideProxy(def, definitionOverrides);
163
+ const of = definitionOverrides.fields as Record<
164
+ KeysOfUnion<AnyControlDefinition>,
165
+ Control<any>
166
+ >;
167
+
168
+ const df = displayOverrides.fields as Record<
169
+ KeysOfUnion<TextDisplay | HtmlDisplay>,
170
+ Control<any>
171
+ >;
172
+
173
+ createScopedEffect((c) => {
174
+ const textDisplay =
175
+ isDisplayControl(def) && isTextDisplay(def.displayData)
176
+ ? def.displayData
177
+ : undefined;
178
+ evalExpr(
179
+ c,
180
+ textDisplay?.text,
181
+ df.text,
182
+ textDisplay && firstExpr(formNode, DynamicPropertyType.Display),
183
+ coerceString,
184
+ );
185
+ }, displayOverrides);
186
+
187
+ createScopedEffect((c) => {
188
+ const htmlDisplay =
189
+ isDisplayControl(def) && isHtmlDisplay(def.displayData)
190
+ ? def.displayData
191
+ : undefined;
192
+ evalExpr(
193
+ c,
194
+ htmlDisplay?.html,
195
+ df.html,
196
+ htmlDisplay && firstExpr(formNode, DynamicPropertyType.Display),
197
+ coerceString,
198
+ );
199
+ }, displayOverrides);
200
+
201
+ updateComputedValue(of.displayData, () =>
202
+ isDisplayControl(def)
203
+ ? createOverrideProxy(def.displayData, displayOverrides)
204
+ : undefined,
205
+ );
206
+
207
+ createScopedEffect((c) => {
208
+ evalExpr(
209
+ c,
210
+ def.hidden,
211
+ of.hidden,
212
+ firstExpr(formNode, DynamicPropertyType.Visible),
213
+ (r) => !r,
214
+ );
215
+ }, definitionOverrides);
216
+
217
+ createScopedEffect((c) => {
218
+ evalExpr(
219
+ c,
220
+ isControlReadonly(def),
221
+ of.readonly,
222
+ firstExpr(formNode, DynamicPropertyType.Readonly),
223
+ (r) => !!r,
224
+ );
225
+ }, definitionOverrides);
226
+
227
+ createScopedEffect((c) => {
228
+ evalExpr(
229
+ c,
230
+ isControlDisabled(def),
231
+ of.disabled,
232
+ firstExpr(formNode, DynamicPropertyType.Disabled),
233
+ (r) => !!r,
234
+ );
235
+ }, definitionOverrides);
236
+
237
+ createScopedEffect((c) => {
238
+ evalExpr(
239
+ c,
240
+ isDataControl(def) ? def.defaultValue : undefined,
241
+ of.defaultValue,
242
+ isDataControl(def)
243
+ ? firstExpr(formNode, DynamicPropertyType.DefaultValue)
244
+ : undefined,
245
+ (r) => r,
246
+ );
247
+ }, definitionOverrides);
248
+
249
+ createScopedEffect((c) => {
250
+ evalExpr(
251
+ c,
252
+ isActionControl(def) ? def.actionData : undefined,
253
+ of.actionData,
254
+ isActionControl(def)
255
+ ? firstExpr(formNode, DynamicPropertyType.ActionData)
256
+ : undefined,
257
+ (r) => r,
258
+ );
259
+ }, definitionOverrides);
260
+
261
+ createScopedEffect((c) => {
262
+ evalExpr(
263
+ c,
264
+ def.title,
265
+ of.title,
266
+ firstExpr(formNode, DynamicPropertyType.Label),
267
+ coerceString,
268
+ );
269
+ }, definitionOverrides);
270
+
271
+ const control = createScoped<ControlState>(controlImpl, {
272
+ definition,
273
+ dataNode: undefined,
274
+ schemaInterface,
275
+ disabled: false,
276
+ readonly: false,
277
+ clearHidden: false,
278
+ hidden: false,
279
+ variables: controlImpl.fields.variables.current.value ?? {},
280
+ stateId,
281
+ });
282
+
283
+ const {
284
+ dataNode,
285
+ hidden,
286
+ readonly,
287
+ style,
288
+ layoutStyle,
289
+ allowedOptions,
290
+ disabled,
291
+ variables,
292
+ } = control.fields;
293
+
294
+ createScopedEffect(
295
+ (c) =>
296
+ evalExpr(
297
+ c,
298
+ undefined,
299
+ style,
300
+ firstExpr(formNode, DynamicPropertyType.Style),
301
+ coerceStyle,
302
+ ),
303
+ scope,
304
+ );
305
+
306
+ createScopedEffect(
307
+ (c) =>
308
+ evalExpr(
309
+ c,
310
+ undefined,
311
+ layoutStyle,
312
+ firstExpr(formNode, DynamicPropertyType.LayoutStyle),
313
+ coerceStyle,
314
+ ),
315
+ scope,
316
+ );
317
+
318
+ createScopedEffect(
319
+ (c) =>
320
+ evalExpr(
321
+ c,
322
+ undefined,
323
+ allowedOptions,
324
+ firstExpr(formNode, DynamicPropertyType.AllowedOptions),
325
+ (x) => x,
326
+ ),
327
+ scope,
328
+ );
329
+
330
+ updateComputedValue(dataNode, () => lookupDataNode(definition, parent));
331
+ updateComputedValue(
332
+ hidden,
333
+ () =>
334
+ !!cf.hidden.value ||
335
+ definition.hidden ||
336
+ (dataNode.value &&
337
+ (!validDataNode(dataNode.value) ||
338
+ hideDisplayOnly(dataNode.value, schemaInterface, definition))),
339
+ );
340
+
341
+ updateComputedValue(
342
+ readonly,
343
+ () => !!cf.readonly.value || isControlReadonly(definition),
344
+ );
345
+ updateComputedValue(
346
+ disabled,
347
+ () => !!cf.disabled.value || isControlDisabled(definition),
348
+ );
349
+
350
+ updateComputedValue(variables, () => {
351
+ return controlImpl.fields.variables.value ?? {};
352
+ });
353
+
354
+ createSyncEffect(() => {
355
+ const dn = dataNode.value;
356
+ if (dn) {
357
+ dn.control.disabled = disabled.value;
358
+ }
359
+ }, scope);
360
+
361
+ setupValidation(
362
+ controlImpl,
363
+ definition,
364
+ dataNode,
365
+ schemaInterface,
366
+ parent,
367
+ formNode,
368
+ );
369
+
370
+ createSyncEffect(() => {
371
+ const dn = dataNode.value?.control;
372
+ if (dn && isDataControl(definition)) {
373
+ if (definition.hidden) {
374
+ if (
375
+ controlImpl.fields.clearHidden.value &&
376
+ !definition.dontClearHidden
377
+ ) {
378
+ // console.log("Clearing hidden");
379
+ dn.value = undefined;
380
+ }
381
+ } else if (
382
+ dn.value === undefined &&
383
+ definition.defaultValue != null &&
384
+ !definition.adornments?.some(
385
+ (x) => x.type === ControlAdornmentType.Optional,
386
+ ) &&
387
+ definition.renderOptions?.type != DataRenderType.NullToggle
388
+ ) {
389
+ // console.log(
390
+ // "Setting to default",
391
+ // definition.defaultValue,
392
+ // definition.field,
393
+ // );
394
+ // const [required, dcv] = isDataControl(definition)
395
+ // ? [definition.required, definition.defaultValue]
396
+ // : [false, undefined];
397
+ // const field = ctx.dataNode?.schema.field;
398
+ // return (
399
+ // dcv ??
400
+ // (field
401
+ // ? ctx.dataNode!.elementIndex != null
402
+ // ? elementValueForField(field)
403
+ // : defaultValueForField(field, required)
404
+ // : undefined)
405
+ // );
406
+
407
+ dn.value = definition.defaultValue;
408
+ }
409
+ }
410
+ }, scope);
411
+ return createOverrideProxy(control.current.value, control);
412
+ });
413
+ },
414
+ };
415
+ }
416
+
417
+ function firstExpr(
418
+ formNode: FormNode,
419
+ property: DynamicPropertyType,
420
+ ): EntityExpression | undefined {
421
+ return formNode.definition.dynamic?.find(
422
+ (x) => x.type === property && x.expr.type,
423
+ )?.expr;
424
+ }
425
+
426
+ function coerceStyle(v: unknown): any {
427
+ return typeof v === "object" ? v : undefined;
428
+ }
429
+
430
+ function coerceString(v: unknown): string {
431
+ return typeof v === "string" ? v : (v?.toString() ?? "");
432
+ }
433
+
434
+ function createScopedMetaValue<A>(
435
+ formNode: FormNode,
436
+ c: Control<any>,
437
+ key: string,
438
+ init: (scope: CleanupScope) => A,
439
+ ): A {
440
+ return ensureMetaValue(c, key, () => {
441
+ const holder = createScoped<A | undefined>(c, undefined, {
442
+ equals: (a, b) => a === b,
443
+ });
444
+ const effect = createScopedEffect((c) => (holder.value = init(c)), holder);
445
+ effect.run = () => {
446
+ console.log(
447
+ "ControlState being recreated:",
448
+ effect.subscriptions.map(
449
+ (x) =>
450
+ `${x[1]?.mask} ${jsonPathString(getControlPath(x[0], unsafeRestoreControl(formNode.definition)))}`,
451
+ ),
452
+ );
453
+ };
454
+ return holder;
455
+ }).value!;
456
+ }
457
+
458
+ export function createOverrideProxy<
459
+ A extends object,
460
+ B extends Record<string, any>,
461
+ >(proxyFor: A, handlers: Control<B>): A {
462
+ const overrides = getCurrentFields(handlers);
463
+ const allOwn = Reflect.ownKeys(proxyFor);
464
+ Reflect.ownKeys(overrides).forEach((k) => {
465
+ if (!allOwn.includes(k)) allOwn.push(k);
466
+ });
467
+ return new Proxy(proxyFor, {
468
+ get(target: A, p: string | symbol, receiver: any): any {
469
+ if (Object.hasOwn(overrides, p)) {
470
+ return overrides[p as keyof B]!.value;
471
+ }
472
+ return Reflect.get(target, p, receiver);
473
+ },
474
+ ownKeys(target: A): ArrayLike<string | symbol> {
475
+ return allOwn;
476
+ },
477
+ has(target: A, p: string | symbol): boolean {
478
+ return Reflect.has(proxyFor, p) || Reflect.has(overrides, p);
479
+ },
480
+ getOwnPropertyDescriptor(target, k) {
481
+ if (Object.hasOwn(overrides, k))
482
+ return {
483
+ enumerable: true,
484
+ configurable: true,
485
+ };
486
+ return Reflect.getOwnPropertyDescriptor(target, k);
487
+ },
488
+ });
489
+ }
490
+
491
+ type KeysOfUnion<T> = T extends T ? keyof T : never;
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export * from "./controlDefinition";
2
+ export * from "./entityExpression";
3
+ export * from "./schemaNode";
4
+ export * from "./schemaBuilder";
5
+ export * from "./schemaField";
6
+ export * from "./schemaInterface";
7
+ export * from "./schemaValidator";
8
+ export * from "./schemaDataNode";
9
+ export * from "./defaultSchemaInterface";
10
+ export * from "./formNode";
11
+ export * from "./formState";
12
+ export * from "./util";