@getodk/xforms-engine 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/client/AttributeNode.d.ts +4 -6
  2. package/dist/client/BaseNode.d.ts +5 -0
  3. package/dist/client/form/FormInstanceConfig.d.ts +15 -0
  4. package/dist/index.js +654 -429
  5. package/dist/index.js.map +1 -1
  6. package/dist/instance/Attribute.d.ts +13 -19
  7. package/dist/instance/Group.d.ts +2 -1
  8. package/dist/instance/InputControl.d.ts +5 -0
  9. package/dist/instance/ModelValue.d.ts +4 -0
  10. package/dist/instance/Note.d.ts +4 -0
  11. package/dist/instance/PrimaryInstance.d.ts +3 -2
  12. package/dist/instance/RangeControl.d.ts +4 -0
  13. package/dist/instance/RankControl.d.ts +5 -1
  14. package/dist/instance/Root.d.ts +2 -1
  15. package/dist/instance/SelectControl.d.ts +5 -1
  16. package/dist/instance/TriggerControl.d.ts +4 -0
  17. package/dist/instance/UploadControl.d.ts +3 -2
  18. package/dist/instance/abstract/DescendantNode.d.ts +5 -4
  19. package/dist/instance/abstract/InstanceNode.d.ts +6 -5
  20. package/dist/instance/abstract/ValueNode.d.ts +2 -1
  21. package/dist/instance/attachments/buildAttributes.d.ts +6 -2
  22. package/dist/instance/hierarchy.d.ts +2 -2
  23. package/dist/instance/internal-api/AttributeContext.d.ts +6 -0
  24. package/dist/instance/internal-api/InstanceConfig.d.ts +2 -0
  25. package/dist/instance/internal-api/InstanceValueContext.d.ts +6 -0
  26. package/dist/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.d.ts +0 -1
  27. package/dist/instance/internal-api/serialization/ClientReactiveSerializableValueNode.d.ts +4 -0
  28. package/dist/integration/xpath/adapter/XFormsXPathNode.d.ts +1 -1
  29. package/dist/integration/xpath/adapter/kind.d.ts +5 -3
  30. package/dist/integration/xpath/adapter/traversal.d.ts +3 -3
  31. package/dist/integration/xpath/static-dom/StaticAttribute.d.ts +1 -0
  32. package/dist/lib/codecs/items/SingleValueItemCodec.d.ts +1 -1
  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 +1 -1
  36. package/dist/parse/XFormDOM.d.ts +3 -0
  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 +5 -1
  40. package/dist/parse/model/BindPreloadDefinition.d.ts +6 -10
  41. package/dist/parse/model/Event.d.ts +8 -0
  42. package/dist/parse/model/LeafNodeDefinition.d.ts +5 -2
  43. package/dist/parse/model/ModelActionMap.d.ts +9 -0
  44. package/dist/parse/model/ModelDefinition.d.ts +8 -1
  45. package/dist/parse/model/NoteNodeDefinition.d.ts +3 -2
  46. package/dist/parse/model/RangeNodeDefinition.d.ts +2 -1
  47. package/dist/parse/model/RootDefinition.d.ts +1 -0
  48. package/dist/solid.js +654 -429
  49. package/dist/solid.js.map +1 -1
  50. package/package.json +21 -17
  51. package/src/client/AttributeNode.ts +4 -6
  52. package/src/client/BaseNode.ts +6 -0
  53. package/src/client/form/FormInstanceConfig.ts +17 -0
  54. package/src/client/validation.ts +1 -1
  55. package/src/entrypoints/FormInstance.ts +1 -0
  56. package/src/instance/Attribute.ts +43 -59
  57. package/src/instance/Group.ts +5 -6
  58. package/src/instance/InputControl.ts +16 -1
  59. package/src/instance/ModelValue.ts +16 -1
  60. package/src/instance/Note.ts +15 -1
  61. package/src/instance/PrimaryInstance.ts +8 -10
  62. package/src/instance/RangeControl.ts +15 -1
  63. package/src/instance/RankControl.ts +17 -2
  64. package/src/instance/Root.ts +5 -6
  65. package/src/instance/SelectControl.ts +16 -2
  66. package/src/instance/TriggerControl.ts +15 -1
  67. package/src/instance/UploadControl.ts +9 -8
  68. package/src/instance/abstract/DescendantNode.ts +4 -8
  69. package/src/instance/abstract/InstanceNode.ts +7 -5
  70. package/src/instance/abstract/ValueNode.ts +2 -1
  71. package/src/instance/attachments/buildAttributes.ts +15 -4
  72. package/src/instance/children/childrenInitOptions.ts +2 -1
  73. package/src/instance/children/normalizeChildInitOptions.ts +1 -1
  74. package/src/instance/hierarchy.ts +2 -2
  75. package/src/instance/internal-api/AttributeContext.ts +6 -0
  76. package/src/instance/internal-api/InstanceConfig.ts +6 -1
  77. package/src/instance/internal-api/InstanceValueContext.ts +6 -0
  78. package/src/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.ts +0 -1
  79. package/src/instance/internal-api/serialization/ClientReactiveSerializableValueNode.ts +4 -0
  80. package/src/instance/repeat/RepeatInstance.ts +3 -4
  81. package/src/integration/xpath/adapter/XFormsXPathNode.ts +1 -0
  82. package/src/integration/xpath/adapter/engineDOMAdapter.ts +2 -2
  83. package/src/integration/xpath/adapter/kind.ts +6 -1
  84. package/src/integration/xpath/adapter/names.ts +1 -0
  85. package/src/integration/xpath/adapter/traversal.ts +5 -6
  86. package/src/integration/xpath/static-dom/StaticAttribute.ts +1 -0
  87. package/src/lib/client-reactivity/instance-state/createValueNodeInstanceState.ts +2 -1
  88. package/src/lib/client-reactivity/instance-state/prepareInstancePayload.ts +1 -0
  89. package/src/lib/codecs/NoteCodec.ts +1 -1
  90. package/src/lib/codecs/items/SingleValueItemCodec.ts +1 -3
  91. package/src/lib/reactivity/createInstanceValueState.ts +177 -52
  92. package/src/lib/reactivity/node-state/createSharedNodeState.ts +2 -2
  93. package/src/lib/xml-serialization.ts +9 -1
  94. package/src/parse/XFormDOM.ts +9 -0
  95. package/src/parse/body/GroupElementDefinition.ts +1 -1
  96. package/src/parse/body/control/InputControlDefinition.ts +1 -1
  97. package/src/parse/expression/ActionComputationExpression.ts +12 -0
  98. package/src/parse/model/ActionDefinition.ts +70 -0
  99. package/src/parse/model/AttributeDefinition.ts +10 -2
  100. package/src/parse/model/AttributeDefinitionMap.ts +1 -1
  101. package/src/parse/model/BindDefinition.ts +1 -6
  102. package/src/parse/model/BindPreloadDefinition.ts +44 -12
  103. package/src/parse/model/Event.ts +9 -0
  104. package/src/parse/model/LeafNodeDefinition.ts +5 -1
  105. package/src/parse/model/ModelActionMap.ts +37 -0
  106. package/src/parse/model/ModelDefinition.ts +18 -3
  107. package/src/parse/model/NoteNodeDefinition.ts +5 -2
  108. package/src/parse/model/RangeNodeDefinition.ts +5 -2
  109. package/src/parse/model/RootDefinition.ts +22 -4
  110. package/dist/lib/reactivity/createAttributeValueState.d.ts +0 -15
  111. package/src/lib/reactivity/createAttributeValueState.ts +0 -189
