@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.
- package/dist/client/AttributeNode.d.ts +2 -5
- package/dist/client/BaseNode.d.ts +5 -0
- package/dist/client/form/FormInstanceConfig.d.ts +15 -0
- package/dist/index.js +395 -231
- package/dist/index.js.map +1 -1
- package/dist/instance/Attribute.d.ts +17 -11
- package/dist/instance/Group.d.ts +0 -2
- package/dist/instance/InputControl.d.ts +2 -0
- package/dist/instance/PrimaryInstance.d.ts +0 -2
- package/dist/instance/Root.d.ts +0 -2
- package/dist/instance/UploadControl.d.ts +0 -2
- package/dist/instance/abstract/InstanceNode.d.ts +2 -2
- package/dist/instance/abstract/ValueNode.d.ts +2 -1
- package/dist/instance/attachments/buildAttributes.d.ts +6 -2
- package/dist/instance/hierarchy.d.ts +1 -2
- package/dist/instance/internal-api/AttributeContext.d.ts +6 -0
- package/dist/instance/internal-api/InstanceConfig.d.ts +2 -0
- package/dist/instance/internal-api/InstanceValueContext.d.ts +6 -0
- package/dist/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.d.ts +0 -1
- package/dist/instance/internal-api/serialization/ClientReactiveSerializableValueNode.d.ts +4 -0
- package/dist/instance/repeat/RepeatInstance.d.ts +0 -2
- package/dist/lib/codecs/items/SingleValueItemCodec.d.ts +1 -1
- package/dist/lib/reactivity/createInstanceValueState.d.ts +4 -1
- package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +2 -2
- package/dist/lib/xml-serialization.d.ts +1 -1
- package/dist/parse/XFormDOM.d.ts +3 -0
- package/dist/parse/expression/ActionComputationExpression.d.ts +4 -0
- package/dist/parse/model/ActionDefinition.d.ts +15 -0
- package/dist/parse/model/AttributeDefinition.d.ts +3 -1
- package/dist/parse/model/BindPreloadDefinition.d.ts +6 -10
- package/dist/parse/model/Event.d.ts +8 -0
- package/dist/parse/model/LeafNodeDefinition.d.ts +5 -2
- package/dist/parse/model/ModelActionMap.d.ts +9 -0
- package/dist/parse/model/ModelDefinition.d.ts +8 -1
- package/dist/parse/model/NoteNodeDefinition.d.ts +3 -2
- package/dist/parse/model/RangeNodeDefinition.d.ts +2 -1
- package/dist/parse/model/RootDefinition.d.ts +1 -0
- package/dist/solid.js +395 -231
- package/dist/solid.js.map +1 -1
- package/package.json +21 -17
- package/src/client/AttributeNode.ts +2 -5
- package/src/client/BaseNode.ts +6 -0
- package/src/client/form/FormInstanceConfig.ts +17 -0
- package/src/client/validation.ts +1 -1
- package/src/entrypoints/FormInstance.ts +1 -0
- package/src/instance/Attribute.ts +45 -45
- package/src/instance/Group.ts +1 -10
- package/src/instance/InputControl.ts +8 -1
- package/src/instance/ModelValue.ts +7 -1
- package/src/instance/Note.ts +6 -1
- package/src/instance/PrimaryInstance.ts +1 -11
- package/src/instance/RangeControl.ts +6 -1
- package/src/instance/RankControl.ts +7 -1
- package/src/instance/Root.ts +1 -10
- package/src/instance/SelectControl.ts +6 -1
- package/src/instance/TriggerControl.ts +6 -1
- package/src/instance/UploadControl.ts +1 -10
- package/src/instance/abstract/DescendantNode.ts +0 -5
- package/src/instance/abstract/InstanceNode.ts +2 -2
- package/src/instance/abstract/ValueNode.ts +2 -1
- package/src/instance/attachments/buildAttributes.ts +11 -4
- package/src/instance/children/normalizeChildInitOptions.ts +1 -1
- package/src/instance/hierarchy.ts +1 -3
- package/src/instance/internal-api/AttributeContext.ts +6 -0
- package/src/instance/internal-api/InstanceConfig.ts +6 -1
- package/src/instance/internal-api/InstanceValueContext.ts +6 -0
- package/src/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.ts +0 -1
- package/src/instance/internal-api/serialization/ClientReactiveSerializableValueNode.ts +4 -0
- package/src/instance/repeat/RepeatInstance.ts +1 -10
- package/src/lib/client-reactivity/instance-state/createValueNodeInstanceState.ts +2 -1
- package/src/lib/client-reactivity/instance-state/prepareInstancePayload.ts +1 -0
- package/src/lib/codecs/NoteCodec.ts +1 -1
- package/src/lib/codecs/items/SingleValueItemCodec.ts +1 -3
- package/src/lib/reactivity/createInstanceValueState.ts +152 -53
- package/src/lib/reactivity/node-state/createSharedNodeState.ts +2 -2
- package/src/lib/xml-serialization.ts +9 -1
- package/src/parse/XFormDOM.ts +9 -0
- package/src/parse/body/GroupElementDefinition.ts +1 -1
- package/src/parse/body/control/InputControlDefinition.ts +1 -1
- package/src/parse/expression/ActionComputationExpression.ts +12 -0
- package/src/parse/model/ActionDefinition.ts +70 -0
- package/src/parse/model/AttributeDefinition.ts +3 -2
- package/src/parse/model/AttributeDefinitionMap.ts +1 -1
- package/src/parse/model/BindDefinition.ts +1 -6
- package/src/parse/model/BindPreloadDefinition.ts +44 -12
- package/src/parse/model/Event.ts +9 -0
- package/src/parse/model/LeafNodeDefinition.ts +5 -1
- package/src/parse/model/ModelActionMap.ts +37 -0
- package/src/parse/model/ModelDefinition.ts +18 -3
- package/src/parse/model/NoteNodeDefinition.ts +5 -2
- package/src/parse/model/RangeNodeDefinition.ts +5 -2
- package/src/parse/model/RootDefinition.ts +22 -4
- package/dist/lib/reactivity/createAttributeValueState.d.ts +0 -15
- 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
|
|
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:
|
|
28
|
+
const isEditInitialLoad = (context: ValueContext) => {
|
|
18
29
|
return context.rootDocument.initializationMode === 'edit';
|
|
19
30
|
};
|
|
20
31
|
|
|
21
|
-
const getInitialValue = (context:
|
|
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:
|
|
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:
|
|
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
|
|
103
|
+
const isLoading = (context: ValueContext) => {
|
|
98
104
|
return isInstanceFirstLoad(context) || isEditInitialLoad(context);
|
|
99
105
|
};
|
|
100
106
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
135
|
+
if (preload.event === XFORM_EVENT.xformsRevalidate) {
|
|
136
|
+
postloadValue(context, setValue, preload);
|
|
117
137
|
return;
|
|
118
138
|
}
|
|
119
139
|
|
|
120
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
184
|
+
* @see {@link preloadValue} for important details about spec ordering of
|
|
135
185
|
* events and computations.
|
|
136
186
|
*/
|
|
137
187
|
const createCalculation = (
|
|
138
|
-
context:
|
|
188
|
+
context: ValueContext,
|
|
139
189
|
setRelevantValue: SimpleAtomicStateSetter<string>,
|
|
140
|
-
|
|
190
|
+
computation: ActionComputationExpression<'string'> | BindComputationExpression<'calculate'>
|
|
141
191
|
): void => {
|
|
142
|
-
context
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
16
|
+
[K in Extract<keyof Spec, string>]: Spec[K] extends MutablePropertySpec<any>
|
|
17
17
|
? K
|
|
18
18
|
: never;
|
|
19
|
-
}[
|
|
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
|
-
|
|
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
|
};
|
package/src/parse/XFormDOM.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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() {
|