@getodk/xforms-engine 0.14.0 → 0.16.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 (125) hide show
  1. package/dist/client/AttributeNode.d.ts +50 -0
  2. package/dist/client/BaseNode.d.ts +5 -0
  3. package/dist/client/form/FormInstanceConfig.d.ts +15 -0
  4. package/dist/client/index.d.ts +1 -0
  5. package/dist/client/node-types.d.ts +1 -1
  6. package/dist/client/validation.d.ts +7 -1
  7. package/dist/index.js +730 -294
  8. package/dist/index.js.map +1 -1
  9. package/dist/instance/Attribute.d.ts +64 -0
  10. package/dist/instance/Group.d.ts +2 -0
  11. package/dist/instance/InputControl.d.ts +2 -0
  12. package/dist/instance/PrimaryInstance.d.ts +2 -0
  13. package/dist/instance/Root.d.ts +2 -0
  14. package/dist/instance/UploadControl.d.ts +2 -0
  15. package/dist/instance/abstract/InstanceNode.d.ts +5 -2
  16. package/dist/instance/abstract/ValueNode.d.ts +2 -0
  17. package/dist/instance/attachments/buildAttributes.d.ts +7 -0
  18. package/dist/instance/internal-api/AttributeContext.d.ts +35 -0
  19. package/dist/instance/internal-api/InstanceConfig.d.ts +2 -0
  20. package/dist/instance/internal-api/InstanceValueContext.d.ts +6 -0
  21. package/dist/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.d.ts +15 -0
  22. package/dist/instance/internal-api/serialization/ClientReactiveSerializableParentNode.d.ts +2 -0
  23. package/dist/instance/internal-api/serialization/ClientReactiveSerializableTemplatedNode.d.ts +2 -2
  24. package/dist/instance/internal-api/serialization/ClientReactiveSerializableValueNode.d.ts +4 -0
  25. package/dist/instance/repeat/BaseRepeatRange.d.ts +5 -0
  26. package/dist/instance/repeat/RepeatInstance.d.ts +2 -2
  27. package/dist/integration/xpath/adapter/XFormsXPathNode.d.ts +2 -2
  28. package/dist/lib/client-reactivity/instance-state/createAttributeNodeInstanceState.d.ts +3 -0
  29. package/dist/lib/client-reactivity/instance-state/createTemplatedNodeInstanceState.d.ts +0 -3
  30. package/dist/lib/codecs/items/SingleValueItemCodec.d.ts +1 -1
  31. package/dist/lib/names/NamespaceDeclarationMap.d.ts +1 -1
  32. package/dist/lib/reactivity/createAttributeState.d.ts +16 -0
  33. package/dist/lib/reactivity/createInstanceValueState.d.ts +4 -1
  34. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +2 -2
  35. package/dist/lib/xml-serialization.d.ts +5 -9
  36. package/dist/parse/XFormDOM.d.ts +4 -1
  37. package/dist/parse/expression/ActionComputationExpression.d.ts +4 -0
  38. package/dist/parse/model/ActionDefinition.d.ts +15 -0
  39. package/dist/parse/model/AttributeDefinition.d.ts +24 -0
  40. package/dist/parse/model/{RootAttributeMap.d.ts → AttributeDefinitionMap.d.ts} +4 -10
  41. package/dist/parse/model/BindPreloadDefinition.d.ts +6 -10
  42. package/dist/parse/model/Event.d.ts +8 -0
  43. package/dist/parse/model/GroupDefinition.d.ts +4 -1
  44. package/dist/parse/model/LeafNodeDefinition.d.ts +5 -1
  45. package/dist/parse/model/ModelActionMap.d.ts +9 -0
  46. package/dist/parse/model/ModelDefinition.d.ts +8 -1
  47. package/dist/parse/model/NodeDefinition.d.ts +8 -3
  48. package/dist/parse/model/NoteNodeDefinition.d.ts +3 -2
  49. package/dist/parse/model/RangeNodeDefinition.d.ts +2 -1
  50. package/dist/parse/model/RepeatDefinition.d.ts +4 -1
  51. package/dist/parse/model/RootDefinition.d.ts +3 -2
  52. package/dist/solid.js +730 -294
  53. package/dist/solid.js.map +1 -1
  54. package/package.json +21 -17
  55. package/src/client/AttributeNode.ts +59 -0
  56. package/src/client/BaseNode.ts +6 -0
  57. package/src/client/form/FormInstanceConfig.ts +17 -0
  58. package/src/client/index.ts +1 -0
  59. package/src/client/node-types.ts +1 -0
  60. package/src/client/validation.ts +9 -1
  61. package/src/entrypoints/FormInstance.ts +1 -0
  62. package/src/instance/Attribute.ts +164 -0
  63. package/src/instance/Group.ts +7 -0
  64. package/src/instance/InputControl.ts +8 -0
  65. package/src/instance/ModelValue.ts +7 -0
  66. package/src/instance/Note.ts +6 -0
  67. package/src/instance/PrimaryInstance.ts +7 -0
  68. package/src/instance/RangeControl.ts +6 -0
  69. package/src/instance/RankControl.ts +7 -0
  70. package/src/instance/Root.ts +7 -0
  71. package/src/instance/SelectControl.ts +6 -0
  72. package/src/instance/TriggerControl.ts +6 -0
  73. package/src/instance/UploadControl.ts +5 -0
  74. package/src/instance/abstract/DescendantNode.ts +0 -1
  75. package/src/instance/abstract/InstanceNode.ts +4 -1
  76. package/src/instance/abstract/ValueNode.ts +2 -0
  77. package/src/instance/attachments/buildAttributes.ts +15 -0
  78. package/src/instance/children/normalizeChildInitOptions.ts +1 -1
  79. package/src/instance/internal-api/AttributeContext.ts +40 -0
  80. package/src/instance/internal-api/InstanceConfig.ts +6 -1
  81. package/src/instance/internal-api/InstanceValueContext.ts +6 -0
  82. package/src/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.ts +18 -0
  83. package/src/instance/internal-api/serialization/ClientReactiveSerializableParentNode.ts +2 -0
  84. package/src/instance/internal-api/serialization/ClientReactiveSerializableTemplatedNode.ts +2 -3
  85. package/src/instance/internal-api/serialization/ClientReactiveSerializableValueNode.ts +4 -0
  86. package/src/instance/repeat/BaseRepeatRange.ts +14 -0
  87. package/src/instance/repeat/RepeatInstance.ts +5 -5
  88. package/src/integration/xpath/adapter/XFormsXPathNode.ts +3 -1
  89. package/src/lib/client-reactivity/instance-state/createAttributeNodeInstanceState.ts +16 -0
  90. package/src/lib/client-reactivity/instance-state/createParentNodeInstanceState.ts +5 -5
  91. package/src/lib/client-reactivity/instance-state/createRootInstanceState.ts +6 -9
  92. package/src/lib/client-reactivity/instance-state/createTemplatedNodeInstanceState.ts +5 -15
  93. package/src/lib/client-reactivity/instance-state/createValueNodeInstanceState.ts +2 -1
  94. package/src/lib/client-reactivity/instance-state/prepareInstancePayload.ts +1 -0
  95. package/src/lib/codecs/NoteCodec.ts +1 -1
  96. package/src/lib/codecs/items/SingleValueItemCodec.ts +1 -3
  97. package/src/lib/names/NamespaceDeclarationMap.ts +1 -1
  98. package/src/lib/reactivity/createAttributeState.ts +51 -0
  99. package/src/lib/reactivity/createInstanceValueState.ts +152 -53
  100. package/src/lib/reactivity/node-state/createSharedNodeState.ts +2 -2
  101. package/src/lib/xml-serialization.ts +38 -34
  102. package/src/parse/XFormDOM.ts +9 -0
  103. package/src/parse/body/GroupElementDefinition.ts +1 -1
  104. package/src/parse/body/control/InputControlDefinition.ts +1 -1
  105. package/src/parse/expression/ActionComputationExpression.ts +12 -0
  106. package/src/parse/model/ActionDefinition.ts +70 -0
  107. package/src/parse/model/AttributeDefinition.ts +59 -0
  108. package/src/parse/model/{RootAttributeMap.ts → AttributeDefinitionMap.ts} +7 -13
  109. package/src/parse/model/BindDefinition.ts +1 -6
  110. package/src/parse/model/BindPreloadDefinition.ts +44 -12
  111. package/src/parse/model/Event.ts +9 -0
  112. package/src/parse/model/GroupDefinition.ts +6 -0
  113. package/src/parse/model/LeafNodeDefinition.ts +5 -0
  114. package/src/parse/model/ModelActionMap.ts +37 -0
  115. package/src/parse/model/ModelDefinition.ts +18 -3
  116. package/src/parse/model/NodeDefinition.ts +11 -3
  117. package/src/parse/model/NoteNodeDefinition.ts +5 -2
  118. package/src/parse/model/RangeNodeDefinition.ts +5 -2
  119. package/src/parse/model/RepeatDefinition.ts +8 -1
  120. package/src/parse/model/RootDefinition.ts +27 -9
  121. package/src/parse/model/nodeDefinitionMap.ts +1 -1
  122. package/dist/error/TemplatedNodeAttributeSerializationError.d.ts +0 -22
  123. package/dist/parse/model/RootAttributeDefinition.d.ts +0 -21
  124. package/src/error/TemplatedNodeAttributeSerializationError.ts +0 -24
  125. package/src/parse/model/RootAttributeDefinition.ts +0 -44
