@getodk/xforms-engine 0.1.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.
Files changed (208) hide show
  1. package/README.md +44 -0
  2. package/dist/.vite/manifest.json +7 -0
  3. package/dist/XFormDOM.d.ts +31 -0
  4. package/dist/XFormDataType.d.ts +26 -0
  5. package/dist/XFormDefinition.d.ts +14 -0
  6. package/dist/body/BodyDefinition.d.ts +52 -0
  7. package/dist/body/BodyElementDefinition.d.ts +32 -0
  8. package/dist/body/RepeatDefinition.d.ts +15 -0
  9. package/dist/body/UnsupportedBodyElementDefinition.d.ts +10 -0
  10. package/dist/body/control/ControlDefinition.d.ts +16 -0
  11. package/dist/body/control/InputDefinition.d.ts +5 -0
  12. package/dist/body/control/select/ItemDefinition.d.ts +13 -0
  13. package/dist/body/control/select/ItemsetDefinition.d.ts +16 -0
  14. package/dist/body/control/select/ItemsetNodesetContext.d.ts +11 -0
  15. package/dist/body/control/select/ItemsetNodesetExpression.d.ts +5 -0
  16. package/dist/body/control/select/ItemsetValueExpression.d.ts +6 -0
  17. package/dist/body/control/select/SelectDefinition.d.ts +23 -0
  18. package/dist/body/group/BaseGroupDefinition.d.ts +46 -0
  19. package/dist/body/group/LogicalGroupDefinition.d.ts +6 -0
  20. package/dist/body/group/PresentationGroupDefinition.d.ts +11 -0
  21. package/dist/body/group/RepeatGroupDefinition.d.ts +12 -0
  22. package/dist/body/group/StructuralGroupDefinition.d.ts +6 -0
  23. package/dist/body/text/HintDefinition.d.ts +11 -0
  24. package/dist/body/text/LabelDefinition.d.ts +20 -0
  25. package/dist/body/text/TextElementDefinition.d.ts +32 -0
  26. package/dist/body/text/TextElementOutputPart.d.ts +12 -0
  27. package/dist/body/text/TextElementPart.d.ts +12 -0
  28. package/dist/body/text/TextElementReferencePart.d.ts +6 -0
  29. package/dist/body/text/TextElementStaticPart.d.ts +6 -0
  30. package/dist/client/BaseNode.d.ts +138 -0
  31. package/dist/client/EngineConfig.d.ts +78 -0
  32. package/dist/client/FormLanguage.d.ts +63 -0
  33. package/dist/client/GroupNode.d.ts +24 -0
  34. package/dist/client/OpaqueReactiveObjectFactory.d.ts +70 -0
  35. package/dist/client/RepeatInstanceNode.d.ts +28 -0
  36. package/dist/client/RepeatRangeNode.d.ts +94 -0
  37. package/dist/client/RootNode.d.ts +31 -0
  38. package/dist/client/SelectNode.d.ts +60 -0
  39. package/dist/client/StringNode.d.ts +41 -0
  40. package/dist/client/SubtreeNode.d.ts +52 -0
  41. package/dist/client/TextRange.d.ts +55 -0
  42. package/dist/client/hierarchy.d.ts +48 -0
  43. package/dist/client/index.d.ts +11 -0
  44. package/dist/client/node-types.d.ts +1 -0
  45. package/dist/expression/DependencyContext.d.ts +12 -0
  46. package/dist/expression/DependentExpression.d.ts +43 -0
  47. package/dist/index.d.ts +16 -0
  48. package/dist/index.js +37622 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/instance/Group.d.ts +31 -0
  51. package/dist/instance/RepeatInstance.d.ts +60 -0
  52. package/dist/instance/RepeatRange.d.ts +81 -0
  53. package/dist/instance/Root.d.ts +70 -0
  54. package/dist/instance/SelectField.d.ts +45 -0
  55. package/dist/instance/StringField.d.ts +39 -0
  56. package/dist/instance/Subtree.d.ts +30 -0
  57. package/dist/instance/abstract/DescendantNode.d.ts +76 -0
  58. package/dist/instance/abstract/InstanceNode.d.ts +107 -0
  59. package/dist/instance/children.d.ts +2 -0
  60. package/dist/instance/hierarchy.d.ts +12 -0
  61. package/dist/instance/identity.d.ts +7 -0
  62. package/dist/instance/index.d.ts +8 -0
  63. package/dist/instance/internal-api/EvaluationContext.d.ts +34 -0
  64. package/dist/instance/internal-api/InstanceConfig.d.ts +8 -0
  65. package/dist/instance/internal-api/SubscribableDependency.d.ts +59 -0
  66. package/dist/instance/internal-api/TranslationContext.d.ts +4 -0
  67. package/dist/instance/internal-api/ValueContext.d.ts +22 -0
  68. package/dist/instance/resource.d.ts +10 -0
  69. package/dist/instance/text/FormattedTextStub.d.ts +1 -0
  70. package/dist/instance/text/TextChunk.d.ts +11 -0
  71. package/dist/instance/text/TextRange.d.ts +10 -0
  72. package/dist/lib/dom/query.d.ts +20 -0
  73. package/dist/lib/reactivity/createChildrenState.d.ts +36 -0
  74. package/dist/lib/reactivity/createComputedExpression.d.ts +12 -0
  75. package/dist/lib/reactivity/createSelectItems.d.ts +16 -0
  76. package/dist/lib/reactivity/createValueState.d.ts +44 -0
  77. package/dist/lib/reactivity/materializeCurrentStateChildren.d.ts +18 -0
  78. package/dist/lib/reactivity/node-state/createClientState.d.ts +9 -0
  79. package/dist/lib/reactivity/node-state/createCurrentState.d.ts +6 -0
  80. package/dist/lib/reactivity/node-state/createEngineState.d.ts +5 -0
  81. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +22 -0
  82. package/dist/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.d.ts +6 -0
  83. package/dist/lib/reactivity/node-state/createSpecifiedState.d.ts +139 -0
  84. package/dist/lib/reactivity/node-state/representations.d.ts +25 -0
  85. package/dist/lib/reactivity/scope.d.ts +23 -0
  86. package/dist/lib/reactivity/text/createFieldHint.d.ts +5 -0
  87. package/dist/lib/reactivity/text/createNodeLabel.d.ts +5 -0
  88. package/dist/lib/reactivity/text/createTextRange.d.ts +19 -0
  89. package/dist/lib/reactivity/types.d.ts +21 -0
  90. package/dist/lib/unique-id.d.ts +27 -0
  91. package/dist/lib/xpath/analysis.d.ts +22 -0
  92. package/dist/model/BindComputation.d.ts +30 -0
  93. package/dist/model/BindDefinition.d.ts +31 -0
  94. package/dist/model/BindElement.d.ts +6 -0
  95. package/dist/model/DescendentNodeDefinition.d.ts +25 -0
  96. package/dist/model/ModelBindMap.d.ts +15 -0
  97. package/dist/model/ModelDefinition.d.ts +10 -0
  98. package/dist/model/NodeDefinition.d.ts +74 -0
  99. package/dist/model/RepeatInstanceDefinition.d.ts +15 -0
  100. package/dist/model/RepeatSequenceDefinition.d.ts +19 -0
  101. package/dist/model/RepeatTemplateDefinition.d.ts +29 -0
  102. package/dist/model/RootDefinition.d.ts +24 -0
  103. package/dist/model/SubtreeDefinition.d.ts +14 -0
  104. package/dist/model/ValueNodeDefinition.d.ts +15 -0
  105. package/dist/solid.js +37273 -0
  106. package/dist/solid.js.map +1 -0
  107. package/package.json +87 -0
  108. package/src/XFormDOM.ts +224 -0
  109. package/src/XFormDataType.ts +64 -0
  110. package/src/XFormDefinition.ts +40 -0
  111. package/src/body/BodyDefinition.ts +202 -0
  112. package/src/body/BodyElementDefinition.ts +62 -0
  113. package/src/body/RepeatDefinition.ts +54 -0
  114. package/src/body/UnsupportedBodyElementDefinition.ts +17 -0
  115. package/src/body/control/ControlDefinition.ts +42 -0
  116. package/src/body/control/InputDefinition.ts +9 -0
  117. package/src/body/control/select/ItemDefinition.ts +31 -0
  118. package/src/body/control/select/ItemsetDefinition.ts +36 -0
  119. package/src/body/control/select/ItemsetNodesetContext.ts +26 -0
  120. package/src/body/control/select/ItemsetNodesetExpression.ts +8 -0
  121. package/src/body/control/select/ItemsetValueExpression.ts +11 -0
  122. package/src/body/control/select/SelectDefinition.ts +74 -0
  123. package/src/body/group/BaseGroupDefinition.ts +137 -0
  124. package/src/body/group/LogicalGroupDefinition.ts +11 -0
  125. package/src/body/group/PresentationGroupDefinition.ts +28 -0
  126. package/src/body/group/RepeatGroupDefinition.ts +91 -0
  127. package/src/body/group/StructuralGroupDefinition.ts +11 -0
  128. package/src/body/text/HintDefinition.ts +26 -0
  129. package/src/body/text/LabelDefinition.ts +54 -0
  130. package/src/body/text/TextElementDefinition.ts +97 -0
  131. package/src/body/text/TextElementOutputPart.ts +27 -0
  132. package/src/body/text/TextElementPart.ts +31 -0
  133. package/src/body/text/TextElementReferencePart.ts +21 -0
  134. package/src/body/text/TextElementStaticPart.ts +26 -0
  135. package/src/client/BaseNode.ts +180 -0
  136. package/src/client/EngineConfig.ts +83 -0
  137. package/src/client/FormLanguage.ts +77 -0
  138. package/src/client/GroupNode.ts +33 -0
  139. package/src/client/OpaqueReactiveObjectFactory.ts +100 -0
  140. package/src/client/README.md +39 -0
  141. package/src/client/RepeatInstanceNode.ts +41 -0
  142. package/src/client/RepeatRangeNode.ts +100 -0
  143. package/src/client/RootNode.ts +36 -0
  144. package/src/client/SelectNode.ts +69 -0
  145. package/src/client/StringNode.ts +46 -0
  146. package/src/client/SubtreeNode.ts +57 -0
  147. package/src/client/TextRange.ts +63 -0
  148. package/src/client/hierarchy.ts +63 -0
  149. package/src/client/index.ts +29 -0
  150. package/src/client/node-types.ts +10 -0
  151. package/src/expression/DependencyContext.ts +53 -0
  152. package/src/expression/DependentExpression.ts +102 -0
  153. package/src/index.ts +35 -0
  154. package/src/instance/Group.ts +82 -0
  155. package/src/instance/RepeatInstance.ts +164 -0
  156. package/src/instance/RepeatRange.ts +214 -0
  157. package/src/instance/Root.ts +264 -0
  158. package/src/instance/SelectField.ts +204 -0
  159. package/src/instance/StringField.ts +93 -0
  160. package/src/instance/Subtree.ts +79 -0
  161. package/src/instance/abstract/DescendantNode.ts +182 -0
  162. package/src/instance/abstract/InstanceNode.ts +257 -0
  163. package/src/instance/children.ts +52 -0
  164. package/src/instance/hierarchy.ts +54 -0
  165. package/src/instance/identity.ts +11 -0
  166. package/src/instance/index.ts +37 -0
  167. package/src/instance/internal-api/EvaluationContext.ts +41 -0
  168. package/src/instance/internal-api/InstanceConfig.ts +9 -0
  169. package/src/instance/internal-api/SubscribableDependency.ts +61 -0
  170. package/src/instance/internal-api/TranslationContext.ts +5 -0
  171. package/src/instance/internal-api/ValueContext.ts +27 -0
  172. package/src/instance/resource.ts +75 -0
  173. package/src/instance/text/FormattedTextStub.ts +8 -0
  174. package/src/instance/text/TextChunk.ts +20 -0
  175. package/src/instance/text/TextRange.ts +23 -0
  176. package/src/lib/dom/query.ts +49 -0
  177. package/src/lib/reactivity/createChildrenState.ts +60 -0
  178. package/src/lib/reactivity/createComputedExpression.ts +114 -0
  179. package/src/lib/reactivity/createSelectItems.ts +163 -0
  180. package/src/lib/reactivity/createValueState.ts +258 -0
  181. package/src/lib/reactivity/materializeCurrentStateChildren.ts +121 -0
  182. package/src/lib/reactivity/node-state/createClientState.ts +51 -0
  183. package/src/lib/reactivity/node-state/createCurrentState.ts +27 -0
  184. package/src/lib/reactivity/node-state/createEngineState.ts +18 -0
  185. package/src/lib/reactivity/node-state/createSharedNodeState.ts +79 -0
  186. package/src/lib/reactivity/node-state/createSpecifiedPropertyDescriptor.ts +85 -0
  187. package/src/lib/reactivity/node-state/createSpecifiedState.ts +229 -0
  188. package/src/lib/reactivity/node-state/representations.ts +64 -0
  189. package/src/lib/reactivity/scope.ts +106 -0
  190. package/src/lib/reactivity/text/createFieldHint.ts +16 -0
  191. package/src/lib/reactivity/text/createNodeLabel.ts +16 -0
  192. package/src/lib/reactivity/text/createTextRange.ts +155 -0
  193. package/src/lib/reactivity/types.ts +27 -0
  194. package/src/lib/unique-id.ts +34 -0
  195. package/src/lib/xpath/analysis.ts +241 -0
  196. package/src/model/BindComputation.ts +88 -0
  197. package/src/model/BindDefinition.ts +104 -0
  198. package/src/model/BindElement.ts +8 -0
  199. package/src/model/DescendentNodeDefinition.ts +56 -0
  200. package/src/model/ModelBindMap.ts +71 -0
  201. package/src/model/ModelDefinition.ts +19 -0
  202. package/src/model/NodeDefinition.ts +146 -0
  203. package/src/model/RepeatInstanceDefinition.ts +39 -0
  204. package/src/model/RepeatSequenceDefinition.ts +53 -0
  205. package/src/model/RepeatTemplateDefinition.ts +150 -0
  206. package/src/model/RootDefinition.ts +121 -0
  207. package/src/model/SubtreeDefinition.ts +50 -0
  208. package/src/model/ValueNodeDefinition.ts +39 -0