@@ -1,5 +1,7 @@
1
1
  import type { InstanceState } from '../../../client/serialization/InstanceState.ts';
2
2
  import type { QualifiedName } from '../../../lib/names/QualifiedName.ts';
3
+ import type { BindDefinition } from '../../../parse/model/BindDefinition.ts';
4
+ import type { Attribute } from '../../Attribute.ts';
3
5
  import type {
4
6
  ClientReactiveSerializableChildNode,
5
7
  ClientReactiveSerializableParentNode,
@@ -14,10 +16,12 @@ interface ClientReactiveSerializableValueNodeCurrentState {
14
16
  * @todo Consider moving into {@link InstanceState}
15
17
  */
16
18
  get instanceValue(): SerializedInstanceValue;
19
+ get attributes(): readonly Attribute[];
17
20
  }
18
21
 
19
22
  interface ClientReactiveSerializableValueNodeDefinition {
20
23
  readonly qualifiedName: QualifiedName;
24
+ readonly bind: BindDefinition;
21
25
  }
22
26
 
23
27
  export interface ClientReactiveSerializableValueNode {
@@ -135,10 +135,9 @@ export class RepeatInstance
135
135
  this.appearances = definition.bodyElement.appearances;
136
136
 
137
137
  const childrenState = createChildrenState<RepeatInstance, GeneralChildNode>(this);
138
- const attributeState = createAttributeState(this.scope);
138
+ this.attributeState = createAttributeState(this.scope);
139
139
 
140
140
  this.childrenState = childrenState;
141
- this.attributeState = attributeState;
142
141
  this.currentIndex = currentIndex;
143
142
 
144
143
  const state = createSharedNodeState(
@@ -152,7 +151,7 @@ export class RepeatInstance
152
151
  // TODO: only-child <group><label>
153
152
  label: createNodeLabel(this, definition),
154
153
  hint: null,
155
- attributes: attributeState.getAttributes,
154
+ attributes: this.attributeState.getAttributes,
156
155
  children: childrenState.childIds,
157
156
  valueOptions: null,
158
157
  value: null,
@@ -198,7 +197,7 @@ export class RepeatInstance
198
197
  return this.childrenState.getChildren();
199
198
  }
200
199
 
201
- getAttributes(): readonly Attribute[] {
200
+ override getAttributes(): readonly Attribute[] {
202
201
  return this.attributeState.getAttributes();
203
202
  }
204
203
  }
@@ -120,6 +120,7 @@ export interface XFormsXPathPrimaryInstanceNode extends XFormsXPathNode {
120
120
  // prettier-ignore
121
121
  export type XFormsXPathPrimaryInstanceDescendantNodeKind =
122
122
  | XFormsXPathNodeRangeKind
123
+ | XPathAttributeKind
123
124
  | XPathElementKind
124
125
  | XPathTextKind;
125
126
 
@@ -10,10 +10,10 @@ import {
10
10
  } from './names.ts';
11
11
  import {
12
12
  compareDocumentOrder,
13
+ getAttributes,
13
14
  getChildElements,
14
15
  getChildNodes,
15
16
  getContainingEngineXPathDocument,
16
- getEngineXPathAttributes,
17
17
  getNamespaceDeclarations,
18
18
  getNextSiblingElement,
19
19
  getNextSiblingNode,
@@ -43,7 +43,7 @@ export const engineDOMAdapter: EngineDOMAdapter = {
43
43
 
44
44
  // XPathTraversalAdapter
45
45
  compareDocumentOrder,
46
- getAttributes: getEngineXPathAttributes,
46
+ getAttributes: getAttributes,
47
47
  getChildElements: getChildElements,
48
48
  getChildNodes: getChildNodes,
49
49
  getContainingDocument: getContainingEngineXPathDocument,
@@ -1,5 +1,6 @@
1
1
  import type { XPathNodeKind } from '@getodk/xpath';
2
2
  import { XPathNodeKindKey } from '@getodk/xpath';
3
+ import type { Attribute } from '../../../instance/Attribute.ts';
3
4
  import type { AnyChildNode, AnyNode, AnyParentNode } from '../../../instance/hierarchy.ts';
4
5
  import type { PrimaryInstance } from '../../../instance/PrimaryInstance.ts';
5
6
  import type { StaticAttribute } from '../static-dom/StaticAttribute.ts';
@@ -7,6 +8,7 @@ import type { StaticDocument } from '../static-dom/StaticDocument.ts';
7
8
  import type { StaticElement } from '../static-dom/StaticElement.ts';
8
9
  import type { StaticText } from '../static-dom/StaticText.ts';
9
10
  import type {
11
+ XFormsXPathAttribute,
10
12
  XFormsXPathComment,
11
13
  XFormsXPathDocument,
12
14
  XFormsXPathElement,
@@ -17,12 +19,15 @@ export type PrimaryInstanceXPathNode = Extract<AnyNode, XFormsXPathPrimaryInstan
17
19
 
18
20
  export type PrimaryInstanceXPathElement = Extract<AnyChildNode, XFormsXPathElement>;
19
21
 
22
+ export type PrimaryInstanceXPathAttribute = Extract<Attribute, XFormsXPathAttribute>;
23
+
20
24
  export type PrimaryInstanceXPathComment = Extract<AnyChildNode, XFormsXPathComment>;
21
25
 
22
26
  // prettier-ignore
23
27
  export type PrimaryInstanceXPathChildNode =
24
28
  // eslint-disable-next-line @typescript-eslint/sort-type-constituents
25
29
  | PrimaryInstanceXPathElement
30
+ | PrimaryInstanceXPathAttribute
26
31
  | PrimaryInstanceXPathComment;
27
32
 
28
33
  // prettier-ignore
@@ -41,7 +46,7 @@ export type EngineXPathComment =
41
46
 
42
47
  // Giving this a type alias anticipates eventually implementing attributes
43
48
  // in primary instance state as well
44
- export type EngineXPathAttribute = StaticAttribute;
49
+ export type EngineXPathAttribute = PrimaryInstanceXPathAttribute | StaticAttribute;
45
50
 
46
51
  export type EngineXPathText = StaticText;
47
52
 
@@ -128,6 +128,7 @@ export const resolveEngineXPathNodeNamespaceURI = (
128
128
  case 'static-text':
129
129
  return resolveNamespaceURIFromStaticNodeContext(node, prefix);
130
130
 
131
+ case 'attribute':
131
132
  case 'group':
132
133
  case 'input':
133
134
  case 'model-value':
@@ -8,7 +8,6 @@ import type {
8
8
  EngineXPathDocument,
9
9
  EngineXPathElement,
10
10
  EngineXPathNode,
11
- EngineXPathParentNode,
12
11
  PrimaryInstanceXPathChildNode,
13
12
  XFormsXPathChildNode,
14
13
  } from './kind.ts';
@@ -18,13 +17,13 @@ export const getContainingEngineXPathDocument = (node: EngineXPathNode): EngineX
18
17
  return node.rootDocument;
19
18
  };
20
19
 
21
- export const getEngineXPathAttributes = (
22
- node: EngineXPathNode
23
- ): readonly EngineXPathAttribute[] => {
20
+ export const getAttributes = (node: EngineXPathNode): readonly EngineXPathAttribute[] => {
24
21
  if (node.nodeType === 'static-element') {
25
22
  return node.attributes;
26
23
  }
27
-
24
+ if (isEngineXPathElement(node)) {
25
+ return node.getAttributes();
26
+ }
28
27
  return [];
29
28
  };
30
29
 
@@ -43,7 +42,7 @@ export const getEngineXPathAttributes = (
43
42
  */
44
43
  export const getNamespaceDeclarations = (): readonly [] => [];
45
44
 
46
- export const getParentNode = (node: EngineXPathNode): EngineXPathParentNode | null => {
45
+ export const getParentNode = (node: EngineXPathNode): EngineXPathNode | null => {
47
46
  if (node.nodeType === 'repeat-instance') {
48
47
  return node.parent.parent;
49
48
  }
@@ -20,6 +20,7 @@ export class StaticAttribute extends StaticNode<'attribute'> implements XFormsXP
20
20
  readonly nodeset: string;
21
21
  readonly attributes = [] as const;
22
22
  readonly children = null;
23
+ readonly childElements = [];
23
24
  readonly value: string;
24
25
 
25
26
  constructor(
@@ -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
@@ -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,104 @@ 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
- });
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
+ };
146
201
 
147
- createComputed(() => {
148
- if (context.isAttached() && context.isRelevant()) {
149
- const calculated = calculate();
150
- const value = context.decodeInstanceValue(calculated);
202
+ /**
203
+ * Runs the computation without maintaining a reactive listener, so
204
+ * actions that should run only at a specific time are not triggered
205
+ * when referenced elements are updated.
206
+ */
207
+ const createActionCalculation = (
208
+ context: ValueContext,
209
+ setRelevantValue: SimpleAtomicStateSetter<string>,
210
+ computation: ActionComputationExpression<'string'>
211
+ ): void => {
212
+ createComputed(() => {
213
+ if (context.isAttached()) {
214
+ // use untrack so the expression evaluation isn't reactive
215
+ const relevant = untrack(() => context.isRelevant());
216
+ if (!relevant) {
217
+ return;
218
+ }
219
+ const calculated = untrack(() => {
220
+ return context.evaluator.evaluateString(computation.expression, context);
221
+ });
222
+ const value = context.decodeInstanceValue(calculated);
223
+ setRelevantValue(value);
224
+ }
225
+ });
226
+ };
151
227
 
152
- setRelevantValue(value);
228
+ const createValueChangedCalculation = (
229
+ context: ValueContext,
230
+ setRelevantValue: SimpleAtomicStateSetter<string>,
231
+ action: ActionDefinition
232
+ ): void => {
233
+ const { source, ref } = bindToRepeatInstance(context, action);
234
+ if (!source) {
235
+ // no element to listen to
236
+ return;
237
+ }
238
+ let previous = '';
239
+ const sourceElementExpression = new ActionComputationExpression('string', source);
240
+ const calculateValueSource = createComputedExpression(context, sourceElementExpression); // registers listener
241
+ createComputed(() => {
242
+ if (context.isAttached() && context.isRelevant()) {
243
+ const valueSource = calculateValueSource();
244
+ if (previous !== valueSource) {
245
+ // only update if value has changed
246
+ if (referencesCurrentNode(context, ref)) {
247
+ const calc = context.evaluator.evaluateString(action.computation.expression, context);
248
+ const value = context.decodeInstanceValue(calc);
249
+ setRelevantValue(value);
250
+ }
153
251
  }
154
- });
252
+ previous = valueSource;
253
+ }
155
254
  });
156
255
  };
157
256
 
257
+ const registerAction = (
258
+ context: ValueContext,
259
+ setValue: SimpleAtomicStateSetter<string>,
260
+ action: ActionDefinition
261
+ ) => {
262
+ if (action.events.includes(XFORM_EVENT.odkInstanceFirstLoad)) {
263
+ if (isInstanceFirstLoad(context)) {
264
+ createActionCalculation(context, setValue, action.computation);
265
+ }
266
+ }
267
+ if (action.events.includes(XFORM_EVENT.odkInstanceLoad)) {
268
+ if (!isAddingRepeatChild(context)) {
269
+ createActionCalculation(context, setValue, action.computation);
270
+ }
271
+ }
272
+ if (action.events.includes(XFORM_EVENT.odkNewRepeat)) {
273
+ if (isAddingRepeatChild(context)) {
274
+ createActionCalculation(context, setValue, action.computation);
275
+ }
276
+ }
277
+ if (action.events.includes(XFORM_EVENT.xformsValueChanged)) {
278
+ createValueChangedCalculation(context, setValue, action);
279
+ }
280
+ };
281
+
158
282
  export type InstanceValueState = SimpleAtomicState<string>;
159
283
 
160
284
  /**
@@ -168,25 +292,26 @@ export type InstanceValueState = SimpleAtomicState<string>;
168
292
  * nodes defined with one
169
293
  * - prevents downstream writes to nodes in a readonly state
170
294
  */
171
- export const createInstanceValueState = (context: InstanceValueContext): InstanceValueState => {
295
+ export const createInstanceValueState = (context: ValueContext): InstanceValueState => {
172
296
  return context.scope.runTask(() => {
173
297
  const initialValue = getInitialValue(context);
174
298
  const baseValueState = createSignal(initialValue);
175
299
  const relevantValueState = createRelevantValueState(context, baseValueState);
176
300
 
177
- /**
178
- * @see {@link setPreloadUIDValue} for important details about spec ordering of events and computations.
179
- */
180
- setPreloadUIDValue(context, relevantValueState);
301
+ const [, setValue] = relevantValueState;
181
302
 
182
- const { calculate } = context.definition.bind;
303
+ preloadValue(context, setValue);
183
304
 
305
+ const { calculate } = context.definition.bind;
184
306
  if (calculate != null) {
185
- const [, setValue] = relevantValueState;
186
-
187
307
  createCalculation(context, setValue, calculate);
188
308
  }
189
309
 
310
+ const action = context.definition.model.actions.get(context.contextReference());
311
+ if (action) {
312
+ registerAction(context, setValue, action);
313
+ }
314
+
190
315
  return guardDownstreamReadonlyWrites(context, relevantValueState);
191
316
  });
192
317
  };
@@ -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,
@@ -153,7 +153,15 @@ export const serializeParentElementXML = (
153
153
  export const serializeLeafElementXML = (
154
154
  qualifiedName: QualifiedName,
155
155
  xmlValue: EscapedXMLText,
156
+ attributes: readonly Attribute[],
156
157
  namespaceDeclarations?: NamespaceDeclarationMap
157
158
  ): string => {
158
- return serializeElementXML(qualifiedName, xmlValue.normalize(), '', namespaceDeclarations);
159
+ const serializedAttributes =
160
+ attributes?.map((attribute) => attribute.instanceState.instanceXML).join('') ?? '';
161
+ return serializeElementXML(
162
+ qualifiedName,
163
+ xmlValue.normalize(),
164
+ serializedAttributes,
165
+ namespaceDeclarations
166
+ );
159
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
+ }