Skip to content

Commit

Permalink
Merge pull request #291 from getodk/features/emit-submission-payload-…
Browse files Browse the repository at this point in the history
…to-host-app

Emit submission payload(s) to host app
  • Loading branch information
eyelidlessness authored Jan 27, 2025
2 parents 0700362 + c60e37a commit f1fecfb
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 21 deletions.
6 changes: 6 additions & 0 deletions .changeset/friendly-monkeys-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@getodk/web-forms': minor
---

- Emit submission payload when subscribed to `submit` event
- Emit chunked submission payload when subscribed to new `submitChunked` event
5 changes: 5 additions & 0 deletions .changeset/strange-needles-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@getodk/xforms-engine': patch
---

Fix: correct types for chunked/monolithic submission result
9 changes: 2 additions & 7 deletions packages/scenario/src/jr/Scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import type {
RepeatRangeUncontrolledNode,
RootNode,
SelectNode,
SubmissionChunkedType,
SubmissionOptions,
SubmissionResult,
} from '@getodk/xforms-engine';
import { constants as ENGINE_CONSTANTS } from '@getodk/xforms-engine';
import type { Accessor, Setter } from 'solid-js';
Expand Down Expand Up @@ -965,10 +962,8 @@ export class Scenario {
* more about Collect's responsibility for submission (beyond serialization,
* already handled by {@link proposed_serializeInstance}).
*/
prepareWebFormsSubmission<ChunkedType extends SubmissionChunkedType>(
options?: SubmissionOptions<ChunkedType>
): Promise<SubmissionResult<ChunkedType>> {
return this.instanceRoot.prepareSubmission<ChunkedType>(options);
prepareWebFormsSubmission() {
return this.instanceRoot.prepareSubmission();
}

// TODO: consider adapting tests which use the following interfaces to use
Expand Down
103 changes: 96 additions & 7 deletions packages/web-forms/src/components/OdkWebForm.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,107 @@
<script setup lang="ts">
import type { MissingResourceBehavior } from '@getodk/xforms-engine';
import type {
ChunkedSubmissionResult,
MissingResourceBehavior,
MonolithicSubmissionResult,
} from '@getodk/xforms-engine';
import { initializeForm, type FetchFormAttachment, type RootNode } from '@getodk/xforms-engine';
import Button from 'primevue/button';
import Card from 'primevue/card';
import PrimeMessage from 'primevue/message';
import { computed, provide, reactive, ref, watchEffect, type ComponentPublicInstance } from 'vue';
import type { ComponentPublicInstance } from 'vue';
import { computed, getCurrentInstance, provide, reactive, ref, watchEffect } from 'vue';
import { FormInitializationError } from '../lib/error/FormInitializationError.ts';
import FormLoadFailureDialog from './Form/FormLoadFailureDialog.vue';
import FormHeader from './FormHeader.vue';
import QuestionList from './QuestionList.vue';
const webFormsVersion = __WEB_FORMS_VERSION__;
const props = defineProps<{
interface OdkWebFormsProps {
formXml: string;
fetchFormAttachment: FetchFormAttachment;
missingResourceBehavior?: MissingResourceBehavior;
}>();
const emit = defineEmits(['submit']);
/**
* Note: this parameter must be set when subscribing to the
* {@link OdkWebFormEmits.submitChunked | submitChunked} event.
*/
submissionMaxSize?: number;
}
const props = defineProps<OdkWebFormsProps>();
// 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`.
type OdkWebFormEmits = {
submit: [submissionPayload: MonolithicSubmissionResult];
submitChunked: [submissionPayload: ChunkedSubmissionResult];
};
/**
* Supports {@link isEmitSubscribed}.
*
* @see
* {@link https://mokkapps.de/vue-tips/check-if-component-has-event-listener-attached}
*
* Usage here is intentionally different from the linked article: for reasons
* unknown, {@link getCurrentInstance} returns `null` called in a
* {@link computed} function body (or any function body), but produces the
* expected value assigned to a top level value as it is here.
*/
const instance = getCurrentInstance();
type OdkWebFormEmitsEventType = keyof OdkWebFormEmits;
/**
* A Vue _template_ event handler is subscribed with syntax like:
*
* ```vue
* <OdkWebForm @whatever-event-type="handler" />
* ```
*
* At runtime, its props key is a concatenation of the prefix "on" and the
* PascalCase variant of the same event type. Since we already
* {@link defineEmits} in camelCase, this type represents that key format.
*/
type EventKey = `on${Capitalize<OdkWebFormEmitsEventType>}`;
/**
* @see {@link https://mokkapps.de/vue-tips/check-if-component-has-event-listener-attached}
* @see {@link instance}
* @see {@link EventKey}
*/
const isEmitSubscribed = (eventKey: EventKey): boolean => {
return eventKey in (instance?.vnode.props ?? {});
};
const emitSubmit = async (root: RootNode) => {
if (isEmitSubscribed('onSubmit')) {
const payload = await root.prepareSubmission({
chunked: 'monolithic',
});
emit('submit', payload);
}
};
const emitSubmitChunked = async (root: RootNode) => {
if (isEmitSubscribed('onSubmitChunked')) {
const maxSize = props.submissionMaxSize;
if (maxSize == null) {
throw new Error('The `submissionMaxSize` prop is required for chunked submissions');
}
const payload = await root.prepareSubmission({
chunked: 'chunked',
maxSize,
});
emit('submitChunked', payload);
}
};
const emit = defineEmits<OdkWebFormEmits>();
const odkForm = ref<RootNode>();
const submitPressed = ref(false);
Expand All @@ -38,8 +122,13 @@ initializeForm(props.formXml, {
});
const handleSubmit = () => {
if (odkForm.value?.validationState.violations?.length === 0) {
emit('submit');
const root = odkForm.value;
if (root?.validationState.violations?.length === 0) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
emitSubmit(root);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
emitSubmitChunked(root);
} else {
submitPressed.value = true;
document.scrollingElement?.scrollTo(0, 0);
Expand Down
19 changes: 17 additions & 2 deletions packages/web-forms/src/demo/FormPreview.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<script setup lang="ts">
import { xformFixturesByCategory, XFormResource } from '@getodk/common/fixtures/xforms.ts';
import type { FetchFormAttachment, MissingResourceBehavior } from '@getodk/xforms-engine';
import type {
ChunkedSubmissionResult,
FetchFormAttachment,
MissingResourceBehavior,
MonolithicSubmissionResult,
} from '@getodk/xforms-engine';
import { constants as ENGINE_CONSTANTS } from '@getodk/xforms-engine';
import { ref } from 'vue';
import { useRoute } from 'vue-router';
Expand Down Expand Up @@ -50,17 +55,27 @@ xformResource
alert('Failed to load the Form XML');
});
const handleSubmit = () => {
const handleSubmit = (payload: MonolithicSubmissionResult) => {
// eslint-disable-next-line no-console
console.log('submission payload:', payload);
alert(`Submit button was pressed`);
};
const handleSubmitChunked = (payload: ChunkedSubmissionResult) => {
// eslint-disable-next-line no-console
console.log('CHUNKED submission payload:', payload);
};
</script>
<template>
<template v-if="formPreviewState">
<OdkWebForm
:form-xml="formPreviewState.formXML"
:fetch-form-attachment="formPreviewState.fetchFormAttachment"
:missing-resource-behavior="formPreviewState.missingResourceBehavior"
:submission-max-size="Infinity"
@submit="handleSubmit"
@submit-chunked="handleSubmitChunked"
/>
<FeedbackButton />
</template>
Expand Down
14 changes: 9 additions & 5 deletions packages/xforms-engine/src/client/RootNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import type { RootDefinition } from '../parse/model/RootDefinition.ts';
import type { BaseNode, BaseNodeState } from './BaseNode.ts';
import type { ActiveLanguage, FormLanguage, FormLanguages } from './FormLanguage.ts';
import type { GeneralChildNode } from './hierarchy.ts';
import type { SubmissionChunkedType, SubmissionOptions } from './submission/SubmissionOptions.ts';
import type { SubmissionResult } from './submission/SubmissionResult.ts';
import type { SubmissionOptions } from './submission/SubmissionOptions.ts';
import type {
ChunkedSubmissionResult,
MonolithicSubmissionResult,
SubmissionResult,
} from './submission/SubmissionResult.ts';
import type { AncestorNodeValidationState } from './validation.ts';

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

0 comments on commit f1fecfb

Please sign in to comment.