@@ -0,0 +1,53 @@
1
+ import type { AnyDependentExpression } from './DependentExpression.ts';
2
+
3
+ export abstract class DependencyContext {
4
+ abstract get parentReference(): string | null;
5
+ abstract get reference(): string | null;
6
+
7
+ protected readonly dependentExpressions = new Set<AnyDependentExpression>();
8
+
9
+ protected dependencyExpressionsCache: ReadonlySet<string> | null = null;
10
+
11
+ get dependencyExpressions(): ReadonlySet<string> {
12
+ let { dependencyExpressionsCache } = this;
13
+
14
+ if (dependencyExpressionsCache == null) {
15
+ dependencyExpressionsCache = new Set(
16
+ Array.from(this.dependentExpressions).flatMap((expression) => {
17
+ return Array.from(expression.dependencyReferences);
18
+ })
19
+ );
20
+ this.dependencyExpressionsCache = dependencyExpressionsCache;
21
+ }
22
+
23
+ return dependencyExpressionsCache;
24
+ }
25
+
26
+ get isTranslated(): boolean {
27
+ return this._isTranslated;
28
+ }
29
+
30
+ // Note: this is a bit of type system "cleverness" that helped to prevent a
31
+ // bug repeatedly encountered in earlier prototyping. The default value is
32
+ // false (currently backed by _isTranslated; this note should be updated if
33
+ // that changes). Its value can be read at any time, but it may only be
34
+ // overridden with `true`. So if a `DependencyContext` is established as
35
+ // dependent on translations by one `DependentExpression`, another expression
36
+ // which does not depend on translations cannot override that.
37
+ //
38
+ // This doesn't deserve so much explanation in its own right, but it's worth
39
+ // calling out here as a pattern we may find valuable in other cases where
40
+ // interfaces allow writes from outside (which I've generally tried to avoid,
41
+ // or significantly restrict, to avoid bugs like the one described above).
42
+ set isTranslated(value: true) {
43
+ this._isTranslated = value;
44
+ }
45
+
46
+ // Update note on `set isTranslated` if this internal storage changes.
47
+ protected _isTranslated = false;
48
+
49
+ registerDependentExpression(expression: AnyDependentExpression): void {
50
+ this.dependencyExpressionsCache = null;
51
+ this.dependentExpressions.add(expression);
52
+ }
53
+ }
@@ -0,0 +1,102 @@
1
+ import type { XFormsXPathEvaluator } from '@getodk/xpath';
2
+ import { getNodesetDependencies, isItextFunctionCalled } from '../lib/xpath/analysis.ts';
3
+ import type { DependencyContext } from './DependencyContext.ts';
4
+
5
+ const evaluatorMethodsByResultType = {
6
+ boolean: 'evaluateBoolean',
7
+ nodes: 'evaluateNodes',
8
+ string: 'evaluateString',
9
+ } as const;
10
+
11
+ type EvaluatorMethodsByResultType = typeof evaluatorMethodsByResultType;
12
+
13
+ export type DependentExpressionResultType = keyof EvaluatorMethodsByResultType;
14
+
15
+ export type DependentExpressionEvaluatorMethod<Type extends DependentExpressionResultType> =
16
+ EvaluatorMethodsByResultType[Type];
17
+
18
+ export type DependentExpressionResult<Type extends DependentExpressionResultType> = ReturnType<
19
+ XFormsXPathEvaluator[DependentExpressionEvaluatorMethod<Type>]
20
+ >;
21
+
22
+ interface SemanticDependencyOptions {
23
+ /**
24
+ * @default false
25
+ */
26
+ readonly parentContext?: boolean | undefined;
27
+
28
+ /**
29
+ * @default false
30
+ */
31
+ readonly translations?: boolean | undefined;
32
+ }
33
+
34
+ interface DependentExpressionOptions {
35
+ /**
36
+ * @default false
37
+ */
38
+ readonly ignoreContextReference?: boolean;
39
+
40
+ /**
41
+ * @default true
42
+ */
43
+ readonly ignoreNullExpressions?: boolean;
44
+
45
+ readonly semanticDependencies?: SemanticDependencyOptions;
46
+ }
47
+
48
+ export class DependentExpression<Type extends DependentExpressionResultType> {
49
+ readonly dependencyReferences: ReadonlySet<string> = new Set();
50
+ readonly isTranslated: boolean = false;
51
+ readonly evaluatorMethod: DependentExpressionEvaluatorMethod<Type>;
52
+
53
+ constructor(
54
+ context: DependencyContext,
55
+ readonly resultType: Type,
56
+ readonly expression: string,
57
+ options: DependentExpressionOptions = {}
58
+ ) {
59
+ this.evaluatorMethod = evaluatorMethodsByResultType[resultType];
60
+
61
+ const {
62
+ ignoreContextReference = false,
63
+ ignoreNullExpressions = true,
64
+ semanticDependencies = {
65
+ parentContext: false,
66
+ translations: false,
67
+ },
68
+ } = options;
69
+
70
+ const dependencyReferences = new Set<string>(
71
+ getNodesetDependencies(expression, {
72
+ contextReference: context.reference,
73
+ ignoreContextReference,
74
+ ignoreNullExpressions,
75
+ })
76
+ );
77
+
78
+ const parentDependency = semanticDependencies.parentContext ? context.parentReference : null;
79
+
80
+ if (parentDependency != null) {
81
+ dependencyReferences.add(parentDependency);
82
+ }
83
+
84
+ this.dependencyReferences = dependencyReferences;
85
+
86
+ const isTranslated = semanticDependencies.translations && isItextFunctionCalled(expression);
87
+
88
+ if (isTranslated) {
89
+ this.isTranslated = true;
90
+ context.isTranslated = true;
91
+ }
92
+
93
+ context.registerDependentExpression(this);
94
+ }
95
+
96
+ toString(): string | null {
97
+ return this.expression;
98
+ }
99
+ }
100
+
101
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
+ export type AnyDependentExpression = DependentExpression<any>;
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { InitializeForm } from './index.ts';
2
+ import { initializeForm as engine__initializeForm } from './instance/index.ts';
3
+
4
+ export const initializeForm: InitializeForm = engine__initializeForm;
5
+
6
+ export type * from './client/EngineConfig.ts';
7
+ export type * from './client/FormLanguage.ts';
8
+ export type * from './client/GroupNode.ts';
9
+ export type * from './client/OpaqueReactiveObjectFactory.ts';
10
+ export type * from './client/RepeatInstanceNode.ts';
11
+ export type * from './client/RepeatRangeNode.ts';
12
+ export type * from './client/RootNode.ts';
13
+ export type * from './client/SelectNode.ts';
14
+ export type * from './client/StringNode.ts';
15
+ export type * from './client/SubtreeNode.ts';
16
+ export type * from './client/TextRange.ts';
17
+ export type {
18
+ AnyChildNode,
19
+ AnyLeafNode,
20
+ AnyNode,
21
+ AnyParentNode,
22
+ GeneralChildNode,
23
+ GeneralParentNode,
24
+ } from './client/hierarchy.ts';
25
+ export type * from './client/index.ts';
26
+
27
+ // TODO: notwithstanding potential conflicts with parallel work on `web-forms`
28
+ // (former `ui-vue`), these are the last remaining references **outside of
29
+ // `xforms-engine`** to anything besides /client/* and the `initializeForm`
30
+ // entrypoint implementation. We'll refine the various `definition` types in due
31
+ // time.
32
+ export type {
33
+ AnySelectDefinition,
34
+ SelectDefinition,
35
+ } from './body/control/select/SelectDefinition.ts';
@@ -0,0 +1,82 @@
1
+ import type { Accessor } from 'solid-js';
2
+ import type { GroupDefinition, GroupNode } from '../client/GroupNode.ts';
3
+ import type { TextRange } from '../index.ts';
4
+ import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts';
5
+ import { createChildrenState } from '../lib/reactivity/createChildrenState.ts';
6
+ import type { MaterializedChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts';
7
+ import { materializeCurrentStateChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts';
8
+ import type { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts';
9
+ import type { EngineState } from '../lib/reactivity/node-state/createEngineState.ts';
10
+ import type { SharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
11
+ import { createSharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
12
+ import { createNodeLabel } from '../lib/reactivity/text/createNodeLabel.ts';
13
+ import type { DescendantNodeSharedStateSpec } from './abstract/DescendantNode.ts';
14
+ import { DescendantNode } from './abstract/DescendantNode.ts';
15
+ import { buildChildren } from './children.ts';
16
+ import type { GeneralChildNode, GeneralParentNode } from './hierarchy.ts';
17
+ import type { NodeID } from './identity.ts';
18
+ import type { EvaluationContext } from './internal-api/EvaluationContext.ts';
19
+ import type { SubscribableDependency } from './internal-api/SubscribableDependency.ts';
20
+
21
+ // prettier-ignore
22
+ interface GroupStateSpec extends DescendantNodeSharedStateSpec {
23
+ readonly label: Accessor<TextRange<'label'> | null>;
24
+ readonly hint: null;
25
+ readonly children: Accessor<readonly NodeID[]>;
26
+ readonly valueOptions: null;
27
+ readonly value: null;
28
+ }
29
+
30
+ export class Group
31
+ extends DescendantNode<GroupDefinition, GroupStateSpec, GeneralChildNode>
32
+ implements GroupNode, EvaluationContext, SubscribableDependency
33
+ {
34
+ private readonly childrenState: ChildrenState<GeneralChildNode>;
35
+
36
+ // InstanceNode
37
+ protected readonly state: SharedNodeState<GroupStateSpec>;
38
+ protected override engineState: EngineState<GroupStateSpec>;
39
+
40
+ // GroupNode
41
+ readonly currentState: MaterializedChildren<CurrentState<GroupStateSpec>, GeneralChildNode>;
42
+
43
+ readonly nodeType = 'group';
44
+
45
+ constructor(parent: GeneralParentNode, definition: GroupDefinition) {
46
+ super(parent, definition);
47
+
48
+ const childrenState = createChildrenState<Group, GeneralChildNode>(this);
49
+
50
+ this.childrenState = childrenState;
51
+
52
+ const state = createSharedNodeState(
53
+ this.scope,
54
+ {
55
+ ...this.buildSharedStateSpec(parent, definition),
56
+
57
+ label: createNodeLabel(this, definition),
58
+ hint: null,
59
+ children: childrenState.childIds,
60
+ valueOptions: null,
61
+ value: null,
62
+ },
63
+ {
64
+ clientStateFactory: this.engineConfig.stateFactory,
65
+ }
66
+ );
67
+
68
+ this.state = state;
69
+ this.engineState = state.engineState;
70
+ this.currentState = materializeCurrentStateChildren(state.currentState, childrenState);
71
+
72
+ childrenState.setChildren(buildChildren(this));
73
+ }
74
+
75
+ protected computeReference(parent: GeneralParentNode): string {
76
+ return this.computeChildStepReference(parent);
77
+ }
78
+
79
+ getChildren(): readonly GeneralChildNode[] {
80
+ return this.childrenState.getChildren();
81
+ }
82
+ }
@@ -0,0 +1,164 @@
1
+ import type { Accessor } from 'solid-js';
2
+ import { createComputed, createSignal, on } from 'solid-js';
3
+ import type { RepeatDefinition, RepeatInstanceNode } from '../client/RepeatInstanceNode.ts';
4
+ import type { TextRange } from '../index.ts';
5
+ import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts';
6
+ import { createChildrenState } from '../lib/reactivity/createChildrenState.ts';
7
+ import type { MaterializedChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts';
8
+ import { materializeCurrentStateChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts';
9
+ import type { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts';
10
+ import type { EngineState } from '../lib/reactivity/node-state/createEngineState.ts';
11
+ import type { SharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
12
+ import { createSharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
13
+ import { createNodeLabel } from '../lib/reactivity/text/createNodeLabel.ts';
14
+ import type { RepeatRange } from './RepeatRange.ts';
15
+ import type { DescendantNodeSharedStateSpec } from './abstract/DescendantNode.ts';
16
+ import { DescendantNode } from './abstract/DescendantNode.ts';
17
+ import { buildChildren } from './children.ts';
18
+ import type { AnyChildNode, GeneralChildNode } from './hierarchy.ts';
19
+ import type { NodeID } from './identity.ts';
20
+ import type { EvaluationContext } from './internal-api/EvaluationContext.ts';
21
+ import type { SubscribableDependency } from './internal-api/SubscribableDependency.ts';
22
+
23
+ export type { RepeatDefinition };
24
+
25
+ interface RepeatInstanceStateSpec extends DescendantNodeSharedStateSpec {
26
+ readonly label: Accessor<TextRange<'label'> | null>;
27
+ readonly hint: null;
28
+ readonly children: Accessor<readonly NodeID[]>;
29
+ readonly valueOptions: null;
30
+ readonly value: null;
31
+ }
32
+
33
+ interface RepeatInstanceOptions {
34
+ readonly precedingPrimaryInstanceNode: Comment | Element;
35
+ readonly precedingInstance: RepeatInstance | null;
36
+ }
37
+
38
+ export class RepeatInstance
39
+ extends DescendantNode<RepeatDefinition, RepeatInstanceStateSpec, GeneralChildNode>
40
+ implements RepeatInstanceNode, EvaluationContext, SubscribableDependency
41
+ {
42
+ private readonly childrenState: ChildrenState<GeneralChildNode>;
43
+ private readonly currentIndex: Accessor<number>;
44
+
45
+ // InstanceNode
46
+ protected readonly state: SharedNodeState<RepeatInstanceStateSpec>;
47
+ protected override engineState: EngineState<RepeatInstanceStateSpec>;
48
+
49
+ // RepeatInstanceNode
50
+ readonly nodeType = 'repeat-instance';
51
+
52
+ readonly currentState: MaterializedChildren<
53
+ CurrentState<RepeatInstanceStateSpec>,
54
+ GeneralChildNode
55
+ >;
56
+
57
+ constructor(
58
+ override readonly parent: RepeatRange,
59
+ definition: RepeatDefinition,
60
+ options: RepeatInstanceOptions
61
+ ) {
62
+ super(parent, definition);
63
+
64
+ const childrenState = createChildrenState<RepeatInstance, GeneralChildNode>(this);
65
+
66
+ this.childrenState = childrenState;
67
+
68
+ options.precedingPrimaryInstanceNode.after(this.contextNode);
69
+
70
+ const { precedingInstance } = options;
71
+ const precedingIndex = precedingInstance?.currentIndex ?? (() => -1);
72
+ const initialIndex = precedingIndex() + 1;
73
+ const [currentIndex, setCurrentIndex] = createSignal(initialIndex);
74
+
75
+ this.currentIndex = currentIndex;
76
+
77
+ const state = createSharedNodeState(
78
+ this.scope,
79
+ {
80
+ ...this.buildSharedStateSpec(parent, definition),
81
+
82
+ label: createNodeLabel(this, definition),
83
+ hint: null,
84
+ children: childrenState.childIds,
85
+ valueOptions: null,
86
+ value: null,
87
+ },
88
+ {
89
+ clientStateFactory: this.engineConfig.stateFactory,
90
+ }
91
+ );
92
+
93
+ this.state = state;
94
+ this.engineState = state.engineState;
95
+ this.currentState = materializeCurrentStateChildren(state.currentState, childrenState);
96
+
97
+ // Maintain current index state, updating as the parent range's children
98
+ // state is changed. Notable Solid reactivity nuances:
99
+ //
100
+ // - `createComputed` is the Solid API which is explicitly called out for
101
+ // supporting performing reactive writes. It's also generally considered a
102
+ // "smell", but it seems the most appropriate for a first pass on this.
103
+ // - `on(..., { defer: true })` allows us to *synchronously* delay reactive
104
+ // index updates until after the full form tree is built, where this
105
+ // `RepeatInstance` is being constructed but it hasn't yet been appended
106
+ // to the parent range's reactive `children`.
107
+ // - the same logic for deferring reaction on form init should apply for
108
+ // adding new instances to a live form.
109
+ this.scope.runTask(() => {
110
+ // TODO: even as minimal as this currently is, maybe we should move this
111
+ // into a named function under src/lib/reactivity (for consistency with
112
+ // other reactive implementations of specific XForms semantics)?
113
+ const computeCurrentIndex = parent.getInstanceIndex.bind(parent, this);
114
+
115
+ createComputed(on(computeCurrentIndex, setCurrentIndex, { defer: true }));
116
+ });
117
+
118
+ childrenState.setChildren(buildChildren(this));
119
+ }
120
+
121
+ protected computeReference(parent: RepeatRange): string {
122
+ const currentPosition = this.currentIndex() + 1;
123
+
124
+ return `${parent.contextReference}[${currentPosition}]`;
125
+ }
126
+
127
+ protected override initializeContextNode(parentContextNode: Element, nodeName: string): Element {
128
+ return this.createContextNode(parentContextNode, nodeName);
129
+ }
130
+
131
+ override subscribe(): void {
132
+ super.subscribe();
133
+ this.currentIndex();
134
+ }
135
+
136
+ getChildren(): readonly GeneralChildNode[] {
137
+ return this.childrenState.getChildren();
138
+ }
139
+
140
+ /**
141
+ * Performs repeat instance node-specific removal logic, then general node
142
+ * removal logic, in that order:
143
+ *
144
+ * 1. At present, before any reactive state change is performed, the repeat
145
+ * instance's {@link contextNode} is removed from the primary instance's
146
+ * XML DOM backing store (which also removes any descendant nodes from that
147
+ * store, as a side effect). This behavior may become unnecessary if/when
148
+ * we phase out use of this XML DOM backing store. This should be peformed
149
+ * first, so that any following reactive logic which evaluates affected
150
+ * XPath expressions will be performed against a state consistent with the
151
+ * repeat instance's removal (and that of its XML DOM descendants).
152
+ *
153
+ * 2. Performs any remaining removal logic as defined in
154
+ * {@link DescendantNode.remove}.
155
+ *
156
+ * These removal steps **must also** occur before any update to the parent
157
+ * {@link RepeatRange}'s reactive children state.
158
+ */
159
+ override remove(this: AnyChildNode): void {
160
+ this.contextNode.remove();
161
+
162
+ super.remove();
163
+ }
164
+ }
@@ -0,0 +1,214 @@
1
+ import { insertAtIndex } from '@getodk/common/lib/array/insert.ts';
2
+ import type { Accessor } from 'solid-js';
3
+ import type { RepeatRangeNode } from '../client/RepeatRangeNode.ts';
4
+ import type { ChildrenState } from '../lib/reactivity/createChildrenState.ts';
5
+ import { createChildrenState } from '../lib/reactivity/createChildrenState.ts';
6
+ import type { MaterializedChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts';
7
+ import { materializeCurrentStateChildren } from '../lib/reactivity/materializeCurrentStateChildren.ts';
8
+ import type { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts';
9
+ import type { EngineState } from '../lib/reactivity/node-state/createEngineState.ts';
10
+ import type { SharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
11
+ import { createSharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
12
+ import { createNodeLabel } from '../lib/reactivity/text/createNodeLabel.ts';
13
+ import type { RepeatSequenceDefinition } from '../model/RepeatSequenceDefinition.ts';
14
+ import type { RepeatDefinition } from './RepeatInstance.ts';
15
+ import { RepeatInstance } from './RepeatInstance.ts';
16
+ import type { Root } from './Root.ts';
17
+ import type { DescendantNodeSharedStateSpec } from './abstract/DescendantNode.ts';
18
+ import { DescendantNode } from './abstract/DescendantNode.ts';
19
+ import type { GeneralParentNode } from './hierarchy.ts';
20
+ import type { NodeID } from './identity.ts';
21
+ import type { EvaluationContext } from './internal-api/EvaluationContext.ts';
22
+ import type { SubscribableDependency } from './internal-api/SubscribableDependency.ts';
23
+ import type { TextRange } from './text/TextRange.ts';
24
+
25
+ interface RepeatRangeStateSpec extends DescendantNodeSharedStateSpec {
26
+ readonly hint: null;
27
+ readonly label: Accessor<TextRange<'label'> | null>;
28
+ readonly children: Accessor<readonly NodeID[]>;
29
+ readonly valueOptions: null;
30
+ readonly value: null;
31
+ }
32
+
33
+ export class RepeatRange
34
+ extends DescendantNode<RepeatSequenceDefinition, RepeatRangeStateSpec, RepeatInstance>
35
+ implements RepeatRangeNode, EvaluationContext, SubscribableDependency
36
+ {
37
+ /**
38
+ * A repeat range doesn't have a corresponding primary instance element of its
39
+ * own, and its instances are appended to the range's parent element. During
40
+ * creation of the initial primary instance state and DOM trees, we _could_
41
+ * reliably append all of the range's instances in order as the definition
42
+ * tree is recursed. But that would fail to handle some instance addition
43
+ * cases afterwards.
44
+ *
45
+ * Most notably, we need to know where in the primary instance tree to append
46
+ * instances created for a range which is currently empty. As a lucky
47
+ * coincidence, this need coincides with the ability to add instances at any
48
+ * arbitrary index within the range. In each case, we can reference a primary
49
+ * instance DOM node which will become the new instance's preceding sibling.
50
+ * Where the range is empty, we use this {@link Comment} node (itself created
51
+ * and appended during range initialization) in lieu of a nonexistent
52
+ * preceding instance's {@link contextNode}.
53
+ *
54
+ * @todo We likely want to remove these during submission serialization.
55
+ * @todo Can we use a
56
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Range | DOM Range}
57
+ * instead?
58
+ */
59
+ private readonly anchorNode: Comment;
60
+
61
+ private readonly childrenState: ChildrenState<RepeatInstance>;
62
+
63
+ // InstanceNode
64
+ protected readonly state: SharedNodeState<RepeatRangeStateSpec>;
65
+ protected override engineState: EngineState<RepeatRangeStateSpec>;
66
+
67
+ // RepeatRangeNode
68
+ readonly nodeType = 'repeat-range';
69
+
70
+ readonly currentState: MaterializedChildren<CurrentState<RepeatRangeStateSpec>, RepeatInstance>;
71
+
72
+ constructor(parent: GeneralParentNode, definition: RepeatSequenceDefinition) {
73
+ super(parent, definition);
74
+
75
+ const childrenState = createChildrenState<RepeatRange, RepeatInstance>(this);
76
+
77
+ this.childrenState = childrenState;
78
+
79
+ const state = createSharedNodeState(
80
+ this.scope,
81
+ {
82
+ ...this.buildSharedStateSpec(parent, definition),
83
+
84
+ label: createNodeLabel(this, definition),
85
+ hint: null,
86
+ children: childrenState.childIds,
87
+ valueOptions: null,
88
+ value: null,
89
+ },
90
+ {
91
+ clientStateFactory: this.engineConfig.stateFactory,
92
+ }
93
+ );
94
+
95
+ this.anchorNode = this.contextNode.ownerDocument.createComment(
96
+ `Begin repeat range: ${definition.nodeset}`
97
+ );
98
+ this.contextNode.append(this.anchorNode);
99
+
100
+ this.state = state;
101
+ this.engineState = state.engineState;
102
+ this.currentState = materializeCurrentStateChildren(state.currentState, childrenState);
103
+
104
+ definition.instances.forEach((instanceDefinition, index) => {
105
+ const afterIndex = index - 1;
106
+
107
+ this.addInstances(afterIndex, 1, instanceDefinition);
108
+ });
109
+ }
110
+
111
+ private getLastIndex(): number {
112
+ return this.engineState.children.length - 1;
113
+ }
114
+
115
+ protected override initializeContextNode(parentContextNode: Element): Element {
116
+ return parentContextNode;
117
+ }
118
+
119
+ protected computeReference(parent: GeneralParentNode): string {
120
+ return this.computeChildStepReference(parent);
121
+ }
122
+
123
+ getInstanceIndex(instance: RepeatInstance): number {
124
+ return this.engineState.children.indexOf(instance.nodeId);
125
+ }
126
+
127
+ addInstances(
128
+ afterIndex = this.getLastIndex(),
129
+ count = 1,
130
+ definition: RepeatDefinition = this.definition.template
131
+ ): Root {
132
+ return this.scope.runTask(() => {
133
+ let precedingInstance: RepeatInstance | null;
134
+
135
+ if (afterIndex === -1) {
136
+ precedingInstance = null;
137
+ } else {
138
+ const instance = this.childrenState.getChildren()[afterIndex];
139
+
140
+ if (instance == null) {
141
+ throw new Error(`No repeat instance at index ${afterIndex}`);
142
+ }
143
+
144
+ precedingInstance = instance;
145
+ }
146
+
147
+ const precedingPrimaryInstanceNode = precedingInstance?.contextNode ?? this.anchorNode;
148
+
149
+ const newInstance = new RepeatInstance(this, definition, {
150
+ precedingPrimaryInstanceNode,
151
+ precedingInstance,
152
+ });
153
+ const initialIndex = afterIndex + 1;
154
+
155
+ this.childrenState.setChildren((currentInstances) => {
156
+ return insertAtIndex(currentInstances, initialIndex, newInstance);
157
+ });
158
+
159
+ if (count > 1) {
160
+ return this.addInstances(initialIndex, count - 1);
161
+ }
162
+
163
+ return this.root;
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Removes the {@link RepeatInstance}s corresponding to the specified range of
169
+ * indexes, and then removes those repeat instances from the repeat range's
170
+ * own children state in that order:
171
+ *
172
+ * 1. Identify the set of {@link RepeatInstance}s to be removed.
173
+ *
174
+ * 2. For each {@link RepeatInstance} pending removal, perform that node's
175
+ * removal logic. @see {@link RepeatInstance.remove} for more detail.
176
+ *
177
+ * 3. Finalize update to the repeat range's own {@link childrenState},
178
+ * omitting those {@link RepeatInstance}s which were removed.
179
+ *
180
+ * This ordering ensures a consistent representation of state is established
181
+ * prior to any downstream reactive updates, and ensures that removed nodes'
182
+ * reactivity is cleaned up.
183
+ */
184
+ removeInstances(startIndex: number, count = 1): Root {
185
+ return this.scope.runTask(() => {
186
+ this.childrenState.setChildren((currentInstances) => {
187
+ const updatedInstances = currentInstances.slice();
188
+ const removedInstances = updatedInstances.splice(startIndex, count);
189
+
190
+ removedInstances.forEach((instance) => {
191
+ instance.remove();
192
+ });
193
+
194
+ return updatedInstances;
195
+ });
196
+
197
+ return this.root;
198
+ });
199
+ }
200
+
201
+ override subscribe(): void {
202
+ super.subscribe();
203
+
204
+ // Subscribing to children can support reactive expressions dependent on the
205
+ // repeat range itself (e.g. `count()`).
206
+ this.childrenState.getChildren().forEach((child) => {
207
+ child.subscribe();
208
+ });
209
+ }
210
+
211
+ getChildren(): readonly RepeatInstance[] {
212
+ return this.childrenState.getChildren();
213
+ }
214
+ }