@@ -1,11 +1,7 @@
1
1
  import type { InstanceState } from '../../../client/serialization/InstanceState.ts';
2
- import { TemplatedNodeAttributeSerializationError } from '../../../error/TemplatedNodeAttributeSerializationError.ts';
3
2
  import type { ClientReactiveSerializableTemplatedNode } from '../../../instance/internal-api/serialization/ClientReactiveSerializableTemplatedNode.ts';
4
3
  import { serializeParentElementXML } from '../../xml-serialization.ts';
5
4
 
6
- /**
7
- * @see {@link TemplatedNodeAttributeSerializationError}
8
- */
9
5
  export const createTemplatedNodeInstanceState = (
10
6
  node: ClientReactiveSerializableTemplatedNode
11
7
  ): InstanceState => {
@@ -15,17 +11,11 @@ export const createTemplatedNodeInstanceState = (
15
11
  return '';
16
12
  }
17
13
 
18
- const serializedChildren = node.currentState.children.map((child) => {
19
- return child.instanceState.instanceXML;
20
- });
21
-
22
- const { attributes } = node.currentState;
23
-
24
- if (attributes != null) {
25
- throw new TemplatedNodeAttributeSerializationError();
26
- }
27
-
28
- return serializeParentElementXML(node.definition.qualifiedName, serializedChildren);
14
+ return serializeParentElementXML(
15
+ node.definition.qualifiedName,
16
+ node.currentState.children,
17
+ node.currentState.attributes
18
+ );
29
19
  },
