@getodk/xforms-engine 0.15.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 (94) hide show
  1. package/dist/client/AttributeNode.d.ts +2 -5
  2. package/dist/client/BaseNode.d.ts +5 -0
  3. package/dist/client/form/FormInstanceConfig.d.ts +15 -0
  4. package/dist/index.js +395 -231
  5. package/dist/index.js.map +1 -1
  6. package/dist/instance/Attribute.d.ts +17 -11
  7. package/dist/instance/Group.d.ts +0 -2
  8. package/dist/instance/InputControl.d.ts +2 -0
  9. package/dist/instance/PrimaryInstance.d.ts +0 -2
  10. package/dist/instance/Root.d.ts +0 -2
  11. package/dist/instance/UploadControl.d.ts +0 -2
  12. package/dist/instance/abstract/InstanceNode.d.ts +2 -2
  13. package/dist/instance/abstract/ValueNode.d.ts +2 -1
  14. package/dist/instance/attachments/buildAttributes.d.ts +6 -2
  15. package/dist/instance/hierarchy.d.ts +1 -2
  16. package/dist/instance/internal-api/AttributeContext.d.ts +6 -0
  17. package/dist/instance/internal-api/InstanceConfig.d.ts +2 -0
  18. package/dist/instance/internal-api/InstanceValueContext.d.ts +6 -0
  19. package/dist/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.d.ts +0 -1
  20. package/dist/instance/internal-api/serialization/ClientReactiveSerializableValueNode.d.ts +4 -0
  21. package/dist/instance/repeat/RepeatInstance.d.ts +0 -2
  22. package/dist/lib/codecs/items/SingleValueItemCodec.d.ts +1 -1
  23. package/dist/lib/reactivity/createInstanceValueState.d.ts +4 -1
  24. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +2 -2
  25. package/dist/lib/xml-serialization.d.ts +1 -1
  26. package/dist/parse/XFormDOM.d.ts +3 -0
  27. package/dist/parse/expression/ActionComputationExpression.d.ts +4 -0
  28. package/dist/parse/model/ActionDefinition.d.ts +15 -0
  29. package/dist/parse/model/AttributeDefinition.d.ts +3 -1
  30. package/dist/parse/model/BindPreloadDefinition.d.ts +6 -10
  31. package/dist/parse/model/Event.d.ts +8 -0
  32. package/dist/parse/model/LeafNodeDefinition.d.ts +5 -2
  33. package/dist/parse/model/ModelActionMap.d.ts +9 -0
  34. package/dist/parse/model/ModelDefinition.d.ts +8 -1
  35. package/dist/parse/model/NoteNodeDefinition.d.ts +3 -2
  36. package/dist/parse/model/RangeNodeDefinition.d.ts +2 -1
  37. package/dist/parse/model/RootDefinition.d.ts +1 -0
  38. package/dist/solid.js +395 -231
  39. package/dist/solid.js.map +1 -1
  40. package/package.json +21 -17
  41. package/src/client/AttributeNode.ts +2 -5
  42. package/src/client/BaseNode.ts +6 -0
  43. package/src/client/form/FormInstanceConfig.ts +17 -0
  44. package/src/client/validation.ts +1 -1
  45. package/src/entrypoints/FormInstance.ts +1 -0
  46. package/src/instance/Attribute.ts +45 -45
  47. package/src/instance/Group.ts +1 -10
  48. package/src/instance/InputControl.ts +8 -1
  49. package/src/instance/ModelValue.ts +7 -1
  50. package/src/instance/Note.ts +6 -1
  51. package/src/instance/PrimaryInstance.ts +1 -11
  52. package/src/instance/RangeControl.ts +6 -1
  53. package/src/instance/RankControl.ts +7 -1
  54. package/src/instance/Root.ts +1 -10
  55. package/src/instance/SelectControl.ts +6 -1
  56. package/src/instance/TriggerControl.ts +6 -1
  57. package/src/instance/UploadControl.ts +1 -10
  58. package/src/instance/abstract/DescendantNode.ts +0 -5
  59. package/src/instance/abstract/InstanceNode.ts +2 -2
  60. package/src/instance/abstract/ValueNode.ts +2 -1
  61. package/src/instance/attachments/buildAttributes.ts +11 -4
  62. package/src/instance/children/normalizeChildInitOptions.ts +1 -1
  63. package/src/instance/hierarchy.ts +1 -3
  64. package/src/instance/internal-api/AttributeContext.ts +6 -0
  65. package/src/instance/internal-api/InstanceConfig.ts +6 -1
  66. package/src/instance/internal-api/InstanceValueContext.ts +6 -0
  67. package/src/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.ts +0 -1
  68. package/src/instance/internal-api/serialization/ClientReactiveSerializableValueNode.ts +4 -0
  69. package/src/instance/repeat/RepeatInstance.ts +1 -10
  70. package/src/lib/client-reactivity/instance-state/createValueNodeInstanceState.ts +2 -1
  71. package/src/lib/client-reactivity/instance-state/prepareInstancePayload.ts +1 -0
  72. package/src/lib/codecs/NoteCodec.ts +1 -1
  73. package/src/lib/codecs/items/SingleValueItemCodec.ts +1 -3
  74. package/src/lib/reactivity/createInstanceValueState.ts +152 -53
  75. package/src/lib/reactivity/node-state/createSharedNodeState.ts +2 -2
  76. package/src/lib/xml-serialization.ts +9 -1
  77. package/src/parse/XFormDOM.ts +9 -0
  78. package/src/parse/body/GroupElementDefinition.ts +1 -1
  79. package/src/parse/body/control/InputControlDefinition.ts +1 -1
  80. package/src/parse/expression/ActionComputationExpression.ts +12 -0
  81. package/src/parse/model/ActionDefinition.ts +70 -0
  82. package/src/parse/model/AttributeDefinition.ts +3 -2
  83. package/src/parse/model/AttributeDefinitionMap.ts +1 -1
  84. package/src/parse/model/BindDefinition.ts +1 -6
  85. package/src/parse/model/BindPreloadDefinition.ts +44 -12
  86. package/src/parse/model/Event.ts +9 -0
  87. package/src/parse/model/LeafNodeDefinition.ts +5 -1
  88. package/src/parse/model/ModelActionMap.ts +37 -0
  89. package/src/parse/model/ModelDefinition.ts +18 -3
  90. package/src/parse/model/NoteNodeDefinition.ts +5 -2
  91. package/src/parse/model/RangeNodeDefinition.ts +5 -2
  92. package/src/parse/model/RootDefinition.ts +22 -4
  93. package/dist/lib/reactivity/createAttributeValueState.d.ts +0 -15
  94. package/src/lib/reactivity/createAttributeValueState.ts +0 -189
