composable.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. // Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. import { createMessage, getNode } from '@formkit/core'
  3. import type { FormKitNode } from '@formkit/core'
  4. import { computed, shallowRef, toRef, ref, reactive, watch } from 'vue'
  5. import type { ComputedRef, Ref, ShallowRef } from 'vue'
  6. import type { CommonStepperStep } from '@mobile/components/CommonStepper'
  7. import type { ObjectLike } from '@shared/types/utils'
  8. import type { FormRef, FormResetOptions, FormValues } from './types'
  9. export const useForm = () => {
  10. const form: ShallowRef<FormRef | undefined> = shallowRef()
  11. const node = computed(() => form.value?.formNode)
  12. const context = computed(() => node.value?.context)
  13. const values = computed(() => context.value?.value)
  14. const state = computed(() => context.value?.state)
  15. const isValid = computed(() => !!state.value?.valid)
  16. const isDirty = computed(() => !!state.value?.dirty)
  17. const isComplete = computed(() => !!state.value?.complete)
  18. const isSubmitted = computed(() => !!state.value?.submitted)
  19. const isDisabled = computed(() => {
  20. return !!context.value?.disabled || !!state.value?.formUpdaterProcessing
  21. })
  22. /**
  23. * User can submit form, if it is:
  24. * - not disabled
  25. * - has dirty values
  26. * After submit, the values should be reset to new values, so "dirty" state can update.
  27. * It is done automaticaly, if async `@submit` event is used. Otherwise, `formReset` should be used.
  28. */
  29. const canSubmit = computed(() => {
  30. if (isDisabled.value) return false
  31. return isDirty.value
  32. })
  33. const formReset = (
  34. values?: FormValues,
  35. object?: ObjectLike,
  36. options?: FormResetOptions,
  37. ) => {
  38. form.value?.resetForm(values, object, options)
  39. }
  40. const formGroupReset = (
  41. groupNode: FormKitNode,
  42. values?: FormValues,
  43. object?: ObjectLike,
  44. options?: FormResetOptions,
  45. ) => {
  46. form.value?.resetForm(values, object, options, groupNode)
  47. }
  48. const formSubmit = () => {
  49. node.value?.submit()
  50. }
  51. const waitForFormSettled = () => {
  52. return new Promise<FormKitNode>((resolve) => {
  53. const interval = setInterval(() => {
  54. if (!node.value) return
  55. const formNode = node.value
  56. clearInterval(interval)
  57. formNode.settled.then(() => resolve(formNode))
  58. })
  59. })
  60. }
  61. return {
  62. form,
  63. node,
  64. context,
  65. values,
  66. state,
  67. isValid,
  68. isDirty,
  69. isComplete,
  70. isSubmitted,
  71. isDisabled,
  72. canSubmit,
  73. formReset,
  74. formGroupReset,
  75. formSubmit,
  76. waitForFormSettled,
  77. }
  78. }
  79. interface InternalMultiFormSteps {
  80. label: string
  81. order: number
  82. valid: Ref<boolean>
  83. blockingCount: number
  84. errorCount: number
  85. }
  86. export const useMultiStepForm = (
  87. formNode: ComputedRef<FormKitNode | undefined>,
  88. ) => {
  89. const activeStep = ref('')
  90. const internalSteps = reactive<Record<string, InternalMultiFormSteps>>({})
  91. const visitedSteps = ref<string[]>([])
  92. const stepNames = computed(() => Object.keys(internalSteps))
  93. const lastStepName = computed(
  94. () => stepNames.value[stepNames.value.length - 1],
  95. )
  96. // Watch the active steps to track the visited steps.
  97. watch(activeStep, (newStep, oldStep) => {
  98. if (oldStep && !visitedSteps.value.includes(oldStep)) {
  99. visitedSteps.value.push(oldStep)
  100. }
  101. // Trigger showing validation on fields within all visited steps, otherwise it would only visible
  102. // after clicking on the "real" submit button.
  103. visitedSteps.value.forEach((step) => {
  104. const node = getNode(step)
  105. if (!node) return
  106. node.walk((fieldNode) => {
  107. fieldNode.store.set(
  108. createMessage({
  109. key: 'submitted',
  110. value: true,
  111. visible: false,
  112. }),
  113. )
  114. })
  115. })
  116. formNode.value?.emit('autofocus')
  117. })
  118. const setMultiStep = (step?: string) => {
  119. // Go to next step, when no specific step is given.
  120. if (!step) {
  121. const currentIndex = stepNames.value.indexOf(activeStep.value)
  122. activeStep.value = stepNames.value[currentIndex + 1]
  123. } else {
  124. activeStep.value = step
  125. }
  126. }
  127. const multiStepPlugin = (node: FormKitNode) => {
  128. if (node.props.type === 'group') {
  129. internalSteps[node.name] = internalSteps[node.name] || {}
  130. node.on('created', () => {
  131. if (!node.context) return
  132. internalSteps[node.name].valid = toRef(node.context.state, 'valid')
  133. internalSteps[node.name].label =
  134. Object.keys(internalSteps).length.toString()
  135. internalSteps[node.name].order = Object.keys(internalSteps).length
  136. })
  137. // Listen for changes in error count, which a normally errors from the backend after submitting.
  138. node.on('count:errors', ({ payload: count }) => {
  139. internalSteps[node.name].errorCount = count
  140. })
  141. // Listen for changes in count of blocking validations messages.
  142. node.on('count:blocking', ({ payload: count }) => {
  143. internalSteps[node.name].blockingCount = count
  144. })
  145. // The first step should be the default one.
  146. if (activeStep.value === '') {
  147. activeStep.value = node.name
  148. }
  149. }
  150. return false
  151. }
  152. const allSteps = computed<Record<string, CommonStepperStep>>(() => {
  153. const mappedSteps: Record<string, CommonStepperStep> = {}
  154. stepNames.value.forEach((stepName) => {
  155. const alreadyVisisted = visitedSteps.value.includes(stepName)
  156. mappedSteps[stepName] = {
  157. label: internalSteps[stepName].label,
  158. order: internalSteps[stepName].order,
  159. errorCount:
  160. internalSteps[stepName].blockingCount +
  161. internalSteps[stepName].errorCount,
  162. valid:
  163. internalSteps[stepName].valid &&
  164. internalSteps[stepName].errorCount === 0,
  165. disabled: !alreadyVisisted || activeStep.value === stepName,
  166. completed: alreadyVisisted,
  167. }
  168. })
  169. return mappedSteps
  170. })
  171. return {
  172. multiStepPlugin,
  173. setMultiStep,
  174. allSteps,
  175. stepNames,
  176. lastStepName,
  177. activeStep,
  178. visitedSteps,
  179. }
  180. }