@getodk/xforms-engine 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/dist/client/AttributeNode.d.ts +50 -0
  2. package/dist/client/BaseNode.d.ts +5 -0
  3. package/dist/client/form/FormInstanceConfig.d.ts +15 -0
  4. package/dist/client/index.d.ts +1 -0
  5. package/dist/client/node-types.d.ts +1 -1
  6. package/dist/client/validation.d.ts +7 -1
  7. package/dist/index.js +730 -294
  8. package/dist/index.js.map +1 -1
  9. package/dist/instance/Attribute.d.ts +64 -0
  10. package/dist/instance/Group.d.ts +2 -0
  11. package/dist/instance/InputControl.d.ts +2 -0
  12. package/dist/instance/PrimaryInstance.d.ts +2 -0
  13. package/dist/instance/Root.d.ts +2 -0
  14. package/dist/instance/UploadControl.d.ts +2 -0
  15. package/dist/instance/abstract/InstanceNode.d.ts +5 -2
  16. package/dist/instance/abstract/ValueNode.d.ts +2 -0
  17. package/dist/instance/attachments/buildAttributes.d.ts +7 -0
  18. package/dist/instance/internal-api/AttributeContext.d.ts +35 -0
  19. package/dist/instance/internal-api/InstanceConfig.d.ts +2 -0
  20. package/dist/instance/internal-api/InstanceValueContext.d.ts +6 -0
  21. package/dist/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.d.ts +15 -0
  22. package/dist/instance/internal-api/serialization/ClientReactiveSerializableParentNode.d.ts +2 -0
  23. package/dist/instance/internal-api/serialization/ClientReactiveSerializableTemplatedNode.d.ts +2 -2
  24. package/dist/instance/internal-api/serialization/ClientReactiveSerializableValueNode.d.ts +4 -0
  25. package/dist/instance/repeat/BaseRepeatRange.d.ts +5 -0
  26. package/dist/instance/repeat/RepeatInstance.d.ts +2 -2
  27. package/dist/integration/xpath/adapter/XFormsXPathNode.d.ts +2 -2
  28. package/dist/lib/client-reactivity/instance-state/createAttributeNodeInstanceState.d.ts +3 -0
  29. package/dist/lib/client-reactivity/instance-state/createTemplatedNodeInstanceState.d.ts +0 -3
  30. package/dist/lib/codecs/items/SingleValueItemCodec.d.ts +1 -1
  31. package/dist/lib/names/NamespaceDeclarationMap.d.ts +1 -1
  32. package/dist/lib/reactivity/createAttributeState.d.ts +16 -0
  33. package/dist/lib/reactivity/createInstanceValueState.d.ts +4 -1
  34. package/dist/lib/reactivity/node-state/createSharedNodeState.d.ts +2 -2
  35. package/dist/lib/xml-serialization.d.ts +5 -9
  36. package/dist/parse/XFormDOM.d.ts +4 -1
  37. package/dist/parse/expression/ActionComputationExpression.d.ts +4 -0
  38. package/dist/parse/model/ActionDefinition.d.ts +15 -0
  39. package/dist/parse/model/AttributeDefinition.d.ts +24 -0
  40. package/dist/parse/model/{RootAttributeMap.d.ts → AttributeDefinitionMap.d.ts} +4 -10
  41. package/dist/parse/model/BindPreloadDefinition.d.ts +6 -10
  42. package/dist/parse/model/Event.d.ts +8 -0
  43. package/dist/parse/model/GroupDefinition.d.ts +4 -1
  44. package/dist/parse/model/LeafNodeDefinition.d.ts +5 -1
  45. package/dist/parse/model/ModelActionMap.d.ts +9 -0
  46. package/dist/parse/model/ModelDefinition.d.ts +8 -1
  47. package/dist/parse/model/NodeDefinition.d.ts +8 -3
  48. package/dist/parse/model/NoteNodeDefinition.d.ts +3 -2
  49. package/dist/parse/model/RangeNodeDefinition.d.ts +2 -1
  50. package/dist/parse/model/RepeatDefinition.d.ts +4 -1
  51. package/dist/parse/model/RootDefinition.d.ts +3 -2
  52. package/dist/solid.js +730 -294
  53. package/dist/solid.js.map +1 -1
  54. package/package.json +21 -17
  55. package/src/client/AttributeNode.ts +59 -0
  56. package/src/client/BaseNode.ts +6 -0
  57. package/src/client/form/FormInstanceConfig.ts +17 -0
  58. package/src/client/index.ts +1 -0
  59. package/src/client/node-types.ts +1 -0
  60. package/src/client/validation.ts +9 -1
  61. package/src/entrypoints/FormInstance.ts +1 -0
  62. package/src/instance/Attribute.ts +164 -0
  63. package/src/instance/Group.ts +7 -0
  64. package/src/instance/InputControl.ts +8 -0
  65. package/src/instance/ModelValue.ts +7 -0
  66. package/src/instance/Note.ts +6 -0
  67. package/src/instance/PrimaryInstance.ts +7 -0
  68. package/src/instance/RangeControl.ts +6 -0
  69. package/src/instance/RankControl.ts +7 -0
  70. package/src/instance/Root.ts +7 -0
  71. package/src/instance/SelectControl.ts +6 -0
  72. package/src/instance/TriggerControl.ts +6 -0
  73. package/src/instance/UploadControl.ts +5 -0
  74. package/src/instance/abstract/DescendantNode.ts +0 -1
  75. package/src/instance/abstract/InstanceNode.ts +4 -1
  76. package/src/instance/abstract/ValueNode.ts +2 -0
  77. package/src/instance/attachments/buildAttributes.ts +15 -0
  78. package/src/instance/children/normalizeChildInitOptions.ts +1 -1
  79. package/src/instance/internal-api/AttributeContext.ts +40 -0
  80. package/src/instance/internal-api/InstanceConfig.ts +6 -1
  81. package/src/instance/internal-api/InstanceValueContext.ts +6 -0
  82. package/src/instance/internal-api/serialization/ClientReactiveSerializableAttributeNode.ts +18 -0
  83. package/src/instance/internal-api/serialization/ClientReactiveSerializableParentNode.ts +2 -0
  84. package/src/instance/internal-api/serialization/ClientReactiveSerializableTemplatedNode.ts +2 -3
  85. package/src/instance/internal-api/serialization/ClientReactiveSerializableValueNode.ts +4 -0
  86. package/src/instance/repeat/BaseRepeatRange.ts +14 -0
  87. package/src/instance/repeat/RepeatInstance.ts +5 -5
  88. package/src/integration/xpath/adapter/XFormsXPathNode.ts +3 -1
  89. package/src/lib/client-reactivity/instance-state/createAttributeNodeInstanceState.ts +16 -0
  90. package/src/lib/client-reactivity/instance-state/createParentNodeInstanceState.ts +5 -5
  91. package/src/lib/client-reactivity/instance-state/createRootInstanceState.ts +6 -9
  92. package/src/lib/client-reactivity/instance-state/createTemplatedNodeInstanceState.ts +5 -15
  93. package/src/lib/client-reactivity/instance-state/createValueNodeInstanceState.ts +2 -1
  94. package/src/lib/client-reactivity/instance-state/prepareInstancePayload.ts +1 -0
  95. package/src/lib/codecs/NoteCodec.ts +1 -1
  96. package/src/lib/codecs/items/SingleValueItemCodec.ts +1 -3
  97. package/src/lib/names/NamespaceDeclarationMap.ts +1 -1
  98. package/src/lib/reactivity/createAttributeState.ts +51 -0
  99. package/src/lib/reactivity/createInstanceValueState.ts +152 -53
  100. package/src/lib/reactivity/node-state/createSharedNodeState.ts +2 -2
  101. package/src/lib/xml-serialization.ts +38 -34
  102. package/src/parse/XFormDOM.ts +9 -0
  103. package/src/parse/body/GroupElementDefinition.ts +1 -1
  104. package/src/parse/body/control/InputControlDefinition.ts +1 -1
  105. package/src/parse/expression/ActionComputationExpression.ts +12 -0
  106. package/src/parse/model/ActionDefinition.ts +70 -0
  107. package/src/parse/model/AttributeDefinition.ts +59 -0
  108. package/src/parse/model/{RootAttributeMap.ts → AttributeDefinitionMap.ts} +7 -13
  109. package/src/parse/model/BindDefinition.ts +1 -6
  110. package/src/parse/model/BindPreloadDefinition.ts +44 -12
  111. package/src/parse/model/Event.ts +9 -0
  112. package/src/parse/model/GroupDefinition.ts +6 -0
  113. package/src/parse/model/LeafNodeDefinition.ts +5 -0
  114. package/src/parse/model/ModelActionMap.ts +37 -0
  115. package/src/parse/model/ModelDefinition.ts +18 -3
  116. package/src/parse/model/NodeDefinition.ts +11 -3
  117. package/src/parse/model/NoteNodeDefinition.ts +5 -2
  118. package/src/parse/model/RangeNodeDefinition.ts +5 -2
  119. package/src/parse/model/RepeatDefinition.ts +8 -1
  120. package/src/parse/model/RootDefinition.ts +27 -9
  121. package/src/parse/model/nodeDefinitionMap.ts +1 -1
  122. package/dist/error/TemplatedNodeAttributeSerializationError.d.ts +0 -22
  123. package/dist/parse/model/RootAttributeDefinition.d.ts +0 -21
  124. package/src/error/TemplatedNodeAttributeSerializationError.ts +0 -24
  125. package/src/parse/model/RootAttributeDefinition.ts +0 -44
