Ga naar hoofdinhoud

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.
nextDisabledBooleanfalseDisable Next and Submit, whatever validate would say. next() and submit() from a slot scope do nothing either. CnSetupWizard sets it while a required app is missing.
cancellableBooleantrueWhether the dialog can be dismissed (Cancel button, ESC, backdrop click) before reaching the result phase. false hides Cancel and disables all other close affordances — use for a step that must be completed, not merely skipped. The result-phase Close button is always available.

Events​

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

Closing and reopening​

The dialog owns its own open state and closes itself for good. Handle @close by unmounting it — v-if="show" with @close="show = false" — and let a fresh mount reopen it. There is no open prop and no public reopen method: a still-mounted instance that has closed stays closed.

This is deliberate. NcDialog does not self-manage open: on any close it clears its internal showModal flag and then sets it back to true about 300 ms later (bookkeeping for the next open). Since it renders on open && showModal, a dialog that leaves open at its true default silently reopens itself after every Cancel / ESC / backdrop click. CnWizardDialog therefore drives open explicitly and pins it false on close, which also means it cannot be revived in place.

Public methods​

MethodSignatureDescription
setResult(result)({ success?, message?, error? })Switch into the terminal result phase + clear loading (Close only).
setError(message)(string)Recoverable failure: clear loading and show message above the still-editable step so the user can fix and resubmit — without entering the result phase. Prefer this over setResult({ error }) when the submit can be retried (e.g. a taken slug).

The step indicator renders as numbered circles joined by connectors, with a checkmark on completed steps and the active step highlighted in the primary colour.

Slots​

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

Board look​

Set look: "board" in the manifest (or pass look="board" to the dialog) and the dialog draws the board dialog of the screens. An app that sets nothing renders exactly as before.

PropDefaultWhat it does
lookfollows the appboard or nextcloud. The prop wins over the app.
widthper componentconfirm (560), form (640) or wizard (720). No other width is reachable.
eyebrowemptyA context line above the title, uppercase 13px. Not part of the accessible name.
subtitleemptyA sentence under the title.

Widths are capped at the viewport minus 32px. Theme hooks: --cn-dialog-danger (fill of a destructive primary, default --color-error), --cn-dialog-eyebrow-color and --cn-dialog-eyebrow-transform.

The stepper is an ordered list in both looks, with aria-current="step" on the current step; it is no longer a tablist. A finished step is a button only when allowJumpBack is true (e2e tests that selected role="tab" inside a wizard need the new selector).

In the board look the stepper draws 28px circles with the label beside each: a green check on a finished step, a filled primary circle on the current one and an outlined circle on the rest, joined by a 2px connector. The eyebrow reads like New newsletter, step 2 of 5 and follows the step: eyebrowContext sets the context (default the dialog title) and stepEyebrow translates the sentence with {context}, {step} and {total}.

The footer reads Cancel, Back (chevron left) and the primary: Next with a chevron right, or on the last step submitLabel with submitIcon (default a check). CnSetupWizard passes these through.

See also​