Skip to main content

CnWizardDialog

Multi-step modal with per-step slots, back/next navigation, optional per-step validation, and a single result phase. Each step is declared in the steps[] prop ({ id, label, optional?, icon? }) and rendered via a #step-{id} named slot with the navigation API exposed through the slot scope.

Use CnFormDialog for single-step forms, CnRichSubmitDialog for single-step submit-with-files flows, and CnExportWizard for the specific scope+format+delivery export-trigger shape.

Try it

<template>
<div>
<NcButton @click="show = true">Bulk enrol</NcButton>

<CnWizardDialog
v-if="show"
ref="wizard"
dialog-title="Bulk enrol"
:steps="steps"
:validate="validateStep"
@submit="onSubmit"
@close="show = false">
<template #step-audience="{ stepData, setStepData }">
<CohortPicker :value="stepData.cohort" @input="v => setStepData({ cohort: v })" />
</template>
<template #step-course="{ stepData, setStepData }">
<CoursePicker :value="stepData.course" @input="v => setStepData({ course: v })" />
</template>
<template #step-confirm="{ stepData }">
<p>Enrol {{ stepData.cohort?.size }} learners into {{ stepData.course?.name }}?</p>
</template>
</CnWizardDialog>
</div>
</template>

<script>
import { CnWizardDialog } from '@conduction/nextcloud-vue'

export default {
components: { CnWizardDialog },
data() {
return {
show: false,
steps: [
{ id: 'audience', label: 'Audience' },
{ id: 'course', label: 'Course' },
{ id: 'confirm', label: 'Confirm' },
],
}
},
methods: {
validateStep(stepId, stepData) {
if (stepId === 'audience' && !stepData.cohort) return 'Pick a cohort first.'
if (stepId === 'course' && !stepData.course) return 'Pick a course first.'
return true
},
async onSubmit(payload) {
try {
const { data } = await axios.post('/api/enrolments/bulk', payload)
this.$refs.wizard.setResult({ success: true, message: `Enrolled ${data.count} learners.` })
} catch (e) {
this.$refs.wizard.setResult({ error: e.message })
}
},
},
}
</script>

Slot scope

Each #step-{id} slot receives:

FieldTypeDescription
next() => Promise<void>Run validation + advance to the next step.
back() => voidStep back. No validation.
jumpTo(stepId) => voidJump to any step. No validation.
submit() => Promise<void>Run validation + emit @submit.
currentStepObjectThe current step definition.
stepIndexnumberZero-based index of the current step.
totalStepsnumberTotal number of declared steps.
stepDataObjectShared cross-step data.
setStepData(partial: Object) => voidMerge partial into stepData.
isFirstbooleanTrue on the first step.
isLastbooleanTrue on the last step (Next → Submit label flip).

Props

PropTypeDefaultDescription
stepsArray<{id,label,optional?,icon?}>(required)Step declarations. Order is significant.
dialogTitleString'Wizard'Dialog header.
initialStepString''Step id to start on; defaults to the first step.
validate(stepId, stepData) => Promise<boolean|string>nullPer-step validator. Return true to advance, a string to show as an error banner + block navigation.
allowJumpBackBooleantrueAllow clicking a completed-step dot to jump back. Forward jumps via the progress indicator are never allowed.
defaultsObject{}Seed values for stepData.
cancelLabelString'Cancel'Cancel-button label.
backLabelString'Back'Back-button label.
nextLabelString'Next'Next-button label.
submitLabelString'Submit'Final-step submit-button label.
closeLabelString'Close'Close-button label (result phase).
successTextString'Done.'Default success-banner text when result.message is empty.

Events

EventPayloadDescription
submitstepDataUser reached the final step and confirmed.
step-change{ stepId, stepIndex, direction }After every step navigation (next, back, jump).
closeDialog was dismissed.

Public methods

MethodSignatureDescription
setResult(result)({ success?, message?, error? })Switch into the result phase + clear loading.

Slots

  • #step-{id} — body for each declared step (see scope table above).
  • #result-extra — extra content rendered below the result-phase banner. Scope: { result }.

See also