1
1
<script setup lang="ts">
2
+ import type { FormStateSuccessResult } from ' @/lib/init/FormState.ts' ;
3
+ import { initializeFormState } from ' @/lib/init/initializeFormState.ts' ;
4
+ import type { EditInstanceOptions } from ' @/lib/init/loadFormState' ;
5
+ import { loadFormState } from ' @/lib/init/loadFormState' ;
6
+ import { updateSubmittedFormState } from ' @/lib/init/updateSubmittedFormState.ts' ;
7
+ import type {
8
+ HostSubmissionResultCallback ,
9
+ OptionalAwaitableHostSubmissionResult ,
10
+ } from ' @/lib/submission/HostSubmissionResultCallback.ts' ;
2
11
import type {
3
12
ChunkedInstancePayload ,
4
13
FetchFormAttachment ,
5
14
MissingResourceBehavior ,
6
15
MonolithicInstancePayload ,
7
- RootNode ,
8
16
} from ' @getodk/xforms-engine' ;
9
- import { loadForm } from ' @getodk/xforms-engine' ;
10
17
import Button from ' primevue/button' ;
11
18
import Card from ' primevue/card' ;
12
19
import PrimeMessage from ' primevue/message' ;
13
20
import type { ComponentPublicInstance } from ' vue' ;
14
- import { computed , getCurrentInstance , provide , reactive , ref , watchEffect } from ' vue' ;
15
- import { FormInitializationError } from ' ../lib/error/FormInitializationError.ts' ;
21
+ import { computed , getCurrentInstance , provide , ref , watchEffect } from ' vue' ;
16
22
import FormLoadFailureDialog from ' ./Form/FormLoadFailureDialog.vue' ;
17
23
import FormHeader from ' ./FormHeader.vue' ;
18
24
import QuestionList from ' ./QuestionList.vue' ;
19
25
20
26
const webFormsVersion = __WEB_FORMS_VERSION__ ;
21
27
22
- interface OdkWebFormsProps {
23
- formXml: string ;
24
- fetchFormAttachment: FetchFormAttachment ;
25
- missingResourceBehavior? : MissingResourceBehavior ;
28
+ export interface OdkWebFormsProps {
29
+ readonly formXml: string ;
30
+ readonly fetchFormAttachment: FetchFormAttachment ;
31
+ readonly missingResourceBehavior? : MissingResourceBehavior ;
26
32
27
33
/**
28
34
* Note: this parameter must be set when subscribing to the
29
35
* {@link OdkWebFormEmits.submitChunked | submitChunked} event.
30
36
*/
31
- submissionMaxSize? : number ;
37
+ readonly submissionMaxSize? : number ;
38
+
39
+ /**
40
+ * If provided by a host application, referenced instance and attachment
41
+ * resources will be resolved and loaded for editing.
42
+ */
43
+ readonly editInstance? : EditInstanceOptions ;
32
44
}
33
45
34
46
const props = defineProps <OdkWebFormsProps >();
35
47
48
+ const hostSubmissionResultCallbackFactory = (
49
+ currentState : FormStateSuccessResult
50
+ ): HostSubmissionResultCallback => {
51
+ const handleHostSubmissionResult = async (
52
+ hostResult : OptionalAwaitableHostSubmissionResult
53
+ ): Promise <void > => {
54
+ const submissionResult = await hostResult ;
55
+
56
+ state .value = updateSubmittedFormState (submissionResult , currentState );
57
+ };
58
+
59
+ return (hostResult ) => {
60
+ void handleHostSubmissionResult (hostResult );
61
+ };
62
+ };
63
+
36
64
// 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`.
37
65
type OdkWebFormEmits = {
38
- submit: [submissionPayload : MonolithicInstancePayload ];
39
- submitChunked: [submissionPayload : ChunkedInstancePayload ];
66
+ submit: [submissionPayload : MonolithicInstancePayload , callback : HostSubmissionResultCallback ];
67
+ submitChunked: [
68
+ submissionPayload : ChunkedInstancePayload ,
69
+ callback : HostSubmissionResultCallback ,
70
+ ];
40
71
};
41
72
42
73
/**
@@ -50,7 +81,7 @@ type OdkWebFormEmits = {
50
81
* {@link computed } function body (or any function body), but produces the
51
82
* expected value assigned to a top level value as it is here.
52
83
*/
53
- const instance = getCurrentInstance ();
84
+ const componentInstance = getCurrentInstance ();
54
85
55
86
type OdkWebFormEmitsEventType = keyof OdkWebFormEmits ;
56
87
@@ -69,79 +100,67 @@ type EventKey = `on${Capitalize<OdkWebFormEmitsEventType>}`;
69
100
70
101
/**
71
102
* @see {@link https://mokkapps.de/vue-tips/check-if-component-has-event-listener-attached }
72
- * @see {@link instance }
103
+ * @see {@link componentInstance }
73
104
* @see {@link EventKey }
74
105
*/
75
106
const isEmitSubscribed = (eventKey : EventKey ): boolean => {
76
- return eventKey in (instance ?.vnode .props ?? {});
107
+ return eventKey in (componentInstance ?.vnode .props ?? {});
77
108
};
78
109
79
- const emitSubmit = async (root : RootNode ) => {
110
+ const emitSubmit = async (currentState : FormStateSuccessResult ) => {
80
111
if (isEmitSubscribed (' onSubmit' )) {
81
- const payload = await root .prepareInstancePayload ({
112
+ const payload = await currentState . root .prepareInstancePayload ({
82
113
payloadType: ' monolithic' ,
83
114
});
115
+ const callback = hostSubmissionResultCallbackFactory (currentState );
84
116
85
- emit (' submit' , payload );
117
+ emit (' submit' , payload , callback );
86
118
}
87
119
};
88
120
89
- const emitSubmitChunked = async (root : RootNode ) => {
121
+ const emitSubmitChunked = async (currentState : FormStateSuccessResult ) => {
90
122
if (isEmitSubscribed (' onSubmitChunked' )) {
91
123
const maxSize = props .submissionMaxSize ;
92
124
93
125
if (maxSize == null ) {
94
126
throw new Error (' The `submissionMaxSize` prop is required for chunked submissions' );
95
127
}
96
128
97
- const payload = await root .prepareInstancePayload ({
129
+ const payload = await currentState . root .prepareInstancePayload ({
98
130
payloadType: ' chunked' ,
99
131
maxSize ,
100
132
});
133
+ const callback = hostSubmissionResultCallbackFactory (currentState );
101
134
102
- emit (' submitChunked' , payload );
135
+ emit (' submitChunked' , payload , callback );
103
136
}
104
137
};
105
138
106
139
const emit = defineEmits <OdkWebFormEmits >();
107
140
108
- const odkForm = ref < RootNode > ();
141
+ const state = initializeFormState ();
109
142
const submitPressed = ref (false );
110
- const initializeFormError = ref <FormInitializationError | null >();
111
143
112
144
const init = async () => {
113
- const { formXml, fetchFormAttachment, missingResourceBehavior } = props ;
114
-
115
- const formResult = await loadForm (formXml , {
116
- fetchFormAttachment ,
117
- missingResourceBehavior ,
145
+ state .value = await loadFormState (props .formXml , {
146
+ form: {
147
+ fetchFormAttachment: props .fetchFormAttachment ,
148
+ missingResourceBehavior: props .missingResourceBehavior ,
149
+ },
150
+ editInstance: props .editInstance ?? null ,
118
151
});
119
-
120
- if (formResult .status === ' failure' ) {
121
- initializeFormError .value = FormInitializationError .fromError (formResult .error );
122
-
123
- return ;
124
- }
125
-
126
- try {
127
- const { root } = formResult .createInstance ({ stateFactory: reactive });
128
-
129
- odkForm .value = root ;
130
- } catch (error ) {
131
- initializeFormError .value = FormInitializationError .from (error );
132
- }
133
152
};
134
153
135
154
void init ();
136
155
137
- const handleSubmit = () => {
138
- const root = odkForm . value ;
156
+ const handleSubmit = (currentState : FormStateSuccessResult ) => {
157
+ const { root } = currentState ;
139
158
140
- if (root ? .validationState .violations ? .length === 0 ) {
159
+ if (root .validationState .violations .length === 0 ) {
141
160
// eslint-disable-next-line @typescript-eslint/no-floating-promises
142
- emitSubmit (root );
161
+ emitSubmit (currentState );
143
162
// eslint-disable-next-line @typescript-eslint/no-floating-promises
144
- emitSubmitChunked (root );
163
+ emitSubmitChunked (currentState );
145
164
} else {
146
165
submitPressed .value = true ;
147
166
document .scrollingElement ?.scrollTo (0 , 0 );
@@ -153,7 +172,7 @@ const validationErrorMessagePopover = ref<ComponentPublicInstance | null>(null);
153
172
provide (' submitPressed' , submitPressed );
154
173
155
174
const validationErrorMessage = computed (() => {
156
- const violationLength = odkForm .value ! . validationState .violations .length ;
175
+ const violationLength = state .value . root ?. validationState .violations .length ?? 0 ;
157
176
158
177
// TODO: translations
159
178
if (violationLength === 0 ) return ' ' ;
@@ -180,21 +199,21 @@ watchEffect(() => {
180
199
<div
181
200
:class =" {
182
201
'form-initialization-status': true,
183
- loading: odkForm == null && initializeFormError == null ,
184
- error: initializeFormError != null ,
185
- ready: odkForm != null ,
202
+ loading: state.status === 'FORM_STATE_LOADING' ,
203
+ error: state.status === 'FORM_STATE_FAILURE' ,
204
+ ready: state.status === 'FORM_STATE_SUCCESS' ,
186
205
}"
187
206
/>
188
207
189
- <template v-if =" initializeFormError != null " >
208
+ <template v-if =" state . status === ' FORM_STATE_FAILURE ' " >
190
209
<FormLoadFailureDialog
191
210
severity =" error"
192
- :error =" initializeFormError "
211
+ :error =" state.error "
193
212
/>
194
213
</template >
195
214
196
215
<div
197
- v-else-if =" odkForm "
216
+ v-else-if =" state.status === 'FORM_STATE_SUCCESS' "
198
217
class =" odk-form"
199
218
:class =" { 'submit-pressed': submitPressed }"
200
219
>
@@ -204,21 +223,20 @@ watchEffect(() => {
204
223
{{ validationErrorMessage }}
205
224
</PrimeMessage >
206
225
207
- <FormHeader :form =" odkForm " />
226
+ <FormHeader :form =" state.root " />
208
227
209
228
<Card class =" questions-card" >
210
229
<template #content >
211
230
<div class =" form-questions" >
212
231
<div class =" flex flex-column gap-2" >
213
- <QuestionList :nodes =" odkForm .currentState.children" />
232
+ <QuestionList :nodes =" state.root .currentState.children" />
214
233
</div >
215
234
</div >
216
235
</template >
217
236
</Card >
218
237
219
238
<div class =" footer flex justify-content-end flex-wrap gap-3" >
220
- <!-- maybe current state is in odkForm.state.something -->
221
- <Button label =" Send" rounded @click =" handleSubmit()" />
239
+ <Button label =" Send" rounded @click =" handleSubmit(state)" />
222
240
</div >
223
241
</div >
224
242
0 commit comments