Skip to content

Commit f1fecfb

Browse files
Merge pull request #291 from getodk/features/emit-submission-payload-to-host-app
Emit submission payload(s) to host app
2 parents 0700362 + c60e37a commit f1fecfb

File tree

6 files changed

+135
-21
lines changed

6 files changed

+135
-21
lines changed

.changeset/friendly-monkeys-grab.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@getodk/web-forms': minor
3+
---
4+
5+
- Emit submission payload when subscribed to `submit` event
6+
- Emit chunked submission payload when subscribed to new `submitChunked` event

.changeset/strange-needles-compare.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@getodk/xforms-engine': patch
3+
---
4+
5+
Fix: correct types for chunked/monolithic submission result

packages/scenario/src/jr/Scenario.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ import type {
77
RepeatRangeUncontrolledNode,
88
RootNode,
99
SelectNode,
10-
SubmissionChunkedType,
11-
SubmissionOptions,
12-
SubmissionResult,
1310
} from '@getodk/xforms-engine';
1411
import { constants as ENGINE_CONSTANTS } from '@getodk/xforms-engine';
1512
import type { Accessor, Setter } from 'solid-js';
@@ -965,10 +962,8 @@ export class Scenario {
965962
* more about Collect's responsibility for submission (beyond serialization,
966963
* already handled by {@link proposed_serializeInstance}).
967964
*/
968-
prepareWebFormsSubmission<ChunkedType extends SubmissionChunkedType>(
969-
options?: SubmissionOptions<ChunkedType>
970-
): Promise<SubmissionResult<ChunkedType>> {
971-
return this.instanceRoot.prepareSubmission<ChunkedType>(options);
965+
prepareWebFormsSubmission() {
966+
return this.instanceRoot.prepareSubmission();
972967
}
973968

974969
// TODO: consider adapting tests which use the following interfaces to use

packages/web-forms/src/components/OdkWebForm.vue

Lines changed: 96 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,107 @@
11
<script setup lang="ts">
2-
import type { MissingResourceBehavior } from '@getodk/xforms-engine';
2+
import type {
3+
ChunkedSubmissionResult,
4+
MissingResourceBehavior,
5+
MonolithicSubmissionResult,
6+
} from '@getodk/xforms-engine';
37
import { initializeForm, type FetchFormAttachment, type RootNode } from '@getodk/xforms-engine';
48
import Button from 'primevue/button';
59
import Card from 'primevue/card';
610
import PrimeMessage from 'primevue/message';
7-
import { computed, provide, reactive, ref, watchEffect, type ComponentPublicInstance } from 'vue';
11+
import type { ComponentPublicInstance } from 'vue';
12+
import { computed, getCurrentInstance, provide, reactive, ref, watchEffect } from 'vue';
813
import { FormInitializationError } from '../lib/error/FormInitializationError.ts';
914
import FormLoadFailureDialog from './Form/FormLoadFailureDialog.vue';
1015
import FormHeader from './FormHeader.vue';
1116
import QuestionList from './QuestionList.vue';
1217
1318
const webFormsVersion = __WEB_FORMS_VERSION__;
1419
15-
const props = defineProps<{
20+
interface OdkWebFormsProps {
1621
formXml: string;
1722
fetchFormAttachment: FetchFormAttachment;
1823
missingResourceBehavior?: MissingResourceBehavior;
19-
}>();
20-
const emit = defineEmits(['submit']);
24+
25+
/**
26+
* Note: this parameter must be set when subscribing to the
27+
* {@link OdkWebFormEmits.submitChunked | submitChunked} event.
28+
*/
29+
submissionMaxSize?: number;
30+
}
31+
32+
const props = defineProps<OdkWebFormsProps>();
33+
34+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- evidently a type must be used for this to be assigned to a name (which we use!); as an interface, it won't satisfy the `Record` constraint of `defineEmits`.
35+
type OdkWebFormEmits = {
36+
submit: [submissionPayload: MonolithicSubmissionResult];
37+
submitChunked: [submissionPayload: ChunkedSubmissionResult];
38+
};
39+
40+
/**
41+
* Supports {@link isEmitSubscribed}.
42+
*
43+
* @see
44+
* {@link https://mokkapps.de/vue-tips/check-if-component-has-event-listener-attached}
45+
*
46+
* Usage here is intentionally different from the linked article: for reasons
47+
* unknown, {@link getCurrentInstance} returns `null` called in a
48+
* {@link computed} function body (or any function body), but produces the
49+
* expected value assigned to a top level value as it is here.
50+
*/
51+
const instance = getCurrentInstance();
52+
53+
type OdkWebFormEmitsEventType = keyof OdkWebFormEmits;
54+
55+
/**
56+
* A Vue _template_ event handler is subscribed with syntax like:
57+
*
58+
* ```vue
59+
* <OdkWebForm @whatever-event-type="handler" />
60+
* ```
61+
*
62+
* At runtime, its props key is a concatenation of the prefix "on" and the
63+
* PascalCase variant of the same event type. Since we already
64+
* {@link defineEmits} in camelCase, this type represents that key format.
65+
*/
66+
type EventKey = `on${Capitalize<OdkWebFormEmitsEventType>}`;
67+
68+
/**
69+
* @see {@link https://mokkapps.de/vue-tips/check-if-component-has-event-listener-attached}
70+
* @see {@link instance}
71+
* @see {@link EventKey}
72+
*/
73+
const isEmitSubscribed = (eventKey: EventKey): boolean => {
74+
return eventKey in (instance?.vnode.props ?? {});
75+
};
76+
77+
const emitSubmit = async (root: RootNode) => {
78+
if (isEmitSubscribed('onSubmit')) {
79+
const payload = await root.prepareSubmission({
80+
chunked: 'monolithic',
81+
});
82+
83+
emit('submit', payload);
84+
}
85+
};
86+
87+
const emitSubmitChunked = async (root: RootNode) => {
88+
if (isEmitSubscribed('onSubmitChunked')) {
89+
const maxSize = props.submissionMaxSize;
90+
91+
if (maxSize == null) {
92+
throw new Error('The `submissionMaxSize` prop is required for chunked submissions');
93+
}
94+
95+
const payload = await root.prepareSubmission({
96+
chunked: 'chunked',
97+
maxSize,
98+
});
99+
100+
emit('submitChunked', payload);
101+
}
102+
};
103+
104+
const emit = defineEmits<OdkWebFormEmits>();
21105
22106
const odkForm = ref<RootNode>();
23107
const submitPressed = ref(false);
@@ -38,8 +122,13 @@ initializeForm(props.formXml, {
38122
});
39123
40124
const handleSubmit = () => {
41-
if (odkForm.value?.validationState.violations?.length === 0) {
42-
emit('submit');
125+
const root = odkForm.value;
126+
127+
if (root?.validationState.violations?.length === 0) {
128+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
129+
emitSubmit(root);
130+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
131+
emitSubmitChunked(root);
43132
} else {
44133
submitPressed.value = true;
45134
document.scrollingElement?.scrollTo(0, 0);

packages/web-forms/src/demo/FormPreview.vue

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
<script setup lang="ts">
22
import { xformFixturesByCategory, XFormResource } from '@getodk/common/fixtures/xforms.ts';
3-
import type { FetchFormAttachment, MissingResourceBehavior } from '@getodk/xforms-engine';
3+
import type {
4+
ChunkedSubmissionResult,
5+
FetchFormAttachment,
6+
MissingResourceBehavior,
7+
MonolithicSubmissionResult,
8+
} from '@getodk/xforms-engine';
49
import { constants as ENGINE_CONSTANTS } from '@getodk/xforms-engine';
510
import { ref } from 'vue';
611
import { useRoute } from 'vue-router';
@@ -50,17 +55,27 @@ xformResource
5055
alert('Failed to load the Form XML');
5156
});
5257
53-
const handleSubmit = () => {
58+
const handleSubmit = (payload: MonolithicSubmissionResult) => {
59+
// eslint-disable-next-line no-console
60+
console.log('submission payload:', payload);
61+
5462
alert(`Submit button was pressed`);
5563
};
64+
65+
const handleSubmitChunked = (payload: ChunkedSubmissionResult) => {
66+
// eslint-disable-next-line no-console
67+
console.log('CHUNKED submission payload:', payload);
68+
};
5669
</script>
5770
<template>
5871
<template v-if="formPreviewState">
5972
<OdkWebForm
6073
:form-xml="formPreviewState.formXML"
6174
:fetch-form-attachment="formPreviewState.fetchFormAttachment"
6275
:missing-resource-behavior="formPreviewState.missingResourceBehavior"
76+
:submission-max-size="Infinity"
6377
@submit="handleSubmit"
78+
@submit-chunked="handleSubmitChunked"
6479
/>
6580
<FeedbackButton />
6681
</template>

packages/xforms-engine/src/client/RootNode.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import type { RootDefinition } from '../parse/model/RootDefinition.ts';
33
import type { BaseNode, BaseNodeState } from './BaseNode.ts';
44
import type { ActiveLanguage, FormLanguage, FormLanguages } from './FormLanguage.ts';
55
import type { GeneralChildNode } from './hierarchy.ts';
6-
import type { SubmissionChunkedType, SubmissionOptions } from './submission/SubmissionOptions.ts';
7-
import type { SubmissionResult } from './submission/SubmissionResult.ts';
6+
import type { SubmissionOptions } from './submission/SubmissionOptions.ts';
7+
import type {
8+
ChunkedSubmissionResult,
9+
MonolithicSubmissionResult,
10+
SubmissionResult,
11+
} from './submission/SubmissionResult.ts';
812
import type { AncestorNodeValidationState } from './validation.ts';
913

1014
export interface RootNodeState extends BaseNodeState {
@@ -84,7 +88,7 @@ export interface RootNode extends BaseNode {
8488
* A client may specify {@link SubmissionOptions<'chunked'>}, in which case a
8589
* {@link SubmissionResult<'chunked'>} will be produced, with form attachments
8690
*/
87-
prepareSubmission<ChunkedType extends SubmissionChunkedType>(
88-
options?: SubmissionOptions<ChunkedType>
89-
): Promise<SubmissionResult<ChunkedType>>;
91+
prepareSubmission(): Promise<MonolithicSubmissionResult>;
92+
prepareSubmission(options: SubmissionOptions<'monolithic'>): Promise<MonolithicSubmissionResult>;
93+
prepareSubmission(options: SubmissionOptions<'chunked'>): Promise<ChunkedSubmissionResult>;
9094
}

0 commit comments

Comments
 (0)