Skip to content

Commit 3dfd7d9

Browse files
web-forms (Vue UI): quick & dirty reset demo state on “submit”, “restore” state
This definitely needs refinement if we’re going to keep it. I felt it was valuable at this stage in the engine/client interface evolution to integrate the following to see the behavior in action: - Load form state independent of instance - Still create instance on load - On “submit” - Create a new instance (resets form state immediately) - Simulate an “offline queue”-like experience, storing “submitted” instance payloads in a list where they can be: - Removed individually - Cleared completely - **RESTORED** from their payload state The latter isn’t fully working in the engine yet, but the engine/client interface is far enough along that this integration should demonstrate it once it is!
1 parent 1aabc15 commit 3dfd7d9

File tree

5 files changed

+326
-8
lines changed

5 files changed

+326
-8
lines changed

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

+10-7
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
<script setup lang="ts">
2+
import {
3+
initializeFormResultState,
4+
initializeInstanceState,
5+
instanceState,
6+
} from '@/shared-state/form-state.ts';
27
import type {
38
ChunkedInstancePayload,
49
FetchFormAttachment,
510
MissingResourceBehavior,
611
MonolithicInstancePayload,
712
RootNode,
813
} from '@getodk/xforms-engine';
9-
import { loadForm } from '@getodk/xforms-engine';
1014
import Button from 'primevue/button';
1115
import Card from 'primevue/card';
1216
import PrimeMessage from 'primevue/message';
1317
import type { ComponentPublicInstance } from 'vue';
14-
import { computed, getCurrentInstance, provide, reactive, ref, watchEffect } from 'vue';
18+
import { computed, getCurrentInstance, provide, ref, watchEffect } from 'vue';
1519
import { FormInitializationError } from '../lib/error/FormInitializationError.ts';
1620
import FormLoadFailureDialog from './Form/FormLoadFailureDialog.vue';
1721
import FormHeader from './FormHeader.vue';
@@ -105,14 +109,15 @@ const emitSubmitChunked = async (root: RootNode) => {
105109
106110
const emit = defineEmits<OdkWebFormEmits>();
107111
108-
const odkForm = ref<RootNode>();
112+
const odkForm = computed(() => instanceState.value?.root);
113+
109114
const submitPressed = ref(false);
110115
const initializeFormError = ref<FormInitializationError | null>();
111116
112117
const init = async () => {
113118
const { formXml, fetchFormAttachment, missingResourceBehavior } = props;
114119
115-
const formResult = await loadForm(formXml, {
120+
const formResult = await initializeFormResultState(formXml, {
116121
fetchFormAttachment,
117122
missingResourceBehavior,
118123
});
@@ -124,9 +129,7 @@ const init = async () => {
124129
}
125130
126131
try {
127-
const { root } = formResult.createInstance({ stateFactory: reactive });
128-
129-
odkForm.value = root;
132+
initializeInstanceState(formResult);
130133
} catch (error) {
131134
initializeFormError.value = FormInitializationError.from(error);
132135
}

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import { resetInstanceState } from '@/shared-state/form-state.ts';
23
import { xformFixturesByCategory, XFormResource } from '@getodk/common/fixtures/xforms.ts';
34
import type {
45
ChunkedInstancePayload,
@@ -10,7 +11,9 @@ import { constants as ENGINE_CONSTANTS } from '@getodk/xforms-engine';
1011
import { ref } from 'vue';
1112
import { useRoute } from 'vue-router';
1213
import OdkWebForm from '../components/OdkWebForm.vue';
14+
import { cacheInstance } from '../shared-state/instance-cache-state.ts';
1315
import FeedbackButton from './FeedbackButton.vue';
16+
import InstanceCache from './InstanceCache.vue';
1417
1518
const route = useRoute();
1619
@@ -59,7 +62,8 @@ const handleSubmit = (payload: MonolithicInstancePayload) => {
5962
// eslint-disable-next-line no-console
6063
console.log('submission payload:', payload);
6164
62-
alert(`Submit button was pressed`);
65+
cacheInstance(payload);
66+
resetInstanceState();
6367
};
6468
6569
const handleSubmitChunked = (payload: ChunkedInstancePayload) => {
@@ -69,6 +73,7 @@ const handleSubmitChunked = (payload: ChunkedInstancePayload) => {
6973
</script>
7074
<template>
7175
<template v-if="formPreviewState">
76+
<InstanceCache />
7277
<OdkWebForm
7378
:form-xml="formPreviewState.formXML"
7479
:fetch-form-attachment="formPreviewState.fetchFormAttachment"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<script setup lang="ts">
2+
import PrimeButton from 'primevue/button';
3+
import type { AnyInstance, InstantiableFormResult } from '../shared-state/form-state.ts';
4+
import { instantiableFormResult, restoreInstanceState } from '../shared-state/form-state.ts';
5+
import type { InstanceCacheItem } from '../shared-state/instance-cache-state.ts';
6+
import {
7+
clearCache,
8+
evictCachedInstance,
9+
instanceCache,
10+
} from '../shared-state/instance-cache-state.ts';
11+
12+
const formatTimeUnit = (timeUnit: number): string => {
13+
return timeUnit.toString().padStart(2, '0');
14+
};
15+
16+
const formatTimestamp = (date: Date): string => {
17+
const hours = date.getHours();
18+
const minutes = date.getMinutes();
19+
const seconds = date.getSeconds();
20+
21+
return [hours, minutes, seconds].map(formatTimeUnit).join(':');
22+
};
23+
24+
const restoreCacheItem = async (
25+
formResult: InstantiableFormResult,
26+
item: InstanceCacheItem
27+
): Promise<AnyInstance> => {
28+
const instance = await restoreInstanceState(formResult, item.payload);
29+
30+
evictCachedInstance(item);
31+
32+
return instance;
33+
};
34+
</script>
35+
36+
<template>
37+
<section v-if="instantiableFormResult" class="demo-instance-cache">
38+
<details>
39+
<summary>Instance cache</summary>
40+
41+
<p>This is an in-memory demo simulating storage and restoration of instance state, which might be similar to the experience of a Web Forms offline mode.</p>
42+
43+
<template v-if="instanceCache.items.length > 0">
44+
<div class="cache-header flex">
45+
<span>Cached instances ({{ instanceCache.items.length }})</span>
46+
47+
<PrimeButton
48+
text
49+
@click="clearCache()"
50+
>
51+
Clear
52+
</PrimeButton>
53+
</div>
54+
55+
<ul class="items">
56+
<li v-for="item in instanceCache.items" :key="item.cacheTimestamp.getDate()">
57+
<div class="item-row flex">
58+
<PrimeButton
59+
title="Remove from cache"
60+
class="remove-item"
61+
@click="evictCachedInstance(item)"
62+
>
63+
64+
</PrimeButton>
65+
66+
<div class="item-description">
67+
Instance cached at {{ formatTimestamp(item.cacheTimestamp) }}
68+
</div>
69+
70+
<PrimeButton class="restore-item" @click="restoreCacheItem(instantiableFormResult, item)">
71+
Restore instance
72+
</PrimeButton>
73+
</div>
74+
</li>
75+
</ul>
76+
</template>
77+
<template v-else>
78+
<p class="cache-empty">
79+
Cache is empty!
80+
</p>
81+
</template>
82+
</details>
83+
</section>
84+
</template>
85+
86+
<style scoped lang="scss">
87+
.demo-instance-cache {
88+
padding: 1rem;
89+
background-color: var(--surface-0);
90+
}
91+
92+
summary {
93+
cursor: pointer;
94+
}
95+
96+
.flex {
97+
display: flex;
98+
gap: 0.5rem;
99+
align-items: center;
100+
}
101+
102+
.cache-empty {
103+
font-size: 0.85rem;
104+
}
105+
106+
.cache-header {
107+
justify-content: space-between;
108+
}
109+
110+
.items,
111+
.items li {
112+
display: block;
113+
list-style: none;
114+
}
115+
116+
.items {
117+
margin: 0;
118+
padding: 0;
119+
}
120+
121+
.items li {
122+
margin: 0;
123+
padding: 0.25rem;
124+
}
125+
126+
.item-description {
127+
flex-grow: 1;
128+
}
129+
130+
.remove-item {
131+
display: block;
132+
width: 1.5rem;
133+
min-width: 0;
134+
height: 1.5rem;
135+
padding: 0;
136+
}
137+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type {
2+
CreatedFormInstance,
3+
FormInstanceConfig,
4+
LoadFormOptions,
5+
LoadFormResult,
6+
LoadFormSuccessResult,
7+
LoadFormWarningResult,
8+
RestoredFormInstance,
9+
} from '@getodk/xforms-engine';
10+
import { loadForm } from '@getodk/xforms-engine';
11+
import type { Ref } from 'vue';
12+
import { computed, reactive, ref } from 'vue';
13+
import type { AnyInstancePayload } from './instance-cache-state.ts';
14+
15+
// prettier-ignore
16+
export type InstantiableFormResult =
17+
| LoadFormSuccessResult
18+
| LoadFormWarningResult;
19+
20+
export const formResultState: Ref<LoadFormResult | null> = ref(null);
21+
22+
const isInstantiableFormResult = (
23+
formResult: LoadFormResult | null
24+
): formResult is InstantiableFormResult => {
25+
return formResult != null && formResult.status !== 'failure';
26+
};
27+
28+
export const instantiableFormResult = computed(() => {
29+
const formResult = formResultState.value;
30+
31+
if (isInstantiableFormResult(formResult)) {
32+
return formResult;
33+
}
34+
35+
return null;
36+
});
37+
38+
export const initializeFormResultState = async (
39+
formXML: string,
40+
options: LoadFormOptions
41+
): Promise<LoadFormResult> => {
42+
const formResult = await loadForm(formXML, options);
43+
44+
formResultState.value = formResult;
45+
46+
return formResult;
47+
};
48+
49+
const instanceConfig: FormInstanceConfig = {
50+
stateFactory: reactive,
51+
};
52+
53+
// prettier-ignore
54+
export type AnyInstance =
55+
| CreatedFormInstance
56+
| RestoredFormInstance;
57+
58+
export const instanceState: Ref<AnyInstance | null> = ref(null);
59+
60+
export const initializeInstanceState = (
61+
formResult: InstantiableFormResult
62+
): CreatedFormInstance => {
63+
const instance = formResult.createInstance(instanceConfig);
64+
65+
instanceState.value = instance;
66+
67+
return instanceState.value;
68+
};
69+
70+
interface FormInstanceState {
71+
readonly formResult: LoadFormResult;
72+
readonly instance: AnyInstance;
73+
}
74+
75+
export const initializeFormInstanceState = async (
76+
formXML: string,
77+
options: LoadFormOptions
78+
): Promise<FormInstanceState | null> => {
79+
const formResult = await initializeFormResultState(formXML, options);
80+
81+
if (formResult.status === 'failure') {
82+
instanceState.value = null;
83+
84+
return null;
85+
}
86+
87+
const instance = initializeInstanceState(formResult);
88+
89+
return {
90+
formResult,
91+
instance,
92+
};
93+
};
94+
95+
export const restoreInstanceState = async (
96+
formResult: InstantiableFormResult,
97+
payload: AnyInstancePayload
98+
): Promise<AnyInstance> => {
99+
const instance = await formResult.restoreInstance(payload, instanceConfig);
100+
101+
instanceState.value = instance;
102+
103+
return instance;
104+
};
105+
106+
export const resetInstanceState = () => {
107+
const formResult = formResultState.value;
108+
109+
if (isInstantiableFormResult(formResult)) {
110+
initializeInstanceState(formResult);
111+
}
112+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { InstancePayload, InstancePayloadType } from '@getodk/xforms-engine';
2+
import type { Ref } from 'vue';
3+
import { inject, provide, ref } from 'vue';
4+
5+
export type AnyInstancePayload = InstancePayload<InstancePayloadType>;
6+
7+
export interface InstanceCacheItem {
8+
readonly cacheTimestamp: Date;
9+
readonly payload: AnyInstancePayload;
10+
}
11+
12+
export interface InstanceCacheState {
13+
readonly items: readonly InstanceCacheItem[];
14+
}
15+
16+
const defaultInstanceCacheState: InstanceCacheState = {
17+
items: [],
18+
};
19+
20+
export const instanceCache: Ref<InstanceCacheState> =
21+
ref<InstanceCacheState>(defaultInstanceCacheState);
22+
23+
type SetItemsCallback = (items: readonly InstanceCacheItem[]) => readonly InstanceCacheItem[];
24+
25+
const setItems = (callback: SetItemsCallback): InstanceCacheState => {
26+
const current = instanceCache.value.items;
27+
28+
instanceCache.value = {
29+
items: callback(current),
30+
};
31+
32+
return instanceCache.value;
33+
};
34+
35+
export const cacheInstance = (payload: AnyInstancePayload): InstanceCacheState => {
36+
return setItems((current) => [
37+
...current,
38+
{
39+
cacheTimestamp: new Date(),
40+
payload,
41+
},
42+
]);
43+
};
44+
45+
export const evictCachedInstance = (instance: InstanceCacheItem): InstanceCacheState => {
46+
return setItems((items) => {
47+
return items.filter((item) => item !== instance);
48+
});
49+
};
50+
51+
export const provideInstanceCache = () => {
52+
return provide<Ref<InstanceCacheState>, 'instanceCache'>('instanceCache', instanceCache);
53+
};
54+
55+
export const injectInstanceCache = () => {
56+
return inject<Ref<InstanceCacheState>>('instance', instanceCache);
57+
};
58+
59+
export const clearCache = (): InstanceCacheState => {
60+
return setItems(() => []);
61+
};

0 commit comments

Comments
 (0)