@astroapps/forms-core 1.2.3 → 2.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.
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./controlDefinition";
2
+ export * from "./controlBuilder";
2
3
  export * from "./entityExpression";
3
4
  export * from "./schemaNode";
4
5
  export * from "./schemaBuilder";
@@ -8,5 +9,8 @@ export * from "./schemaValidator";
8
9
  export * from "./schemaDataNode";
9
10
  export * from "./defaultSchemaInterface";
10
11
  export * from "./formNode";
11
- export * from "./formState";
12
+ export * from "./formStateNode";
13
+ export * from "./evalExpression";
12
14
  export * from "./util";
15
+ export * from "./overrideProxy";
16
+ export * from "./resolveChildren";
@@ -0,0 +1,40 @@
1
+ import { Control, getCurrentFields } from "@astroapps/controls";
2
+
3
+ export function createOverrideProxy<
4
+ A extends object,
5
+ B extends Record<string, any>,
6
+ >(proxyFor: A, handlers: Control<B>): A {
7
+ const overrides = getCurrentFields(handlers);
8
+ const allOwn = Reflect.ownKeys(proxyFor);
9
+ Reflect.ownKeys(overrides).forEach((k) => {
10
+ if (!allOwn.includes(k)) allOwn.push(k);
11
+ });
12
+ return new Proxy(proxyFor, {
13
+ get(target: A, p: string | symbol, receiver: any): any {
14
+ if (Object.hasOwn(overrides, p)) {
15
+ const nv = overrides[p as keyof B]!.value;
16
+ if (nv !== NoOverride) return nv;
17
+ }
18
+ return Reflect.get(target, p, receiver);
19
+ },
20
+ ownKeys(target: A): ArrayLike<string | symbol> {
21
+ return allOwn;
22
+ },
23
+ has(target: A, p: string | symbol): boolean {
24
+ return Reflect.has(proxyFor, p) || Reflect.has(overrides, p);
25
+ },
26
+ getOwnPropertyDescriptor(target, k) {
27
+ if (Object.hasOwn(overrides, k))
28
+ return {
29
+ enumerable: true,
30
+ configurable: true,
31
+ };
32
+ return Reflect.getOwnPropertyDescriptor(target, k);
33
+ },
34
+ });
35
+ }
36
+
37
+ class NoValue {}
38
+ export const NoOverride = new NoValue();
39
+
40
+ export type KeysOfUnion<T> = T extends T ? keyof T : never;
@@ -0,0 +1,141 @@
1
+ import {
2
+ ChangeListenerFunc,
3
+ CleanupScope,
4
+ Control,
5
+ trackedValue,
6
+ } from "@astroapps/controls";
7
+ import {
8
+ ControlDefinition,
9
+ ControlDefinitionType,
10
+ DataControlDefinition,
11
+ DataRenderType,
12
+ GroupedControlsDefinition,
13
+ GroupRenderType,
14
+ isDataControl,
15
+ } from "./controlDefinition";
16
+ import { SchemaDataNode } from "./schemaDataNode";
17
+ import { FormNode } from "./formNode";
18
+ import { createScopedComputed } from "./util";
19
+ import { SchemaInterface } from "./schemaInterface";
20
+ import { FieldOption } from "./schemaField";
21
+ import { FormStateNode } from "./formStateNode";
22
+ import { groupedControl } from "./controlBuilder";
23
+
24
+ export type ChildResolverFunc = (c: FormStateNode) => ChildNodeSpec[];
25
+
26
+ export interface ChildNodeSpec {
27
+ childKey: string | number;
28
+ create: (scope: CleanupScope, meta: Record<string, any>) => ChildNodeInit;
29
+ }
30
+
31
+ export interface ChildNodeInit {
32
+ definition?: ControlDefinition;
33
+ parent?: SchemaDataNode;
34
+ node?: FormNode | null;
35
+ variables?: (changes: ChangeListenerFunc<any>) => Record<string, any>;
36
+ resolveChildren?: ChildResolverFunc;
37
+ }
38
+
39
+ export function defaultResolveChildNodes(
40
+ formStateNode: FormStateNode,
41
+ ): ChildNodeSpec[] {
42
+ const {
43
+ resolved,
44
+ dataNode: data,
45
+ schemaInterface,
46
+ parent,
47
+ form: node,
48
+ } = formStateNode;
49
+ if (!node) return [];
50
+ const def = resolved.definition;
51
+ if (isDataControl(def)) {
52
+ if (!data) return [];
53
+ const type = def.renderOptions?.type;
54
+ if (type === DataRenderType.CheckList || type === DataRenderType.Radio) {
55
+ const n = node.getChildNodes();
56
+ if (n.length > 0 && resolved.fieldOptions) {
57
+ return resolved.fieldOptions.map((x) => ({
58
+ childKey: x.value?.toString(),
59
+ create: (scope, meta) => {
60
+ meta["fieldOptionValue"] = x.value;
61
+ const vars = createScopedComputed(scope, () => {
62
+ return {
63
+ option: x,
64
+ optionSelected: isOptionSelected(schemaInterface, x, data),
65
+ };
66
+ });
67
+ return {
68
+ definition: {
69
+ type: ControlDefinitionType.Group,
70
+ groupOptions: {
71
+ type: GroupRenderType.Contents,
72
+ },
73
+ } as GroupedControlsDefinition,
74
+ parent,
75
+ node,
76
+ variables: (changes) => ({
77
+ formData: trackedValue(vars, changes),
78
+ }),
79
+ };
80
+ },
81
+ }));
82
+ }
83
+ return [];
84
+ }
85
+ if (data.schema.field.collection && data.elementIndex == null)
86
+ return resolveArrayChildren(data, node);
87
+ }
88
+ return node.getChildNodes().map((x) => ({
89
+ childKey: x.id,
90
+ create: () => ({
91
+ node: x,
92
+ parent: data ?? parent,
93
+ definition: x.definition,
94
+ }),
95
+ }));
96
+ }
97
+
98
+ export function resolveArrayChildren(
99
+ data: SchemaDataNode,
100
+ node: FormNode,
101
+ adjustChild?: (elem: Control<any>, index: number) => Partial<ChildNodeInit>,
102
+ ): ChildNodeSpec[] {
103
+ const childNodes = node.getChildNodes();
104
+ const childCount = childNodes.length;
105
+ const singleChild = childCount === 1 ? childNodes[0] : null;
106
+ return data.control.as<any[]>().elements.map((x, i) => ({
107
+ childKey: x.uniqueId,
108
+ create: () => ({
109
+ definition: !childCount
110
+ ? ({
111
+ type: ControlDefinitionType.Data,
112
+ field: ".",
113
+ hideTitle: true,
114
+ renderOptions: { type: DataRenderType.Standard },
115
+ } as DataControlDefinition)
116
+ : singleChild
117
+ ? singleChild.definition
118
+ : groupedControl([]),
119
+ node: singleChild ?? node,
120
+ parent: data!.getChildElement(i),
121
+ ...(adjustChild?.(x, i) ?? {}),
122
+ }),
123
+ }));
124
+ }
125
+
126
+ function isOptionSelected(
127
+ schemaInterface: SchemaInterface,
128
+ option: FieldOption,
129
+ data: SchemaDataNode,
130
+ ) {
131
+ if (data.schema.field.collection) {
132
+ return !!data.control.as<any[] | undefined>().value?.includes(option.value);
133
+ }
134
+ return (
135
+ schemaInterface.compareValue(
136
+ data.schema.field,
137
+ data.control.value,
138
+ option.value,
139
+ ) === 0
140
+ );
141
+ }
@@ -283,6 +283,9 @@ export function mergeFields(
283
283
  value: any,
284
284
  newFields: SchemaField[],
285
285
  ): SchemaField[] {
286
+ if (name === "*") {
287
+ return newFields.reduce((af, x) => mergeField(x, af), fields);
288
+ }
286
289
  const withType = fields.map((x) =>
287
290
  x.isTypeField ? addFieldOption(x, name, value) : x,
288
291
  );
@@ -186,3 +186,15 @@ export function hideDisplayOnly(
186
186
  schemaInterface.isEmptyValue(context.schema.field, context.control?.value)
187
187
  );
188
188
  }
