Skip to content

Add stepper-layout prop to OdkWebForm component for Collect-like experience #329

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
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
38 changes: 38 additions & 0 deletions packages/common/src/fixtures/other/multi-step.xform.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms"
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:h="http://www.w3.org/1999/xhtml"
xmlns:jr="http://openrosa.org/javarosa"
xmlns:orx="http://openrosa.org/xforms/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<h:head>
<h:title>Multi Step XForm</h:title>
<model>
<instance>
<root id="minimal">
<first-question/>
<second-question/>
<third-question/>
<meta>
<instanceID/>
</meta>
</root>
</instance>
<bind nodeset="/root/first-question" type="string" required="true()"/>
<bind nodeset="/root/second-question" type="string" required="true()"/>
<bind nodeset="/root/third-question" type="string" required="true()"/>
<bind nodeset="/root/meta/instanceID" type="string"/>
</model>
</h:head>
<h:body>
<input ref="/root/first-question">
<label>First question</label>
</input>
<input ref="/root/second-question">
<label>Second question</label>
</input>
<input ref="/root/third-question">
<label>Third question</label>
</input>
</h:body>
</h:html>
6 changes: 4 additions & 2 deletions packages/web-forms/src/components/FormGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { computed } from 'vue';
import FormPanel from './FormPanel.vue';
import QuestionList from './QuestionList.vue';

const props = defineProps<{ node: GroupNode }>();
const props = withDefaults(defineProps<{ node: GroupNode, toggleable?: boolean }>(), {
toggleable: false,
});

const classes = ['group'];

