useMultiStepForm.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { createMessage, getNode } from '@formkit/core'
  3. import { computed, toRef, ref, reactive, watch } from 'vue'
  4. import type { FormStep } from './types.ts'
  5. import type { FormKitNode } from '@formkit/core'
  6. import type { ComputedRef, Ref } from 'vue'
  7. interface InternalMultiFormSteps {
  8. label: string
  9. order: number
  10. valid: Ref<boolean>
  11. blockingCount: number
  12. errorCount: number
  13. }
  14. export const useMultiStepForm = (
  15. formNode: ComputedRef<FormKitNode | undefined>,
  16. ) => {
  17. const activeStep = ref('')
  18. const internalSteps = reactive<Record<string, InternalMultiFormSteps>>({})
  19. const visitedSteps = ref<string[]>([])
  20. const stepNames = computed(() => Object.keys(internalSteps))
  21. const lastStepName = computed(
  22. () => stepNames.value[stepNames.value.length - 1],
  23. )
  24. // Watch the active steps to track the visited steps.
  25. watch(activeStep, (newStep, oldStep) => {
  26. if (oldStep && !visitedSteps.value.includes(oldStep)) {
  27. visitedSteps.value.push(oldStep)
  28. }
  29. // Trigger showing validation on fields within all visited steps, otherwise it would only visible
  30. // after clicking on the "real" submit button.
  31. visitedSteps.value.forEach((step) => {
  32. const node = getNode(step)
  33. if (!node) return
  34. node.walk((fieldNode) => {
  35. fieldNode.store.set(
  36. createMessage({
  37. key: 'submitted',
  38. value: true,
  39. visible: false,
  40. }),
  41. )
  42. })
  43. })
  44. formNode.value?.emit('autofocus')
  45. })
  46. const setMultiStep = (step?: string) => {
  47. // Go to next step, when no specific step is given.
  48. if (!step) {
  49. const currentIndex = stepNames.value.indexOf(activeStep.value)
  50. activeStep.value = stepNames.value[currentIndex + 1]
  51. } else {
  52. activeStep.value = step
  53. }
  54. }
  55. const multiStepPlugin = (node: FormKitNode) => {
  56. if (node.props.type === 'group') {
  57. internalSteps[node.name] = internalSteps[node.name] || {}
  58. node.on('created', () => {
  59. if (!node.context) return
  60. internalSteps[node.name].valid = toRef(node.context.state, 'valid')
  61. internalSteps[node.name].label =
  62. Object.keys(internalSteps).length.toString()
  63. internalSteps[node.name].order = Object.keys(internalSteps).length
  64. })
  65. // Listen for changes in error count, which a normally errors from the backend after submitting.
  66. node.on('count:errors', ({ payload: count }) => {
  67. internalSteps[node.name].errorCount = count
  68. })
  69. // Listen for changes in count of blocking validations messages.
  70. node.on('count:blocking', ({ payload: count }) => {
  71. internalSteps[node.name].blockingCount = count
  72. })
  73. // The first step should be the default one.
  74. if (activeStep.value === '') {
  75. activeStep.value = node.name
  76. }
  77. }
  78. return false
  79. }
  80. const allSteps = computed<Record<string, FormStep>>(() => {
  81. const mappedSteps: Record<string, FormStep> = {}
  82. stepNames.value.forEach((stepName) => {
  83. const alreadyVisited = visitedSteps.value.includes(stepName)
  84. mappedSteps[stepName] = {
  85. label: internalSteps[stepName].label,
  86. order: internalSteps[stepName].order,
  87. errorCount:
  88. internalSteps[stepName].blockingCount +
  89. internalSteps[stepName].errorCount,
  90. valid:
  91. internalSteps[stepName].valid &&
  92. internalSteps[stepName].errorCount === 0,
  93. disabled: !alreadyVisited || activeStep.value === stepName,
  94. completed: alreadyVisited,
  95. }
  96. })
  97. return mappedSteps
  98. })
  99. return {
  100. multiStepPlugin,
  101. setMultiStep,
  102. allSteps,
  103. stepNames,
  104. lastStepName,
  105. activeStep,
  106. visitedSteps,
  107. }
  108. }