189
+
190
+ export function getLoadingControl(data: Control<any>): Control<boolean> {
191
+ return ensureMetaValue(data, "loading", () => newControl(false));
192
+ }
193
+
194
+ export function getRefreshingControl(data: Control<any>): Control<boolean> {
195
+ return ensureMetaValue(data, "refreshing", () => newControl(false));
196
+ }
197
+
198
+ export function getHasMoreControl(data: Control<any>): Control<boolean> {
199
+ return ensureMetaValue(data, "hasMore", () => newControl(false));
200
+ }
package/src/schemaNode.ts CHANGED
@@ -9,7 +9,10 @@ import {
9
9
  export interface SchemaTreeLookup {
10
10
  getSchema(schemaId: string): SchemaNode | undefined;
11
11
 
12
- getSchemaTree(schemaId: string, additional?: SchemaField[]): SchemaTree | undefined;
12
+ getSchemaTree(
13
+ schemaId: string,
14
+ additional?: SchemaField[],
15
+ ): SchemaTree | undefined;
13
16
  }
14
17
 
15
18
  export abstract class SchemaTree {
@@ -63,6 +66,7 @@ export class SchemaNode {
63
66
  public field: SchemaField,
64
67
  public tree: SchemaTree,
65
68
  public parent?: SchemaNode,
69
+ private getChildFields?: () => SchemaField[],
66
70
  ) {}
67
71
 
68
72
  getSchema(schemaId: string): SchemaNode | undefined {
@@ -70,6 +74,7 @@ export class SchemaNode {
70
74
  }
71
75
 
72
76
  getUnresolvedFields(): SchemaField[] {
77
+ if (this.getChildFields) return this.getChildFields();
73
78
  return isCompoundField(this.field) ? this.field.children : [];
74
79
  }
75
80
 
@@ -228,6 +233,54 @@ export function schemaForFieldPath(
228
233
  return schema;
229
234
  }
230
235
 
236
+ export function schemaForDataPath(
237
+ fieldPath: string[],
238
+ schema: SchemaNode,
239
+ ): DataPathNode {
240
+ let i = 0;
241
+ let element = schema.field.collection;
242
+ while (i < fieldPath.length) {
243
+ const nextField = fieldPath[i];
244
+ let childNode: SchemaNode | undefined;
245
+ if (nextField == ".") {
246
+ i++;
247
+ continue;
248
+ } else if (nextField == "..") {
249
+ if (element) {
250
+ element = false;
251
+ i++;
252
+ continue;
253
+ }
254
+ childNode = schema.parent;
255
+ } else {
256
+ childNode = schema.getChildNode(nextField);
257
+ }
258
+ if (!childNode) {
259
+ childNode = createSchemaNode(
260
+ missingField(nextField),
261
+ schema.tree,
262
+ schema,
263
+ );
264
+ } else {
265
+ element = childNode.field.collection;
266
+ }
267
+ schema = childNode;
268
+ i++;
269
+ }
270
+ return { node: schema, element: !!element };
271
+ }
272
+
273
+ export function getParentDataPath({
274
+ node,
275
+ element,
276
+ }: DataPathNode): DataPathNode | undefined {
277
+ if (element) return { node, element: false };
278
+ const parent = node.parent;
279
+ return parent
280
+ ? { node: parent, element: !!parent.field.collection }
281
+ : undefined;
282
+ }
283
+
231
284
  export function getSchemaNodePath(node: SchemaNode) {
232
285
  const paths: string[] = [];
233
286
  let curNode: SchemaNode | undefined = node;
@@ -283,3 +336,8 @@ export function relativeSegmentPath(
283
336
 
284
337
  return "../".repeat(upLevels) + downPath;
285
338
  }
339
+
340
+ export interface DataPathNode {
341
+ node: SchemaNode;
342
+ element: boolean;
343
+ }
@@ -30,3 +30,22 @@ export interface DateValidator extends SchemaValidator {
30
30
  fixedDate?: string | null;
31
31
  daysFromCurrent?: number | null;
32
32
  }
33
+
34
+ export function jsonataValidator(expr: string): JsonataValidator {
35
+ return { type: ValidatorType.Jsonata, expression: expr };
36
+ }
37
+
38
+ export function dateValidator(
39
+ comparison: DateComparison,
40
+ fixedDate?: string | null,
41
+ daysFromCurrent?: number | null,
42
+ ): DateValidator {
43
+ return { type: ValidatorType.Date, comparison, fixedDate, daysFromCurrent };
44
+ }
45
+
46
+ export function lengthValidator(
47
+ min?: number | null,
48
+ max?: number | null,
49
+ ): LengthValidator {
50
+ return { type: ValidatorType.Length, min, max };
51
+ }
package/src/validators.ts CHANGED
@@ -6,13 +6,10 @@ import {
6
6
  SchemaValidator,
7
7
  ValidatorType,
8
8
  } from "./schemaValidator";
9
- import {
10
- ControlDefinition,
11
- DataControlDefinition,
12
- isDataControl,
13
- } from "./controlDefinition";
9
+ import { ControlDefinition, isDataControl } from "./controlDefinition";
14
10
  import { SchemaDataNode } from "./schemaDataNode";
15
11
  import {
12
+ CleanupScope,
16
13
  Control,
17
14
  ControlChange,
18
15
  createCleanupScope,
@@ -21,12 +18,10 @@ import {
21
18
  } from "@astroapps/controls";
22
19
  import { ValidationMessageType } from "./schemaField";
23
20
  import { SchemaInterface } from "./schemaInterface";
24
-
25
- import { FormContextOptions } from "./formState";
26
- import { FormNode } from "./formNode";
27
21
  import { jsonataEval } from "./evalExpression";
28
22
  import { ExpressionType } from "./entityExpression";
29
23
  import { createScopedComputed } from "./util";
24
+ import { FormStateBaseImpl, VariablesFunc } from "./formStateNode";
30
25
 
31
26
  export interface ValidationEvalContext {
32
27
  addSync(validate: (value: unknown) => string | undefined | null): void;
@@ -35,7 +30,7 @@ export interface ValidationEvalContext {
35
30
  parentData: SchemaDataNode;
36
31
  data: SchemaDataNode;
37
32
  schemaInterface: SchemaInterface;
38
- formContext: Control<FormContextOptions>;
33
+ variables?: VariablesFunc;
39
34
  runAsync(af: () => void): void;
40
35
  }
41
36
 
@@ -60,7 +55,7 @@ export const jsonataValidator: ValidatorEval<JsonataValidator> = (
60
55
  context.data.control.setError("jsonata", v?.toString());
61
56
  },
62
57
  schemaInterface: context.schemaInterface,
63
- variables: context.formContext.fields.variables,
58
+ variables: context.variables,
64
59
  runAsync: context.runAsync,
65
60
  },
66
61
  );
@@ -155,12 +150,14 @@ export function createValidators(
155
150
  context.addSync((v) => {
156
151
  const field = context.data.schema.field;
157
152
  return schemaInterface.isEmptyValue(field, v)
158
- ? schemaInterface.validationMessageText(
159
- field,
160
- ValidationMessageType.NotEmpty,
161
- false,
162
- true,
163
- )
153
+ ? def.requiredErrorText
154
+ ? def.requiredErrorText
155
+ : schemaInterface.validationMessageText(
156
+ field,
157
+ ValidationMessageType.NotEmpty,
158
+ false,
159
+ true,
160
+ )
164
161
  : null;
165
162
  });
166
163
  }
@@ -169,19 +166,16 @@ export function createValidators(
169
166
  }
170
167
 
171
168
  export function setupValidation(
172
- controlImpl: Control<FormContextOptions>,
169
+ scope: CleanupScope,
170
+ variables: VariablesFunc | undefined,
173
171
  definition: ControlDefinition,
174
172
  dataNode: Control<SchemaDataNode | undefined>,
175
173
  schemaInterface: SchemaInterface,
176
174
  parent: SchemaDataNode,
177
- formNode: FormNode,
178
- hidden: Control<boolean>,
175
+ visible: Control<boolean | null>,
179
176
  runAsync: (af: () => void) => void,
180
177
  ) {
181
- const validationEnabled = createScopedComputed(
182
- controlImpl,
183
- () => !hidden.value,
184
- );
178
+ const validationEnabled = createScopedComputed(scope, () => !!visible.value);
185
179
  const validatorsScope = createCleanupScope();
186
180
  createEffect(
187
181
  () => {
@@ -189,7 +183,7 @@ export function setupValidation(
189
183
  const dn = dataNode.value;
190
184
  if (dn) {
191
185
  let syncValidations: ((v: unknown) => string | undefined | null)[] = [];
192
- createValidators(formNode.definition, {
186
+ createValidators(definition, {
193
187
  data: dn,
194
188
  parentData: parent,
195
189
  validationEnabled,
@@ -200,7 +194,7 @@ export function setupValidation(
200
194
  addCleanup(cleanup: () => void) {
201
195
  validatorsScope.addCleanup(cleanup);
202
196
  },
203
- formContext: controlImpl,
197
+ variables,
204
198
  runAsync,
205
199
  });
206
200
 
@@ -224,6 +218,6 @@ export function setupValidation(
224
218
  }
225
219
  },
226
220
  (c) => {},
227
- controlImpl,
221
+ scope,
228
222
  );
229
223
  }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "esModuleInterop": true,
11
+ "declaration": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "jsx": "react",
17
+ "outDir": "lib"
18
+ },
19
+ "include": ["test/**/*.ts", "test/**/*.tsx"],
20
+ "exclude": ["node_modules", "lib"]
21
+ }
@@ -1,48 +0,0 @@
1
- import { FormNode } from "./formNode";
2
- import { SchemaDataNode } from "./schemaDataNode";
3
- import { ControlDefinition } from "./controlDefinition";
4
- import { SchemaInterface } from "./schemaInterface";
5
- import { FieldOption } from "./schemaField";
6
- import { Control } from "@astroapps/controls";
7
- import { ExpressionEval, ExpressionEvalContext } from "./evalExpression";
8
- import { EntityExpression } from "./entityExpression";
9
- export interface ControlState {
10
- definition: ControlDefinition;
11
- schemaInterface: SchemaInterface;
12
- dataNode?: SchemaDataNode | undefined;
13
- display?: string;
14
- stateId?: string;
15
- style?: object;
16
- layoutStyle?: object;
17
- allowedOptions?: any[];
18
- readonly: boolean;
19
- hidden: boolean;
20
- disabled: boolean;
21
- clearHidden: boolean;
22
- variables: Record<string, any>;
23
- meta: Control<Record<string, any>>;
24
- }
25
- export interface FormContextOptions {
26
- readonly?: boolean | null;
27
- hidden?: boolean | null;
28
- disabled?: boolean | null;
29
- clearHidden?: boolean;
30
- stateKey?: string;
31
- variables?: Record<string, any>;
32
- }
33
- /**
34
- * Interface representing the form context data.
35
- */
36
- export interface FormContextData {
37
- option?: FieldOption;
38
- optionSelected?: boolean;
39
- }
40
- export interface FormState {
41
- getControlState(parent: SchemaDataNode, formNode: FormNode, context: FormContextOptions, runAsync: (af: () => void) => void): ControlState;
42
- cleanup(): void;
43
- evalExpression(expr: EntityExpression, context: ExpressionEvalContext): void;
44
- getExistingControlState(parent: SchemaDataNode, formNode: FormNode, stateKey?: string): ControlState | undefined;
45
- }
46
- export declare function getControlStateId(parent: SchemaDataNode, formNode: FormNode, stateKey?: string): string;
47
- export declare function createFormState(schemaInterface: SchemaInterface, evaluators?: Record<string, ExpressionEval<any>>): FormState;
48
- export declare function createOverrideProxy<A extends object, B extends Record<string, any>>(proxyFor: A, handlers: Control<B>): A;