Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emit submission payload(s) to host app #291

Merged
merged 3 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>;
}
Loading