Form.vue 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <!-- Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ -->
  2. <template>
  3. <FormKit
  4. v-if="Object.keys(schemaData.fields).length > 0 || $slots.fields"
  5. v-bind:id="formId"
  6. v-model="values"
  7. type="form"
  8. v-bind:config="formConfig"
  9. v-bind:form-class="localClass"
  10. v-bind:actions="false"
  11. v-bind:incomplete-message="false"
  12. v-bind:plugins="localFormKitPlugins"
  13. v-bind:sections-schema="formKitSectionsSchema"
  14. v-on:node="setFormNode"
  15. v-on:submit="onSubmit"
  16. >
  17. <slot name="before-fields" />
  18. <template v-if="!$slots.fields">
  19. <FormKitSchema
  20. v-bind:schema="staticSchema"
  21. v-bind:data="schemaData"
  22. v-bind:library="additionalComponentLibrary"
  23. />
  24. </template>
  25. <template v-else>
  26. <slot name="fields" />
  27. </template>
  28. <slot name="after-fields" />
  29. </FormKit>
  30. </template>
  31. <script setup lang="ts">
  32. import { FormKit, FormKitSchema } from '@formkit/vue'
  33. import FormLayout from '@common/components/form/FormLayout.vue'
  34. import {
  35. computed,
  36. ref,
  37. reactive,
  38. toRef,
  39. watch,
  40. markRaw,
  41. ConcreteComponent,
  42. nextTick,
  43. } from 'vue'
  44. import {
  45. type FormSchemaField,
  46. type FormSchemaLayout,
  47. type FormSchemaNode,
  48. type FormValues,
  49. type ReactiveFormSchemData,
  50. FormValidationVisibility,
  51. } from '@common/types/form'
  52. import type {
  53. FormKitGroupValue,
  54. FormKitPlugin,
  55. FormKitSchemaNode,
  56. FormKitSchemaCondition,
  57. FormKitNode,
  58. FormKitClasses,
  59. FormKitSchemaDOMNode,
  60. FormKitSchemaComponent,
  61. } from '@formkit/core'
  62. import getUuid from '@common/utils/getUuid'
  63. // TODO:
  64. // - Do we need a loading animation?
  65. // - Maybe some default buttons inside the components with loading cycle on submit?
  66. // - Disabled form on submit? (i think it's the default of FormKit)
  67. // - Reset/Clear form handling?
  68. // - ...
  69. // TODO:
  70. interface Props {
  71. schema?: FormSchemaNode[]
  72. formName?: string
  73. changeFields?: Record<string, FormSchemaField>
  74. formKitPlugins?: FormKitPlugin[]
  75. formKitSectionsSchema?: Record<
  76. string,
  77. Partial<FormKitSchemaNode> | FormKitSchemaCondition
  78. >
  79. class?: FormKitClasses | string | Record<string, boolean>
  80. // Can be used to define initial values on frontend side and fetched schema from the server.
  81. initialValues?: Partial<FormValues>
  82. queryParams?: Record<string, unknown>
  83. validationVisibility?: FormValidationVisibility
  84. }
  85. const formId = `form-${getUuid()}`
  86. const props = withDefaults(defineProps<Props>(), {
  87. schema: () => {
  88. return []
  89. },
  90. changeFields: () => {
  91. return {}
  92. },
  93. staticSchema: false,
  94. validationVisibility: FormValidationVisibility.blur,
  95. })
  96. // Rename prop 'class' for usage in the template, because of reserved word
  97. const localClass = toRef(props, 'class')
  98. const emit = defineEmits<{
  99. (e: 'submit', values: FormKitGroupValue): void
  100. (e: 'changed', fieldName: string, newValue: unknown): void
  101. }>()
  102. let formNode: FormKitNode
  103. const setFormNode = (node: FormKitNode) => {
  104. formNode = node
  105. // TODO: maybe we should also emit the node one level above to have the node available without a own getNode-call...
  106. }
  107. const onSubmit = (values: FormKitGroupValue) => {
  108. const emitValues = {
  109. ...values,
  110. formId,
  111. }
  112. emit('submit', emitValues)
  113. }
  114. const changedValuePlugin = (node: FormKitNode) => {
  115. node.on('input', ({ payload: value, origin: node }) => {
  116. emit('changed', value, node.name)
  117. })
  118. }
  119. const localFormKitPlugins = computed(() => {
  120. return [changedValuePlugin, ...(props.formKitPlugins || [])]
  121. })
  122. const formConfig = computed(() => {
  123. return {
  124. validationVisibility: props.validationVisibility,
  125. }
  126. })
  127. // Define the additional component library for the used components which are not form fields.
  128. const additionalComponentLibrary = {
  129. FormLayout: markRaw(FormLayout) as ConcreteComponent,
  130. }
  131. const values = ref<FormValues>({})
  132. // Define the static schema, which will be filled with the real fields from the `schemaData`.
  133. const staticSchema: FormKitSchemaNode[] = []
  134. const schemaData = reactive<ReactiveFormSchemData>({
  135. fields: {},
  136. })
  137. const updateSchemaDataField = (field: FormSchemaField) => {
  138. const newField = {
  139. ...field,
  140. show: field.show ?? true,
  141. }
  142. if (schemaData.fields[field.name]) {
  143. schemaData.fields[field.name] = Object.assign(
  144. schemaData.fields[field.name],
  145. newField,
  146. )
  147. } else {
  148. schemaData.fields[field.name] = newField
  149. }
  150. }
  151. const buildStaticSchema = (schema: FormSchemaNode[]) => {
  152. const buildFormKitField = (
  153. field: FormSchemaField,
  154. ): FormKitSchemaComponent => {
  155. return {
  156. $cmp: 'FormKit',
  157. if: `$fields.${field.name}.show`,
  158. bind: `$fields.${field.name}`,
  159. props: {
  160. type: field.type,
  161. key: field.name,
  162. formId,
  163. value: props.initialValues?.[field.name] ?? field.value,
  164. },
  165. }
  166. }
  167. const getLayoutType = (
  168. layoutItem: FormSchemaLayout,
  169. ): FormKitSchemaDOMNode | FormKitSchemaComponent => {
  170. if ('component' in layoutItem) {
  171. return {
  172. $cmp: layoutItem.component,
  173. props: layoutItem.props,
  174. }
  175. }
  176. return {
  177. $el: layoutItem.element,
  178. attrs: layoutItem.attrs,
  179. }
  180. }
  181. schema.forEach((node) => {
  182. if ((node as FormSchemaLayout).isLayout) {
  183. const layoutItem = node as FormSchemaLayout
  184. if (typeof layoutItem.children === 'string') {
  185. staticSchema.push({
  186. ...getLayoutType(layoutItem),
  187. children: layoutItem.children,
  188. })
  189. } else {
  190. const childrens = layoutItem.children.map((childNode) => {
  191. if (typeof childNode === 'string') {
  192. return childNode
  193. }
  194. if ((childNode as FormSchemaLayout).isLayout) {
  195. return {
  196. ...getLayoutType(childNode as FormSchemaLayout),
  197. children: childNode.children as
  198. | string
  199. | FormKitSchemaNode[]
  200. | FormKitSchemaCondition,
  201. }
  202. }
  203. updateSchemaDataField(childNode as FormSchemaField)
  204. return buildFormKitField(childNode as FormSchemaField)
  205. })
  206. staticSchema.push({
  207. ...getLayoutType(layoutItem),
  208. children: childrens,
  209. })
  210. }
  211. } else {
  212. const field = node as FormSchemaField
  213. // TODO: maybe we can also add better support for Group and List fields, when this bug is fixed:
  214. // https://github.com/formkit/formkit/issues/91
  215. staticSchema.push(buildFormKitField(field))
  216. updateSchemaDataField(field)
  217. }
  218. })
  219. }
  220. const coreWorkflowChanges = ref<Record<string, FormSchemaField>>({})
  221. // TODO: coreWorkflowChanges should be filled from the server call...
  222. const localChangeFields = computed(() => {
  223. if (props.formName) return coreWorkflowChanges.value
  224. return props.changeFields
  225. })
  226. // If something changed in the change fields, we need to update the current schemaData
  227. watch(localChangeFields, (newChangeFields) => {
  228. Object.keys(newChangeFields).forEach((fieldName) => {
  229. const field = {
  230. ...newChangeFields[fieldName],
  231. name: fieldName,
  232. }
  233. updateSchemaDataField(field)
  234. nextTick(() => {
  235. if (field.value !== values.value[fieldName]) {
  236. formNode.at(fieldName)?.input(field.value)
  237. }
  238. })
  239. })
  240. })
  241. // TODO: maybe we should react on schema changes and rebuild the static schema with a new form-id and re-rendering of
  242. // the complete form (= use the formId as the key for the whole form to trigger the re-rendering of the component...)
  243. if (props.formName) {
  244. // TODO: call the GraphQL-Query to fetch the schema.
  245. setTimeout(() => {
  246. buildStaticSchema(toRef(props, 'schema').value)
  247. }, 4000)
  248. } else if (props.schema) {
  249. // localSchema.value = toRef(props, 'schema').value
  250. buildStaticSchema(toRef(props, 'schema').value)
  251. }
  252. </script>