@@ -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,
@@ -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
+ }
@@ -0,0 +1,70 @@
1
+ import { isTextNode } from '@getodk/common/lib/dom/predicates.ts';
2
+ import { ActionComputationExpression } from '../expression/ActionComputationExpression.ts';
3
+ import { type XFormEvent, XFORM_EVENT } from './Event.ts';
4
+ import type { ModelDefinition } from './ModelDefinition.ts';
5
+
6
+ export class ActionDefinition {
7
+ static getRef(model: ModelDefinition, setValueElement: Element): string | null {
8
+ if (setValueElement.hasAttribute('ref')) {
9
+ return setValueElement.getAttribute('ref') ?? null;
10
+ }
11
+ if (setValueElement.hasAttribute('bind')) {
12
+ const bindId = setValueElement.getAttribute('bind');
13
+ const bindDefinition = Array.from(model.binds.values()).find((definition) => {
14
+ return definition.bindElement.getAttribute('id') === bindId;
15
+ });
16
+ return bindDefinition?.nodeset ?? null;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ static getValue(element: Element): string {
22
+ if (element.hasAttribute('value')) {
23
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
24
+ return element.getAttribute('value') || "''";
25
+ }
26
+ if (element.firstChild && isTextNode(element.firstChild)) {
27
+ // use the text content as the literal value
28
+ return `'${element.firstChild.nodeValue}'`;
29
+ }
30
+ return "''";
31
+ }
32
+
33
+ static isKnownEvent = (event: XFormEvent): event is XFormEvent => {
34
+ return Object.values(XFORM_EVENT).includes(event);
35
+ };
36
+
37
+ static getEvents(element: Element): XFormEvent[] {
38
+ const events = element.getAttribute('event')?.split(' ') ?? [];
39
+ const unknownEvents = events.filter((event) => !this.isKnownEvent(event as XFormEvent));
40
+ if (unknownEvents.length) {
41
+ throw new Error(
42
+ `An action was registered for unsupported events: ${unknownEvents.join(', ')}`
43
+ );
44
+ }
45
+ return events as XFormEvent[];
46
+ }
47
+
48
+ readonly ref: string;
49
+ readonly events: XFormEvent[];
50
+ readonly computation: ActionComputationExpression<'string'>;
51
+ readonly source: string | undefined;
52
+
53
+ constructor(
54
+ model: ModelDefinition,
55
+ readonly element: Element,
56
+ source?: string
57
+ ) {
58
+ const ref = ActionDefinition.getRef(model, element);
59
+ if (!ref) {
60
+ throw new Error(
61
+ 'Invalid setvalue element - you must define either "ref" or "bind" attribute'
62
+ );
63
+ }
64
+ this.ref = ref;
65
+ this.events = ActionDefinition.getEvents(element);
66
+ const value = ActionDefinition.getValue(element);
67
+ this.computation = new ActionComputationExpression('string', value);
68
+ this.source = source;
69
+ }
70
+ }
@@ -7,6 +7,7 @@ import {
7
7
  import { QualifiedName } from '../../lib/names/QualifiedName.ts';
8
8
  import { escapeXMLText, serializeAttributeXML } from '../../lib/xml-serialization.ts';
9
9
  import type { BindDefinition } from './BindDefinition.ts';
10
+ import type { ModelDefinition } from './ModelDefinition.ts';
10
11
  import { NodeDefinition } from './NodeDefinition.ts';
11
12
  import type { RootDefinition } from './RootDefinition.ts';
12
13
 
@@ -29,7 +30,7 @@ export class AttributeDefinition
29
30
  readonly qualifiedName: QualifiedName;
30
31
 
31
32
  constructor(
32
- root: RootDefinition,
33
+ readonly model: ModelDefinition,
33
34
  bind: BindDefinition,
34
35
  readonly template: StaticAttribute
35
36
  ) {
@@ -37,7 +38,7 @@ export class AttributeDefinition
37
38
 
38
39
  const { value } = template;
39
40
 
40
- this.root = root;
41
+ this.root = model.root;
41
42
 
42
43
  this.value = value;
43
44
  this.qualifiedName = template.qualifiedName;
@@ -28,7 +28,7 @@ export class AttributeDefinitionMap extends Map<QualifiedName, AttributeDefiniti
28
28
  const nonNamespaceAttributes = instanceNode.attributes.filter(isNonNamespaceAttribute);
29
29
  const definitions = nonNamespaceAttributes.map((attribute) => {
30
30
  const bind = model.binds.getOrCreateBindDefinition(attribute.nodeset);
31
- return new AttributeDefinition(model.root, bind, attribute);
31
+ return new AttributeDefinition(model, bind, attribute);
32
32
  });
33
33
  return new this(definitions);
34
34
  }
@@ -38,10 +38,7 @@ export class BindDefinition<T extends BindType = BindType> extends DependencyCon
38
38
  // https://github.com/getodk/collect/issues/3758 mentions deprecation.
39
39
  readonly saveIncomplete: BindComputationExpression<'saveIncomplete'>;
40
40
 
41
- // TODO: these are deferred until prioritized
42
- // readonly preload: string | null;
43
- // readonly preloadParams: string | null;
44
- // readonly 'max-pixels': string | null;
41
+ // TODO: deferred until prioritized: readonly 'max-pixels': string | null;
45
42
 
46
43
  protected _parentBind: BindDefinition | null | undefined;
47
44
 
@@ -95,8 +92,6 @@ export class BindDefinition<T extends BindType = BindType> extends DependencyCon
95
92
  this.constraintMsg = MessageDefinition.from(this, 'constraintMsg');
96
93
  this.requiredMsg = MessageDefinition.from(this, 'requiredMsg');
97
94
 
98
- // this.preload = BindComputation.forExpression(this, 'preload');
99
- // this.preloadParams = BindComputation.forExpression(this, 'preloadParams');
100
95
  // this['max-pixels'] = BindComputation.forExpression(this, 'max-pixels');
101
96
  }
102
97
 
@@ -1,10 +1,18 @@
1
1
  import { JAVAROSA_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
2
2
  import type { PartiallyKnownString } from '@getodk/common/types/string/PartiallyKnownString.ts';
3
+ import type { AttributeContext } from '../../instance/internal-api/AttributeContext.ts';
4
+ import type { InstanceValueContext } from '../../instance/internal-api/InstanceValueContext.ts';
3
5
  import type { BindElement } from './BindElement.ts';
6
+ import { XFORM_EVENT, type XFormEvent } from './Event.ts';
4
7
 
5
- type PartiallyKnownPreloadParameter<Known extends string> =
6
- // eslint-disable-next-line @typescript-eslint/sort-type-constituents
7
- PartiallyKnownString<NonNullable<Known>> | Extract<Known, null>;
8
+ /**
9
+ * Per {@link https://getodk.github.io/xforms-spec/#preload-attributes:~:text=concatenation%20of%20%E2%80%98uuid%3A%E2%80%99%20and%20uuid()}
10
+ */
11
+ const PRELOAD_UID_EXPRESSION = 'concat("uuid:", uuid())';
12
+
13
+ type PartiallyKnownPreloadParameter<Known extends string> = PartiallyKnownString<
14
+ NonNullable<Known>
15
+ >;
8
16
 
9
17
  interface PreloadParametersByType {
10
18
  readonly uid: string | null;
@@ -61,15 +69,6 @@ const getPreloadInput = (bindElement: BindElement): AnyPreloadInput | null => {
61
69
  *
62
70
  * - {@link type}, a `jr:preload`
63
71
  * - {@link parameter}, an associated `jr:preloadParams` value
64
- *
65
- * @todo It would probably make sense for the _definition_ to also convey:
66
- *
67
- * 1. Which {@link https://getodk.github.io/xforms-spec/#events | event} the
68
- * preload is semantically associated with (noting that the spec may be a tad
69
- * overzealous about this association).
70
- *
71
- * 2. The constant XPath expression (or other computation?) expressed by the
72
- * combined {@link type} and {@link parameter}.
73
72
  */
74
73
  export class BindPreloadDefinition<Type extends PreloadType> implements PreloadInput<Type> {
75
74
  static from(bindElement: BindElement): AnyBindPreloadDefinition | null {
@@ -84,10 +83,43 @@ export class BindPreloadDefinition<Type extends PreloadType> implements PreloadI
84
83
 
85
84
  readonly type: Type;
86
85
  readonly parameter: PreloadParameter<Type>;
86
+ readonly event: XFormEvent;
87
+
88
+ getValue(context: AttributeContext | InstanceValueContext): string | undefined {
89
+ if (this.type === 'uid') {
90
+ return context.evaluator.evaluateString(PRELOAD_UID_EXPRESSION);
91
+ }
92
+ if (this.type === 'timestamp') {
93
+ return context.evaluator.evaluateString('now()');
94
+ }
95
+ if (this.type === 'date') {
96
+ return context.evaluator.evaluateString('today()');
97
+ }
98
+ if (this.type === 'property') {
99
+ const properties = context.instanceConfig.preloadProperties;
100
+ if (this.parameter === 'deviceid') {
101
+ return properties.deviceID;
102
+ }
103
+ if (this.parameter === 'email') {
104
+ return properties.email;
105
+ }
106
+ if (this.parameter === 'phonenumber') {
107
+ return properties.phoneNumber;
108
+ }
109
+ if (this.parameter === 'username') {
110
+ return properties.username;
111
+ }
112
+ }
113
+ return;
114
+ }
87
115
 
88
116
  private constructor(input: PreloadInput<Type>) {
89
117
  this.type = input.type;
90
118
  this.parameter = input.parameter;
119
+ this.event =
120
+ this.type === 'timestamp' && this.parameter === 'end'
121
+ ? XFORM_EVENT.xformsRevalidate
122
+ : XFORM_EVENT.odkInstanceFirstLoad;
91
123
  }
92
124
  }
93
125
 
@@ -0,0 +1,9 @@
1
+ export const XFORM_EVENT = {
2
+ odkInstanceLoad: 'odk-instance-load',
3
+ odkInstanceFirstLoad: 'odk-instance-first-load',
4
+ odkNewRepeat: 'odk-new-repeat',
5
+ xformsRevalidate: 'xforms-revalidate',
6
+ xformsValueChanged: 'xforms-value-changed',
7
+ } as const;
8
+
9
+ export type XFormEvent = (typeof XFORM_EVENT)[keyof typeof XFORM_EVENT];
@@ -6,8 +6,10 @@ import {
6
6
  } from '../../lib/names/NamespaceDeclarationMap.ts';
7
7
  import { QualifiedName } from '../../lib/names/QualifiedName.ts';
8
8
  import type { AnyBodyElementDefinition, ControlElementDefinition } from '../body/BodyDefinition.ts';
9
+ import { AttributeDefinitionMap } from './AttributeDefinitionMap.ts';
9
10
  import type { BindDefinition } from './BindDefinition.ts';
10
11
  import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts';
12
+ import type { ModelDefinition } from './ModelDefinition.ts';
11
13
  import type { ParentNodeDefinition } from './NodeDefinition.ts';
12
14
 
13
15
  export class LeafNodeDefinition<V extends ValueType = ValueType>
@@ -20,9 +22,10 @@ export class LeafNodeDefinition<V extends ValueType = ValueType>
20
22
  readonly namespaceDeclarations: NamespaceDeclarationMap;
21
23
  readonly qualifiedName: QualifiedName;
22
24
  readonly children = null;
23
- readonly attributes = null;
25
+ readonly attributes: AttributeDefinitionMap;
24
26
 
25
27
  constructor(
28
+ readonly model: ModelDefinition,
26
29
  parent: ParentNodeDefinition,
27
30
  bind: BindDefinition,
28
31
  bodyElement: AnyBodyElementDefinition | null,
@@ -37,6 +40,7 @@ export class LeafNodeDefinition<V extends ValueType = ValueType>
37
40
  this.valueType = bind.type.resolved satisfies ValueType as V;
38
41
  this.qualifiedName = template.qualifiedName;
39
42
  this.namespaceDeclarations = new NamespaceDeclarationMap(this);
43
+ this.attributes = AttributeDefinitionMap.from(model, template);
40
44
  }
41
45
 
42
46
  toJSON() {