@@ -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
+ }
@@ -0,0 +1,59 @@
1
+ import { XMLNS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
2
+ import type { StaticAttribute } from '../../integration/xpath/static-dom/StaticAttribute.ts';
3
+ import {
4
+ NamespaceDeclarationMap,
5
+ type NamedNodeDefinition,
6
+ } from '../../lib/names/NamespaceDeclarationMap.ts';
7
+ import { QualifiedName } from '../../lib/names/QualifiedName.ts';
8
+ import { escapeXMLText, serializeAttributeXML } from '../../lib/xml-serialization.ts';
9
+ import type { BindDefinition } from './BindDefinition.ts';
10
+ import type { ModelDefinition } from './ModelDefinition.ts';
11
+ import { NodeDefinition } from './NodeDefinition.ts';
12
+ import type { RootDefinition } from './RootDefinition.ts';
13
+
14
+ export class AttributeDefinition
15
+ extends NodeDefinition<'attribute'>
16
+ implements NamedNodeDefinition
17
+ {
18
+ private readonly serializedXML: string;
19
+
20
+ readonly value: string;
21
+ readonly type = 'attribute';
22
+ readonly namespaceDeclarations: NamespaceDeclarationMap;
23
+ readonly bodyElement = null;
24
+ readonly root: RootDefinition;
25
+ readonly isTranslated: boolean = false;
26
+ readonly parent = null;
27
+ readonly children = null;
28
+ readonly attributes = null;
29
+
30
+ readonly qualifiedName: QualifiedName;
31
+
32
+ constructor(
33
+ readonly model: ModelDefinition,
34
+ bind: BindDefinition,
35
+ readonly template: StaticAttribute
36
+ ) {
37
+ super(bind);
38
+
39
+ const { value } = template;
40
+
41
+ this.root = model.root;
42
+
43
+ this.value = value;
44
+ this.qualifiedName = template.qualifiedName;
45
+ this.namespaceDeclarations = new NamespaceDeclarationMap(this);
46
+
47
+ // We serialize namespace declarations separately
48
+ if (this.qualifiedName.namespaceURI?.href === XMLNS_NAMESPACE_URI) {
49
+ this.serializedXML = '';
50
+ } else {
51
+ const xmlValue = escapeXMLText(this.value, true);
52
+ this.serializedXML = serializeAttributeXML(this.qualifiedName, xmlValue);
53
+ }
54
+ }
55
+
56
+ serializeAttributeXML(): string {
57
+ return this.serializedXML;
58
+ }
59
+ }
@@ -2,8 +2,8 @@ import { XMLNS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
2
2
  import type { StaticAttribute } from '../../integration/xpath/static-dom/StaticAttribute.ts';
3
3
  import type { StaticElement } from '../../integration/xpath/static-dom/StaticElement.ts';
4
4
  import type { QualifiedName } from '../../lib/names/QualifiedName.ts';
5
- import { RootAttributeDefinition } from './RootAttributeDefinition.ts';
6
- import type { RootDefinition } from './RootDefinition.ts';
5
+ import { AttributeDefinition } from './AttributeDefinition.ts';
6
+ import type { ModelDefinition } from './ModelDefinition.ts';
7
7
 
8
8
  /**
9
9
  * @todo We should probably just distinguish these as separate `StaticNode`
@@ -14,12 +14,6 @@ const isNonNamespaceAttribute = (attribute: StaticAttribute) => {
14
14
  };
15
15
 
16
16
  /**
17
- * @todo This can be trivially expanded to a narrowly general case when we
18
- * prioritize work to
19
- * {@link https://github.com/getodk/web-forms/issues/285 | support attributes}
20
- * (as modeled form nodes on par with elements). It's been deferred here to
21
- * avoid expanding scope of an already fairly large yak shave.
22
- *
23
17
  * @todo There's a **much more expansive** general case just waiting for a good
24
18
  * opportuntity to prioritize it. E.g. a `NamedNodeMap<T>`, where T is any
25
19
  * generalized concept of a named node. This expansive generalization would have
@@ -29,17 +23,17 @@ const isNonNamespaceAttribute = (attribute: StaticAttribute) => {
29
23
  *
30
24
  * @see {@link QualifiedName} for more detail.
31
25
  */
32
- export class RootAttributeMap extends Map<QualifiedName, RootAttributeDefinition> {
33
- static from(root: RootDefinition, instanceNode: StaticElement) {
26
+ export class AttributeDefinitionMap extends Map<QualifiedName, AttributeDefinition> {
27
+ static from(model: ModelDefinition, instanceNode: StaticElement) {
34
28
  const nonNamespaceAttributes = instanceNode.attributes.filter(isNonNamespaceAttribute);
35
29
  const definitions = nonNamespaceAttributes.map((attribute) => {
36
- return new RootAttributeDefinition(root, attribute);
30
+ const bind = model.binds.getOrCreateBindDefinition(attribute.nodeset);
31
+ return new AttributeDefinition(model, bind, attribute);
37
32
  });
38
-
39
33
  return new this(definitions);
40
34
  }
41
35
 
42
- private constructor(definitions: readonly RootAttributeDefinition[]) {
36
+ private constructor(definitions: readonly AttributeDefinition[]) {
43
37
  super(
44
38
  definitions.map((attribute) => {
45
39
  return [attribute.qualifiedName, attribute];
@@ -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];
@@ -3,8 +3,10 @@ import { NamespaceDeclarationMap } from '../../lib/names/NamespaceDeclarationMap
3
3
  import { QualifiedName } from '../../lib/names/QualifiedName.ts';
4
4
  import type { AnyBodyElementDefinition } from '../body/BodyDefinition.ts';
5
5
  import type { GroupElementDefinition } from '../body/GroupElementDefinition.ts';
6
+ import { AttributeDefinitionMap } from './AttributeDefinitionMap.ts';
6
7
  import type { BindDefinition } from './BindDefinition.ts';
7
8
  import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts';
9
+ import type { ModelDefinition } from './ModelDefinition.ts';
8
10
  import type { ChildNodeDefinition, ParentNodeDefinition } from './NodeDefinition.ts';
9
11
 
10
12
  export class GroupDefinition extends DescendentNodeDefinition<
@@ -16,8 +18,10 @@ export class GroupDefinition extends DescendentNodeDefinition<
16
18
  readonly namespaceDeclarations: NamespaceDeclarationMap;
17
19
  readonly qualifiedName: QualifiedName;
18
20
  readonly children: readonly ChildNodeDefinition[];
21
+ readonly attributes: AttributeDefinitionMap;
19
22
 
20
23
  constructor(
24
+ model: ModelDefinition,
21
25
  parent: ParentNodeDefinition,
22
26
  bind: BindDefinition,
23
27
  bodyElement: AnyBodyElementDefinition | null,
@@ -37,6 +41,8 @@ export class GroupDefinition extends DescendentNodeDefinition<
37
41
  this.qualifiedName = template.qualifiedName;
38
42
  this.namespaceDeclarations = new NamespaceDeclarationMap(this);
39
43
  this.children = root.buildSubtree(this, template);
44
+
45
+ this.attributes = AttributeDefinitionMap.from(model, template);
40
46
  }
41
47
 
42
48
  toJSON() {
@@ -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,8 +22,10 @@ export class LeafNodeDefinition<V extends ValueType = ValueType>
20
22
  readonly namespaceDeclarations: NamespaceDeclarationMap;
21
23
  readonly qualifiedName: QualifiedName;
22
24
  readonly children = null;
25
+ readonly attributes: AttributeDefinitionMap;
23
26
 
24
27
  constructor(
28
+ readonly model: ModelDefinition,
25
29
  parent: ParentNodeDefinition,
26
30
  bind: BindDefinition,
27
31
  bodyElement: AnyBodyElementDefinition | null,
@@ -36,6 +40,7 @@ export class LeafNodeDefinition<V extends ValueType = ValueType>
36
40
  this.valueType = bind.type.resolved satisfies ValueType as V;
37
41
  this.qualifiedName = template.qualifiedName;
38
42
  this.namespaceDeclarations = new NamespaceDeclarationMap(this);
43
+ this.attributes = AttributeDefinitionMap.from(model, template);
39
44
  }
40
45
 
41
46
  toJSON() {
@@ -0,0 +1,37 @@
1
+ import { ActionDefinition } from './ActionDefinition.ts';
2
+ import { XFORM_EVENT } from './Event.ts';
3
+ import type { ModelDefinition } from './ModelDefinition.ts';
4
+
5
+ const REPEAT_REGEX = /(\[[^\]]*\])/gm;
6
+
7
+ export class ModelActionMap extends Map<string, ActionDefinition> {
8
+ static fromModel(model: ModelDefinition): ModelActionMap {
9
+ return new this(model);
10
+ }
11
+
12
+ static getKey(ref: string): string {
13
+ return ref.replace(REPEAT_REGEX, '');
14
+ }
15
+
16
+ protected constructor(model: ModelDefinition) {
17
+ super(
18
+ model.form.xformDOM.setValues.map((setValueElement) => {
19
+ const action = new ActionDefinition(model, setValueElement);
20
+ if (action.events.includes(XFORM_EVENT.odkNewRepeat)) {
21
+ throw new Error('Model contains "setvalue" element with "odk-new-repeat" event');
22
+ }
23
+ const key = ModelActionMap.getKey(action.ref);
24
+ return [key, action];
25
+ })
26
+ );
27
+ }
28
+
29
+ override get(ref: string): ActionDefinition | undefined {
30
+ return super.get(ModelActionMap.getKey(ref));
31
+ }
32
+
33
+ add(action: ActionDefinition) {
34
+ const key = ModelActionMap.getKey(action.ref);
35
+ this.set(key, action);
36
+ }
37
+ }
@@ -6,6 +6,7 @@ import { parseStaticDocumentFromDOMSubtree } from '../shared/parseStaticDocument
6
6
  import type { XFormDefinition } from '../XFormDefinition.ts';
7
7
  import { generateItextChunks, type ChunkExpressionsByItextId } from './generateItextChunks.ts';
8
8
  import { ItextTranslationsDefinition } from './ItextTranslationsDefinition.ts';
9
+ import { ModelActionMap } from './ModelActionMap.ts';
9
10
  import { ModelBindMap } from './ModelBindMap.ts';
10
11
  import type { AnyNodeDefinition } from './NodeDefinition.ts';
11
12
  import type { NodeDefinitionMap } from './nodeDefinitionMap.ts';
@@ -13,18 +14,23 @@ import { nodeDefinitionMap } from './nodeDefinitionMap.ts';
13
14
  import { RootDefinition } from './RootDefinition.ts';
14
15
  import { SubmissionDefinition } from './SubmissionDefinition.ts';
15
16
 
17
+ type XformsRevalidateListener = () => void;
18
+
16
19
  export class ModelDefinition {
17
20
  readonly binds: ModelBindMap;
21
+ readonly actions: ModelActionMap;
18
22
  readonly root: RootDefinition;
19
23
  readonly nodes: NodeDefinitionMap;
20
24
  readonly instance: StaticDocument;
21
25
  readonly itextTranslations: ItextTranslationsDefinition;
22
26
  readonly itextChunks: Map<string, ChunkExpressionsByItextId>;
27
+ readonly xformsRevalidateListeners: Map<string, XformsRevalidateListener>;
23
28
 
24
29
  constructor(readonly form: XFormDefinition) {
25
30
  const submission = new SubmissionDefinition(form.xformDOM);
26
31
 
27
32
  this.binds = ModelBindMap.fromModel(this);
33
+ this.actions = ModelActionMap.fromModel(this);
28
34
  this.instance = parseStaticDocumentFromDOMSubtree(form.xformDOM.primaryInstanceRoot, {
29
35
  nodesetPrefix: '/',
30
36
  });
@@ -32,6 +38,7 @@ export class ModelDefinition {
32
38
  this.nodes = nodeDefinitionMap(this.root);
33
39
  this.itextTranslations = ItextTranslationsDefinition.from(form.xformDOM);
34
40
  this.itextChunks = generateItextChunks(form.xformDOM.itextTranslationElements);
41
+ this.xformsRevalidateListeners = new Map();
35
42
  }
36
43
 
37
44
  getNodeDefinition(nodeset: string): AnyNodeDefinition {
@@ -54,10 +61,12 @@ export class ModelDefinition {
54
61
  return definition;
55
62
  }
56
63
 
57
- toJSON() {
58
- const { form, ...rest } = this;
64
+ registerXformsRevalidateListener(ref: string, listener: XformsRevalidateListener) {
65
+ this.xformsRevalidateListeners.set(ref, listener);
66
+ }
59
67
 
60
- return rest;
68
+ triggerXformsRevalidateListeners() {
69
+ this.xformsRevalidateListeners.forEach((listener: XformsRevalidateListener) => listener());
61
70
  }
62
71
 
63
72
  getTranslationChunks(
@@ -67,4 +76,10 @@ export class ModelDefinition {
67
76
  const languageMap = this.itextChunks.get(activeLanguage.language);
68
77
  return languageMap?.get(itextId) ?? [];
69
78
  }
79
+
80
+ toJSON() {
81
+ const { form, ...rest } = this;
82
+
83
+ return rest;
84
+ }
70
85
  }
@@ -1,3 +1,4 @@
1
+ import type { StaticAttribute } from '../../integration/xpath/static-dom/StaticAttribute.ts';
1
2
  import type { StaticElement } from '../../integration/xpath/static-dom/StaticElement.ts';
2
3
  import type {
3
4
  NamedSubtreeDefinition,
@@ -6,6 +7,8 @@ import type {
6
7
  import type { QualifiedName } from '../../lib/names/QualifiedName.ts';
7
8
  import type { AnyBodyElementDefinition } from '../body/BodyDefinition.ts';
8
9
  import type { RepeatElementDefinition } from '../body/RepeatElementDefinition.ts';
10
+ import type { AttributeDefinition } from './AttributeDefinition.ts';
11
+ import type { AttributeDefinitionMap } from './AttributeDefinitionMap.ts';
9
12
  import type { BindDefinition } from './BindDefinition.ts';
10
13
  import type { GroupDefinition } from './GroupDefinition.ts';
11
14
  import type { LeafNodeDefinition } from './LeafNodeDefinition.ts';
@@ -45,13 +48,16 @@ export type GroupNodeType = 'group';
45
48
  */
46
49
  export type LeafNodeType = 'leaf-node';
47
50
 
51
+ export type AttributeNodeType = 'attribute';
52
+
48
53
  // prettier-ignore
49
54
  export type NodeDefinitionType =
50
55
  // eslint-disable-next-line @typescript-eslint/sort-type-constituents
51
56
  | RootNodeType
52
57
  | RepeatType
53
58
  | GroupNodeType
54
- | LeafNodeType;
59
+ | LeafNodeType
60
+ | AttributeNodeType;
55
61
 
56
62
  // prettier-ignore
57
63
  export type ParentNodeDefinition =
@@ -76,8 +82,9 @@ export abstract class NodeDefinition<Type extends NodeDefinitionType>
76
82
  abstract readonly isTranslated: boolean;
77
83
  abstract readonly root: RootDefinition;
78
84
  abstract readonly parent: ParentNodeDefinition | null;
79
- abstract readonly template: StaticElement;
85
+ abstract readonly template: StaticAttribute | StaticElement;
80
86
  abstract readonly children: readonly ChildNodeDefinition[] | null;
87
+ abstract readonly attributes: AttributeDefinitionMap | null;
81
88
 
82
89
  readonly nodeset: string;
83
90
 
@@ -92,4 +99,5 @@ export type AnyNodeDefinition =
92
99
  | RootDefinition
93
100
  | AnyRepeatDefinition
94
101
  | GroupDefinition
95
- | LeafNodeDefinition;
102
+ | LeafNodeDefinition
103
+ | AttributeDefinition;
@@ -9,6 +9,7 @@ import type { HintDefinition } from '../text/HintDefinition.ts';
9
9
  import type { LabelDefinition } from '../text/LabelDefinition.ts';
10
10
  import type { BindDefinition } from './BindDefinition.ts';
11
11
  import { LeafNodeDefinition } from './LeafNodeDefinition.ts';
12
+ import type { ModelDefinition } from './ModelDefinition.ts';
12
13
  import type { ParentNodeDefinition } from './NodeDefinition.ts';
13
14
 
14
15
  // prettier-ignore
@@ -41,6 +42,7 @@ export type NoteTextDefinition =
41
42
  */
42
43
  export class NoteNodeDefinition<V extends ValueType = ValueType> extends LeafNodeDefinition<V> {
43
44
  static from<V extends ValueType>(
45
+ model: ModelDefinition,
44
46
  parent: ParentNodeDefinition,
45
47
  bind: BindDefinition<V>,
46
48
  bodyElement: AnyBodyElementDefinition | null,
@@ -57,16 +59,17 @@ export class NoteNodeDefinition<V extends ValueType = ValueType> extends LeafNod
57
59
  return null;
58
60
  }
59
61
 
60
- return new this(parent, bind, bodyElement, noteTextDefinition, node);
62
+ return new this(model, parent, bind, bodyElement, noteTextDefinition, node);
61
63
  }
62
64
 
63
65
  constructor(
66
+ model: ModelDefinition,
64
67
  parent: ParentNodeDefinition,
65
68
  override readonly bind: NoteBindDefinition<V>,
66
69
  override readonly bodyElement: InputControlDefinition,
67
70
  readonly noteTextDefinition: NoteTextDefinition,
68
71
  template: StaticLeafElement
69
72
  ) {
70
- super(parent, bind, bodyElement, template);
73
+ super(model, parent, bind, bodyElement, template);
71
74
  }
72
75
  }
@@ -9,6 +9,7 @@ import type {
9
9
  } from '../body/control/RangeControlDefinition.ts';
10
10
  import type { BindDefinition } from './BindDefinition.ts';
11
11
  import { LeafNodeDefinition } from './LeafNodeDefinition.ts';
12
+ import type { ModelDefinition } from './ModelDefinition.ts';
12
13
  import type { ParentNodeDefinition } from './NodeDefinition.ts';
13
14
 
14
15
  const RANGE_VALUE_TYPES = ['decimal', 'int'] as const;
@@ -89,6 +90,7 @@ export class RangeNodeDefinition<V extends RangeValueType = RangeValueType>
89
90
  implements RangeLeafNodeDefinition<V>
90
91
  {
91
92
  static from<V extends ValueType>(
93
+ model: ModelDefinition,
92
94
  parent: ParentNodeDefinition,
93
95
  bind: BindDefinition<V>,
94
96
  bodyElement: RangeControlDefinition,
@@ -96,18 +98,19 @@ export class RangeNodeDefinition<V extends RangeValueType = RangeValueType>
96
98
  ): RangeNodeDefinition<Extract<V, RangeValueType>> {
97
99
  assertRangeBindDefinition(bind);
98
100
 
99
- return new this(parent, bind, bodyElement, node);
101
+ return new this(model, parent, bind, bodyElement, node);
100
102
  }
101
103
 
102
104
  readonly bounds: RangeNodeBoundsDefinition<V>;
103
105
 
104
106
  private constructor(
107
+ model: ModelDefinition,
105
108
  parent: ParentNodeDefinition,
106
109
  override readonly bind: BindDefinition<V>,
107
110
  override readonly bodyElement: RangeControlDefinition,
108
111
  node: StaticLeafElement
109
112
  ) {
110
- super(parent, bind, bodyElement, node);
113
+ super(model, parent, bind, bodyElement, node);
111
114
 
112
115
  this.bounds = RangeNodeBoundsDefinition.from(bodyElement.bounds, bind);
113
116
  }
@@ -19,9 +19,11 @@ import type { NamespaceURL } from '../../lib/names/NamespaceURL.ts';
19
19
  import type { QualifiedName, QualifiedNameSource } from '../../lib/names/QualifiedName.ts';
20
20
  import type { RepeatElementDefinition } from '../body/RepeatElementDefinition.ts';
21
21
  import { RepeatCountControlExpression } from '../expression/RepeatCountControlExpression.ts';
22
+ import { AttributeDefinitionMap } from './AttributeDefinitionMap.ts';
22
23
  import type { BindDefinition } from './BindDefinition.ts';
23
24
  import { DescendentNodeDefinition } from './DescendentNodeDefinition.ts';
24
25
  import type { GroupDefinition } from './GroupDefinition.ts';
26
+ import type { ModelDefinition } from './ModelDefinition.ts';
25
27
  import type { ChildNodeDefinition, ParentNodeDefinition } from './NodeDefinition.ts';
26
28
  import type { RootDefinition } from './RootDefinition.ts';
27
29
 
@@ -313,18 +315,20 @@ export interface UncontrolledRepeatDefinition extends RepeatDefinition {
313
315
  */
314
316
  export class RepeatDefinition extends DescendentNodeDefinition<'repeat', RepeatElementDefinition> {
315
317
  static from(
318
+ model: ModelDefinition,
316
319
  parent: ParentNodeDefinition,
317
320
  bind: BindDefinition,
318
321
  bodyElement: RepeatElementDefinition,
319
322
  instanceNodes: RepeatInstanceNodes
320
323
  ): AnyRepeatDefinition;
321
324
  static from(
325
+ model: ModelDefinition,
322
326
  parent: ParentNodeDefinition,
323
327
  bind: BindDefinition,
324
328
  bodyElement: RepeatElementDefinition,
325
329
  instanceNodes: RepeatInstanceNodes
326
330
  ): RepeatDefinition {
327
- return new this(parent, bind, bodyElement, instanceNodes);
331
+ return new this(model, parent, bind, bodyElement, instanceNodes);
328
332
  }
329
333
 
330
334
  readonly type = 'repeat';
@@ -333,8 +337,10 @@ export class RepeatDefinition extends DescendentNodeDefinition<'repeat', RepeatE
333
337
  readonly template: StaticElement;
334
338
  readonly namespaceDeclarations: NamespaceDeclarationMap;
335
339
  readonly qualifiedName: QualifiedName;
340
+ readonly attributes: AttributeDefinitionMap;
336
341
 
337
342
  private constructor(
343
+ model: ModelDefinition,
338
344
  parent: ParentNodeDefinition,
339
345
  bind: BindDefinition,
340
346
  bodyElement: RepeatElementDefinition,
@@ -353,6 +359,7 @@ export class RepeatDefinition extends DescendentNodeDefinition<'repeat', RepeatE
353
359
  this.children = root.buildSubtree(self, template);
354
360
 
355
361
  const initialCount = this.omitTemplate(instanceNodes).length;
362
+ this.attributes = AttributeDefinitionMap.from(model, template);
356
363
 
357
364
  this.count = RepeatCountControlExpression.from(bodyElement, initialCount);
358
365
  }