30
20
  };
31
21
  };
@@ -14,8 +14,9 @@ export const createValueNodeInstanceState = (
14
14
  }
15
15
 
16
16
  const xmlValue = escapeXMLText(node.currentState.instanceValue);
17
+ const attributes = node.currentState.attributes;
17
18
 
18
- return serializeLeafElementXML(qualifiedName, xmlValue);
19
+ return serializeLeafElementXML(qualifiedName, xmlValue, attributes);
19
20
  },
20
21
  };
21
22
  };
@@ -208,6 +208,7 @@ export const prepareInstancePayload = <PayloadType extends InstancePayloadType>(
208
208
  instanceRoot: ClientReactiveSerializableInstance,
209
209
  options: PrepareInstancePayloadOptions<PayloadType>
210
210
  ): InstancePayload<PayloadType> => {
211
+ instanceRoot.root.parent.model.triggerXformsRevalidateListeners();
211
212
  const validation = validateInstance(instanceRoot);
212
213
  const submissionMeta = instanceRoot.definition.submission;
213
214
  const instanceFile = new InstanceFile(instanceRoot);
@@ -10,7 +10,7 @@ export type NoteRuntimeValue<V extends ValueType> =
10
10
  // prettier-ignore
11
11
  export type NoteInputValue<V extends ValueType> =
12
12
  | RuntimeInputValue<V>
13
- | RuntimeValue<V>
13
+ | RuntimeValue<V> // eslint-disable-line @typescript-eslint/no-redundant-type-constituents
14
14
  | null;
15
15
 
16
16
  export class NoteCodec<V extends ValueType> extends ValueCodec<
@@ -12,9 +12,7 @@ export type SingleValueSelectRuntimeValues =
12
12
  * @see {@link encodeValueFactory}
13
13
  */
14
14
  // prettier-ignore
15
- type SingleValueSelectCodecValues =
16
- | SingleValueSelectRuntimeValues
17
- | readonly string[];
15
+ type SingleValueSelectCodecValues = readonly string[];
18
16
 
19
17
  /**
20
18
  * @todo This is more permissive than it should be, allowing an array of any
@@ -11,7 +11,7 @@ type NamedNodeDefinitionMap = ReadonlyMap<QualifiedName, NamedNodeDefinition>;
11
11
  export interface NamedSubtreeDefinition extends NamedNodeDefinition {
12
12
  readonly namespaceDeclarations: NamespaceDeclarationMap;
13
13
  readonly parent: NamedSubtreeDefinition | null;
14
- readonly attributes?: NamedNodeDefinitionMap;
14
+ readonly attributes: NamedNodeDefinitionMap | null;
15
15
  }
16
16
 
17
17
  /**
@@ -0,0 +1,51 @@
1
+ import type { Accessor, Setter, Signal } from 'solid-js';
2
+ import { createSignal } from 'solid-js';
3
+ import type { Attribute } from '../../instance/Attribute.ts';
4
+ import type { ReactiveScope } from './scope.ts';
5
+
6
+ export interface AttributeState {
7
+ readonly attributes: Signal<readonly Attribute[]>;
8
+ readonly getAttributes: Accessor<readonly Attribute[]>;
9
+ readonly setAttributes: Setter<readonly Attribute[]>;
10
+ }
11
+
12
+ /**
13
+ * Creates attributes state suitable for all node types
14
+ *
15
+ * The produced {@link AttributeState.attributes} (and its get/set convenience
16
+ * methods) signal is intended to be used to store the engine's attribute state,
17
+ * and update that state when appropriate.
18
+ */
19
+ export const createAttributeState = (scope: ReactiveScope): AttributeState => {
20
+ return scope.runTask(() => {
21
+ const baseState = createSignal<readonly Attribute[]>([]);
22
+ const [getAttributes, baseSetAttributes] = baseState;
23
+
24
+ type AttributeSetterCallback = (prev: readonly Attribute[]) => readonly Attribute[];
25
+ type AttributeOrSetterCallback = AttributeSetterCallback | readonly Attribute[];
26
+
27
+ const setAttributes: Setter<readonly Attribute[]> = (
28
+ valueOrSetterCallback: AttributeOrSetterCallback
29
+ ) => {
30
+ let setterCallback: AttributeSetterCallback;
31
+
32
+ if (typeof valueOrSetterCallback === 'function') {
33
+ setterCallback = valueOrSetterCallback;
34
+ } else {
35
+ setterCallback = (_) => valueOrSetterCallback;
36
+ }
37
+
38
+ return baseSetAttributes((prev) => {
39
+ return setterCallback(prev);
40
+ });
41
+ };
42
+
43
+ const attributes: Signal<readonly Attribute[]> = [getAttributes, setAttributes];
44
+
45
+ return {
46
+ attributes,
47
+ getAttributes,
48
+ setAttributes,
49
+ };
50
+ });
51
+ };
@@ -1,24 +1,35 @@
1
1
  import type { Signal } from 'solid-js';
2
2
  import { createComputed, createMemo, createSignal, untrack } from 'solid-js';
3
+ import type { AttributeContext } from '../../instance/internal-api/AttributeContext.ts';
3
4
  import type { InstanceValueContext } from '../../instance/internal-api/InstanceValueContext.ts';
5
+ import { ActionComputationExpression } from '../../parse/expression/ActionComputationExpression.ts';
4
6
  import type { BindComputationExpression } from '../../parse/expression/BindComputationExpression.ts';
7
+ import { ActionDefinition } from '../../parse/model/ActionDefinition.ts';
8
+ import type { AnyBindPreloadDefinition } from '../../parse/model/BindPreloadDefinition.ts';
9
+ import { XFORM_EVENT } from '../../parse/model/Event.ts';
5
10
  import { createComputedExpression } from './createComputedExpression.ts';
6
11
  import type { SimpleAtomicState, SimpleAtomicStateSetter } from './types.ts';
7
12
 
8
- const isInstanceFirstLoad = (context: InstanceValueContext) => {
13
+ const REPEAT_INDEX_REGEX = /([^[]*)(\[[0-9]+\])/g;
14
+
15
+ type ValueContext = AttributeContext | InstanceValueContext;
16
+
17
+ const isInstanceFirstLoad = (context: ValueContext) => {
9
18
  return context.rootDocument.initializationMode === 'create';
10
19
  };
11
20
 
21
+ const isAddingRepeatChild = (context: ValueContext) => {
22
+ return context.rootDocument.isAttached();
23
+ };
24
+
12
25
  /**
13
26
  * Special case, does not correspond to any event.
14
- *
15
- * @see {@link shouldPreloadUID}
16
27
  */
17
- const isEditInitialLoad = (context: InstanceValueContext) => {
28
+ const isEditInitialLoad = (context: ValueContext) => {
18
29
  return context.rootDocument.initializationMode === 'edit';
19
30
  };
20
31
 
21
- const getInitialValue = (context: InstanceValueContext): string => {
32
+ const getInitialValue = (context: ValueContext): string => {
22
33
  const sourceNode = context.instanceNode ?? context.definition.template;
23
34
 
24
35
  return context.decodeInstanceValue(sourceNode.value);
@@ -36,7 +47,7 @@ type RelevantValueState = SimpleAtomicState<string>;
36
47
  * node/context's relevance is restored
37
48
  */
38
49
  const createRelevantValueState = (
39
- context: InstanceValueContext,
50
+ context: ValueContext,
40
51
  baseValueState: BaseValueState
41
52
  ): RelevantValueState => {
42
53
  return context.scope.runTask(() => {
@@ -59,7 +70,7 @@ const createRelevantValueState = (
59
70
  * (client/user) writes when the field is in a `readonly` state.
60
71
  */
61
72
  const guardDownstreamReadonlyWrites = (
62
- context: InstanceValueContext,
73
+ context: ValueContext,
63
74
  baseState: SimpleAtomicState<string>
64
75
  ): SimpleAtomicState<string> => {
65
76
  const { readonly } = context.definition.bind;
@@ -83,47 +94,86 @@ const guardDownstreamReadonlyWrites = (
83
94
  return [getValue, setValue];
84
95
  };
85
96
 
86
- /**
87
- * Per {@link https://getodk.github.io/xforms-spec/#preload-attributes:~:text=concatenation%20of%20%E2%80%98uuid%3A%E2%80%99%20and%20uuid()}
88
- */
89
- const PRELOAD_UID_EXPRESSION = 'concat("uuid:", uuid())';
90
-
91
97
  /**
92
98
  * @todo It feels increasingly awkward to keep piling up preload stuff here, but it won't stay that way for long. In the meantime, this seems like the best way to express the cases where `preload="uid"` should be effective, i.e.:
93
99
  *
94
100
  * - When an instance is first loaded ({@link isInstanceFirstLoad})
95
101
  * - When an instance is initially loaded for editing ({@link isEditInitialLoad})
96
102
  */
97
- const shouldPreloadUID = (context: InstanceValueContext) => {
103
+ const isLoading = (context: ValueContext) => {
98
104
  return isInstanceFirstLoad(context) || isEditInitialLoad(context);
99
105
  };
100
106
 
101
- /**
102
- * @todo This is a temporary one-off, until we support the full range of
103
- * {@link https://getodk.github.io/xforms-spec/#preload-attributes | preloads}.
104
- *
105
- * @todo ALSO, IMPORTANTLY(!): the **call site** for this function is
106
- * semantically where we would expect to trigger a
107
- * {@link https://getodk.github.io/xforms-spec/#event:odk-instance-first-load | odk-instance-first-load event},
108
- * _and compute_ preloads semantically associated with that event.
109
- */
110
- const setPreloadUIDValue = (
111
- context: InstanceValueContext,
112
- valueState: RelevantValueState
113
- ): void => {
107
+ const setValueIfPreloadDefined = (
108
+ context: ValueContext,
109
+ setValue: SimpleAtomicStateSetter<string>,
110
+ preload: AnyBindPreloadDefinition
111
+ ) => {
112
+ const value = preload.getValue(context);
113
+ if (value) {
114
+ setValue(value);
115
+ }
116
+ };
117
+
118
+ const postloadValue = (
119
+ context: ValueContext,
120
+ setValue: SimpleAtomicStateSetter<string>,
121
+ preload: AnyBindPreloadDefinition
122
+ ) => {
123
+ const ref = context.contextReference();
124
+ context.definition.model.registerXformsRevalidateListener(ref, () => {
125
+ setValueIfPreloadDefined(context, setValue, preload);
126
+ });
127
+ };
128
+
129
+ const preloadValue = (context: ValueContext, setValue: SimpleAtomicStateSetter<string>): void => {
114
130
  const { preload } = context.definition.bind;
131
+ if (!preload) {
132
+ return;
133
+ }
115
134
 
116
- if (preload?.type !== 'uid' || !shouldPreloadUID(context)) {
135
+ if (preload.event === XFORM_EVENT.xformsRevalidate) {
136
+ postloadValue(context, setValue, preload);
117
137
  return;
118
138
  }
119
139
 
120
- const preloadUIDValue = context.evaluator.evaluateString(PRELOAD_UID_EXPRESSION, {
140
+ if (isLoading(context)) {
141
+ setValueIfPreloadDefined(context, setValue, preload);
142
+ }
143
+ };
144
+
145
+ const referencesCurrentNode = (context: ValueContext, ref: string): boolean => {
146
+ const nodes = context.evaluator.evaluateNodes(ref, {
121
147
  contextNode: context.contextNode,
122
148
  });
149
+ if (nodes.length > 1) {
150
+ throw new Error(
151
+ 'You are trying to target a repeated field. Currently you may only target a field in a specific repeat instance. XPath nodeset has more than one node.'
152
+ );
153
+ }
154
+ return nodes.includes(context.contextNode);
155
+ };
123
156
 
124
- const [, setValue] = valueState;
125
-
126
- setValue(preloadUIDValue);
157
+ // Replaces the unbound repeat references in source and ref, with references
158
+ // bound to the repeat instace of the context.
159
+ const bindToRepeatInstance = (
160
+ context: ValueContext,
161
+ action: ActionDefinition
162
+ ): { source: string | undefined; ref: string } => {
163
+ let source = action.source;
164
+ let ref = action.ref;
165
+ if (source) {
166
+ const contextRef = context.contextReference();
167
+ for (const part of contextRef.matchAll(REPEAT_INDEX_REGEX)) {
168
+ const unbound = part[1] + '/';
169
+ if (source.includes(unbound)) {
170
+ const bound = part[0] + '/';
171
+ source = source.replace(unbound, bound);
172
+ ref = ref.replace(unbound, bound);
173
+ }
174
+ }
175
+ }
176
+ return { source, ref };
127
177
  };
128
178
 
129
179
  /**
@@ -131,30 +181,78 @@ const setPreloadUIDValue = (
131
181
  * computations to the provided value setter, on initialization and any
132
182
  * subsequent reactive update.
133
183
  *
134
- * @see {@link setPreloadUIDValue} for important details about spec ordering of
184
+ * @see {@link preloadValue} for important details about spec ordering of
135
185
  * events and computations.
136
186
  */
137
187
  const createCalculation = (
138
- context: InstanceValueContext,
188
+ context: ValueContext,
139
189
  setRelevantValue: SimpleAtomicStateSetter<string>,
140
- calculateDefinition: BindComputationExpression<'calculate'>
190
+ computation: ActionComputationExpression<'string'> | BindComputationExpression<'calculate'>
141
191
  ): void => {
142
- context.scope.runTask(() => {
143
- const calculate = createComputedExpression(context, calculateDefinition, {
144
- defaultValue: '',
145
- });
146
-
147
- createComputed(() => {
148
- if (context.isAttached() && context.isRelevant()) {
149
- const calculated = calculate();
150
- const value = context.decodeInstanceValue(calculated);
192
+ const calculate = createComputedExpression(context, computation);
193
+ createComputed(() => {
194
+ if (context.isAttached() && context.isRelevant()) {
195
+ const calculated = calculate();
196
+ const value = context.decodeInstanceValue(calculated);
197
+ setRelevantValue(value);
198
+ }
199
+ });
200
+ };
151
201
 
152
- setRelevantValue(value);
202
+ const createValueChangedCalculation = (
203
+ context: ValueContext,
204
+ setRelevantValue: SimpleAtomicStateSetter<string>,
205
+ action: ActionDefinition
206
+ ): void => {
207
+ const { source, ref } = bindToRepeatInstance(context, action);
208
+ if (!source) {
209
+ // no element to listen to
210
+ return;
211
+ }
212
+ let previous = '';
213
+ const sourceElementExpression = new ActionComputationExpression('string', source);
214
+ const calculateValueSource = createComputedExpression(context, sourceElementExpression); // registers listener
215
+ createComputed(() => {
216
+ if (context.isAttached() && context.isRelevant()) {
217
+ const valueSource = calculateValueSource();
218
+ if (previous !== valueSource) {
219
+ // only update if value has changed
220
+ if (referencesCurrentNode(context, ref)) {
221
+ const calc = context.evaluator.evaluateString(action.computation.expression, context);
222
+ const value = context.decodeInstanceValue(calc);
223
+ setRelevantValue(value);
224
+ }
153
225
  }
154
- });
226
+ previous = valueSource;
227
+ }
155
228
  });
156
229
  };
157
230
 
231
+ const registerAction = (
232
+ context: ValueContext,
233
+ setValue: SimpleAtomicStateSetter<string>,
234
+ action: ActionDefinition
235
+ ) => {
236
+ if (action.events.includes(XFORM_EVENT.odkInstanceFirstLoad)) {
237
+ if (isInstanceFirstLoad(context)) {
238
+ createCalculation(context, setValue, action.computation);
239
+ }
240
+ }
241
+ if (action.events.includes(XFORM_EVENT.odkInstanceLoad)) {
242
+ if (!isAddingRepeatChild(context)) {
243
+ createCalculation(context, setValue, action.computation);
244
+ }
245
+ }
246
+ if (action.events.includes(XFORM_EVENT.odkNewRepeat)) {
247
+ if (isAddingRepeatChild(context)) {
248
+ createCalculation(context, setValue, action.computation);
249
+ }
250
+ }
251
+ if (action.events.includes(XFORM_EVENT.xformsValueChanged)) {
252
+ createValueChangedCalculation(context, setValue, action);
253
+ }
254
+ };
255
+
158
256
  export type InstanceValueState = SimpleAtomicState<string>;
159
257
 
160
258
  /**
@@ -168,25 +266,26 @@ export type InstanceValueState = SimpleAtomicState<string>;
168
266
  * nodes defined with one
169
267
  * - prevents downstream writes to nodes in a readonly state
170
268
  */
171
- export const createInstanceValueState = (context: InstanceValueContext): InstanceValueState => {
269
+ export const createInstanceValueState = (context: ValueContext): InstanceValueState => {
172
270
  return context.scope.runTask(() => {
173
271
  const initialValue = getInitialValue(context);
174
272
  const baseValueState = createSignal(initialValue);
175
273
  const relevantValueState = createRelevantValueState(context, baseValueState);
176
274
 
177
- /**
178
- * @see {@link setPreloadUIDValue} for important details about spec ordering of events and computations.
179
- */
180
- setPreloadUIDValue(context, relevantValueState);
275
+ const [, setValue] = relevantValueState;
181
276
 
182
- const { calculate } = context.definition.bind;
277
+ preloadValue(context, setValue);
183
278
 
279
+ const { calculate } = context.definition.bind;
184
280
  if (calculate != null) {
185
- const [, setValue] = relevantValueState;
186
-
187
281
  createCalculation(context, setValue, calculate);
188
282
  }
189
283
 
284
+ const action = context.definition.model.actions.get(context.contextReference());
285
+ if (action) {
286
+ registerAction(context, setValue, action);
287
+ }
288
+
190
289
  return guardDownstreamReadonlyWrites(context, relevantValueState);
191
290
  });
192
291
  };
@@ -13,10 +13,10 @@ import { isComputedPropertySpec, isMutablePropertySpec } from './createSpecified
13
13
  // prettier-ignore
14
14
  type MutableKeyOf<Spec extends StateSpec> = {
15
15
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
- [K in string & keyof Spec]: Spec[K] extends MutablePropertySpec<any>
16
+ [K in Extract<keyof Spec, string>]: Spec[K] extends MutablePropertySpec<any>
17
17
  ? K
18
18
  : never;
19
- }[string & keyof Spec];
19
+ }[Extract<keyof Spec, string>];
20
20
 
21
21
  type SetEnginePropertyState<Spec extends StateSpec> = <K extends MutableKeyOf<Spec>>(
22
22
  key: K,
@@ -1,3 +1,5 @@
1
+ import type { GeneralChildNode } from '../client/hierarchy.ts';
2
+ import type { Attribute } from '../instance/Attribute.ts';
1
3
  import type { NamespaceDeclarationMap } from './names/NamespaceDeclarationMap.ts';
2
4
  import type { QualifiedName } from './names/QualifiedName.ts';
3
5
 
@@ -78,15 +80,6 @@ export const escapeXMLText = <Text extends string>(
78
80
  : (out as EscapedXMLText);
79
81
  };
80
82
 
81
- interface SerializableElementAttribute {
82
- serializeAttributeXML(): string;
83
- }
84
-
85
- interface ElementXMLSerializationOptions {
86
- readonly namespaceDeclarations?: NamespaceDeclarationMap;
87
- readonly attributes?: readonly SerializableElementAttribute[];
88
- }
89
-
90
83
  const serializeElementNamespaceDeclarationXML = (
91
84
  namespaceDeclarations?: NamespaceDeclarationMap
92
85
  ): string => {
@@ -103,24 +96,11 @@ const serializeElementNamespaceDeclarationXML = (
103
96
  .join('');
104
97
  };
105
98
 
106
- const serializeElementAttributeXML = (
107
- attributes?: readonly SerializableElementAttribute[]
108
- ): string => {
109
- if (attributes == null) {
110
- return '';
111
- }
112
-
113
- return attributes
114
- .map((attribute) => {
115
- return attribute.serializeAttributeXML();
116
- })
117
- .join('');
118
- };
119
-
120
99
  const serializeElementXML = (
121
100
  qualifiedName: QualifiedName,
122
101
  children: string,
123
- options: ElementXMLSerializationOptions = {}
102
+ attributes: string,
103
+ namespaceDeclarations?: NamespaceDeclarationMap
124
104
  ): string => {
125
105
  // See JSDoc for the `getPrefixedName` method. If we find we do actually need
126
106
  // custom element (subtree) prefix resolution, we'd uncomment the argument
@@ -133,11 +113,9 @@ const serializeElementXML = (
133
113
  const nodeName = qualifiedName.getPrefixedName(
134
114
  // options.namespaceDeclarations
135
115
  );
136
- const namespaceDeclarations = serializeElementNamespaceDeclarationXML(
137
- options.namespaceDeclarations
138
- );
139
- const attributes = serializeElementAttributeXML(options.attributes);
140
- const prefix = `<${nodeName}${namespaceDeclarations}${attributes}`;
116
+ const serializedNamespaceDeclarations =
117
+ serializeElementNamespaceDeclarationXML(namespaceDeclarations);
118
+ const prefix = `<${nodeName}${serializedNamespaceDeclarations}${attributes}`;
141
119
 
142
120
  if (children === '') {
143
121
  return `${prefix}/>`;
@@ -146,18 +124,44 @@ const serializeElementXML = (
146
124
  return `${prefix}>${children}</${nodeName}>`;
147
125
  };
148
126
 
127
+ export const serializeAttributeXML = (
128
+ qualifiedName: QualifiedName,
129
+ xmlValue: EscapedXMLText
130
+ ): string => {
131
+ const nodeName = qualifiedName.getPrefixedName();
132
+ return ` ${nodeName}="${xmlValue.normalize()}"`;
133
+ };
134
+
149
135
  export const serializeParentElementXML = (
150
136
  qualifiedName: QualifiedName,
151
- serializedChildren: readonly string[],
152
- options?: ElementXMLSerializationOptions
137
+ children: readonly GeneralChildNode[],
138
+ attributes: readonly Attribute[],
139
+ namespaceDeclarations?: NamespaceDeclarationMap
153
140
  ): string => {
154
- return serializeElementXML(qualifiedName, serializedChildren.join(''), options);
141
+ const serializedChildren = children.map((child) => child.instanceState.instanceXML).join('');
142
+ const serializedAttributes = attributes
143
+ .map((attribute) => attribute.instanceState.instanceXML)
144
+ .join('');
145
+ return serializeElementXML(
146
+ qualifiedName,
147
+ serializedChildren,
148
+ serializedAttributes,
149
+ namespaceDeclarations
150
+ );
155
151
  };
156
152
 
157
153
  export const serializeLeafElementXML = (
158
154
  qualifiedName: QualifiedName,
159
155
  xmlValue: EscapedXMLText,
160
- options?: ElementXMLSerializationOptions
156
+ attributes: readonly Attribute[],
157
+ namespaceDeclarations?: NamespaceDeclarationMap
161
158
  ): string => {
162
- return serializeElementXML(qualifiedName, xmlValue.normalize(), options);
159
+ const serializedAttributes =
160
+ attributes?.map((attribute) => attribute.instanceState.instanceXML).join('') ?? '';
161
+ return serializeElementXML(
162
+ qualifiedName,
163
+ xmlValue.normalize(),
164
+ serializedAttributes,
165
+ namespaceDeclarations
166
+ );
163
167
  };
@@ -11,6 +11,7 @@ import type {
11
11
  import { DefaultEvaluator } from '@getodk/xpath';
12
12
 
13
13
  interface DOMBindElement extends KnownAttributeLocalNamedElement<'bind', 'nodeset'> {}
14
+ interface DOMSetValueElement extends KnownAttributeLocalNamedElement<'setvalue', 'event'> {}
14
15
 
15
16
  const getMetaElement = (primaryInstanceRoot: Element): Element | null => {
16
17
  for (const child of primaryInstanceRoot.children) {
@@ -336,6 +337,7 @@ export class XFormDOM {
336
337
 
337
338
  readonly model: Element;
338
339
  readonly binds: readonly DOMBindElement[];
340
+ readonly setValues: readonly DOMSetValueElement[];
339
341
  readonly primaryInstance: Element;
340
342
  readonly primaryInstanceRoot: Element;
341
343
 
@@ -368,6 +370,12 @@ export class XFormDOM {
368
370
  contextNode: model,
369
371
  }
370
372
  );
373
+ const setValues: readonly DOMSetValueElement[] = evaluator.evaluateNodes<DOMSetValueElement>(
374
+ './xf:setvalue[@event]',
375
+ {
376
+ contextNode: model,
377
+ }
378
+ );
371
379
 
372
380
  const instances = evaluator.evaluateNodes<DOMInstanceElement>('./xf:instance', {
373
381
  contextNode: model,
@@ -417,6 +425,7 @@ export class XFormDOM {
417
425
  this.title = title;
418
426
  this.model = model;
419
427
  this.binds = binds;
428
+ this.setValues = setValues;
420
429
  this.primaryInstance = primaryInstance;
421
430
  this.primaryInstanceRoot = primaryInstanceRoot;
422
431
  this.itextTranslationElements = itextTranslationElements;
@@ -39,8 +39,8 @@ export class GroupElementDefinition extends BodyElementDefinition<'group'> {
39
39
  return childName !== 'label';
40
40
  });
41
41
 
42
- this.children = this.body.getChildElementDefinitions(form, this, element, childElements);
43
42
  this.reference = parseNodesetReference(parent, element, 'ref');
43
+ this.children = this.body.getChildElementDefinitions(form, this, element, childElements);
44
44
  this.appearances = structureElementAppearanceParser.parseFrom(element, 'appearance');
45
45
  this.label = LabelDefinition.forGroup(form, this);
46
46
  }
@@ -1,8 +1,8 @@
1
+ import { parseToFloat, parseToInteger } from '../../../lib/number-parsers.ts';
1
2
  import type { XFormDefinition } from '../../XFormDefinition.ts';
2
3
  import type { InputAppearanceDefinition } from '../appearance/inputAppearanceParser.ts';
3
4
  import { inputAppearanceParser } from '../appearance/inputAppearanceParser.ts';
4
5
  import type { BodyElementParentContext } from '../BodyDefinition.ts';
5
- import { parseToFloat, parseToInteger } from '../../../lib/number-parsers.ts';
6
6
  import { ControlDefinition } from './ControlDefinition.ts';
7
7
 
8
8
  export class InputControlDefinition extends ControlDefinition<'input'> {
@@ -0,0 +1,12 @@
1
+ import {
2
+ DependentExpression,
3
+ type DependentExpressionResultType,
4
+ } from './abstract/DependentExpression.ts';
5
+
6
+ export class ActionComputationExpression<
7
+ Type extends DependentExpressionResultType,
8
+ > extends DependentExpression<Type> {
9
+ constructor(resultType: Type, expression: string) {
10
+ super(resultType, expression);
11
+ }
12
+ }