Expand All @@ -18,7 +20,7 @@ const tableLayout = computed(() => {
</script>

<template>
<FormPanel :title="node.currentState.label?.asString" :no-ui="!node.currentState.label" :class="classes">
<FormPanel :title="node.currentState.label?.asString" :no-ui="!node.currentState.label" :toggleable="props.toggleable" :class="classes">
<div :class="{ 'table-layout': tableLayout, 'gap-2': !tableLayout, 'flex': true, 'flex-column': true }">
<QuestionList :nodes="node.currentState.children" />
</div>
Expand Down
26 changes: 21 additions & 5 deletions packages/web-forms/src/components/FormPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface PanelProps {
class?: string[] | string;
labelIcon?: string;
labelNumber?: number;
toggleable?: boolean;
}

const props = withDefaults(defineProps<PanelProps>(), {
Expand All @@ -21,6 +22,7 @@ const props = withDefaults(defineProps<PanelProps>(), {
class: undefined,
labelIcon: undefined,
labelNumber: undefined,
toggleable: false,
});

const panelClass = computed(() => [
Expand All @@ -31,7 +33,9 @@ const panelClass = computed(() => [
const panelState = ref(false);

const toggle = () => {
panelState.value = !panelState.value;
if (props.toggleable) {
panelState.value = !panelState.value;
}
};

const menu = ref<Menu & MenuState>();
Expand All @@ -40,17 +44,25 @@ const toggleMenu = (event: Event) => {
menu.value?.toggle(event);
};
</script>

<template>
<Panel v-if="!noUi" :class="panelClass" :toggleable="true" :collapsed="panelState">
<Panel v-if="!noUi" :class="[panelClass, { 'toggleable-enabled': toggleable }]" :toggleable="toggleable" :collapsed="toggleable ? panelState : false">
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the toggleable-enabled class dynamically

<template #header>
<div class="panel-title" role="button" @click="toggle">
<div v-if="toggleable" class="panel-title" role="button" @click="toggle">
<h2>
<span class="chevron" :class="panelState ? 'icon-keyboard_arrow_down' : 'icon-keyboard_arrow_up'" />
<span v-if="labelNumber" class="label-number">{{ labelNumber }}</span>
<span>{{ title }}</span>
<span v-if="labelIcon" class="ml-2" :class="labelIcon" />
</h2>
</div>
<div v-else>
<h2>
<span v-if="labelNumber" class="label-number">{{ labelNumber }}</span>
<span>{{ title }}</span>
<span v-if="labelIcon" class="ml-2" :class="labelIcon" />
</h2>
</div>
</template>
<template v-if="menuItems && menuItems.length > 0" #icons>
<Button severity="secondary" rounded class="btn-context" :class="{ 'p-focus': menu?.overlayVisible }" icon="icon-more_vert" aria-label="More" @click="toggleMenu" />
Expand Down Expand Up @@ -129,9 +141,8 @@ h2 {
}

:deep(.p-panel-content) {
border-left: 2px solid var(--gray-200);
box-shadow: none;
margin-left: 10px;
border-radius: 0;
padding: 0 0 0 1.5rem;
}

Expand All @@ -140,6 +151,11 @@ h2 {
}
}

.toggleable-enabled :deep(.p-panel-content) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I only add the style for the line on the left of the group if the panel is set to toggleable

Screenshot 2025-03-10 135310

border-left: 2px solid var(--gray-200);
border-radius: 0;
}

.content-wrapper {
display: flex;
flex-direction: column;
Expand Down
20 changes: 16 additions & 4 deletions packages/web-forms/src/components/OdkWebForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { computed, getCurrentInstance, provide, ref, watchEffect } from 'vue';
import FormLoadFailureDialog from './Form/FormLoadFailureDialog.vue';
import FormHeader from './FormHeader.vue';
import QuestionList from './QuestionList.vue';
import QuestionStepper from './QuestionStepper.vue';

const webFormsVersion = __WEB_FORMS_VERSION__;

Expand All @@ -30,6 +31,13 @@ export interface OdkWebFormsProps {
readonly fetchFormAttachment: FetchFormAttachment;
readonly missingResourceBehavior?: MissingResourceBehavior;

/**
* Note: by default all questions will be displayed in a single list,
* with collapsable groups. This param changes to a stepper layout
* closer to Collect.
*/
readonly stepperLayout?: boolean;

/**
* Note: this parameter must be set when subscribing to the
* {@link OdkWebFormEmits.submitChunked | submitChunked} event.
Expand All @@ -43,7 +51,9 @@ export interface OdkWebFormsProps {
readonly editInstance?: EditInstanceOptions;
}

const props = defineProps<OdkWebFormsProps>();
const props = withDefaults(defineProps<OdkWebFormsProps>(), {
stepperLayout: false,
});

const hostSubmissionResultCallbackFactory = (
currentState: FormStateSuccessResult
Expand Down Expand Up @@ -229,18 +239,20 @@ watchEffect(() => {
<template #content>
<div class="form-questions">
<div class="flex flex-column gap-2">
<QuestionList :nodes="state.root.currentState.children" />
<QuestionList v-if="!props.stepperLayout" :nodes="state.root.currentState.children" />
<!-- Note that QuestionStepper has the 'Send' button integrated instead of using the button below -->
<QuestionStepper v-if="props.stepperLayout" :nodes="state.root.currentState.children" @sendFormFromStepper="handleSubmit(state)" />
</div>
</div>
</template>
</Card>

<div class="footer flex justify-content-end flex-wrap gap-3">
<div v-if="!props.stepperLayout" class="footer flex justify-content-end flex-wrap gap-3">
<Button label="Send" rounded @click="handleSubmit(state)" />
</div>
</div>

<div class="powered-by-wrapper">
<div v-if="!props.stepperLayout" class="powered-by-wrapper">
<a class="anchor" href="https://getodk.org" target="_blank">
<span class="caption">Powered by</span>
<img
Expand Down
2 changes: 1 addition & 1 deletion packages/web-forms/src/components/QuestionList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const isControlNode = (node: NonStructuralNode): node is ControlNode => {
<template v-for="node in nodes" :key="node.nodeId">
<template v-if="node.currentState.relevant">
<!-- Render group nodes -->
<FormGroup v-if="isGroupNode(node)" :node="node" />
<FormGroup v-if="isGroupNode(node)" :node="node" toggleable />
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set the default for the prop toggleable to false, meaning the prop needs to be passed here to allow for the collapsable panels in the list view


<!-- Render repeat nodes -->
<RepeatRange v-else-if="isRepeatRangeNode(node)" :node="node" />
Expand Down
Loading