Form.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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. type="form"
  7. v-bind:config="formConfig"
  8. v-bind:form-class="localClass"
  9. v-bind:actions="false"
  10. v-bind:incomplete-message="false"
  11. v-bind:plugins="localFormKitPlugins"
  12. v-bind:sections-schema="formKitSectionsSchema"
  13. v-on:node="setFormNode"
  14. v-on:submit="onSubmit"
  15. >
  16. <slot name="before-fields" />
  17. <template v-if="!$slots.fields">
  18. <FormKitSchema
  19. v-bind:schema="staticSchema"
  20. v-bind:data="schemaData"
  21. v-bind:library="additionalComponentLibrary"
  22. />
  23. </template>
  24. <template v-else>
  25. <slot name="fields" />
  26. </template>
  27. <slot name="after-fields" />
  28. </FormKit>
  29. </template>
  30. <script setup lang="ts">
  31. import { FormKit, FormKitSchema } from '@formkit/vue'
  32. import FormLayout from '@common/components/form/FormLayout.vue'
  33. import {
  34. computed,
  35. ref,
  36. reactive,
  37. toRef,
  38. watch,
  39. markRaw,
  40. ConcreteComponent,
  41. nextTick,
  42. } from 'vue'
  43. import {
  44. type FormSchemaField,
  45. type FormSchemaLayout,
  46. type FormSchemaNode,
  47. type FormValues,
  48. type ReactiveFormSchemData,
  49. FormValidationVisibility,
  50. } from '@common/types/form'
  51. import type {
  52. FormKitGroupValue,
  53. FormKitPlugin,
  54. FormKitSchemaNode,
  55. FormKitSchemaCondition,
  56. FormKitNode,
  57. FormKitClasses,
  58. FormKitSchemaDOMNode,
  59. FormKitSchemaComponent,
  60. FormKitFrameworkContext,
  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. export 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.submit,
  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 formNodeContext = ref<FormKitFrameworkContext | undefined>(undefined)
  104. const setFormNode = (node: FormKitNode) => {
  105. formNode = node
  106. formNodeContext.value = formNode.context
  107. // TODO: maybe we should also emit the node one level above to have the node available without a own getNode-call...
  108. }
  109. // Use the node context value, instead of the v-model, because of performance reason.
  110. const values = computed<FormValues>(() => {
  111. if (!formNodeContext.value) {
  112. return {}
  113. }
  114. return formNodeContext.value.value
  115. })
  116. const onSubmit = (values: FormKitGroupValue) => {
  117. const emitValues = {
  118. ...values,
  119. formId,
  120. }
  121. emit('submit', emitValues)
  122. }
  123. const changedValuePlugin = (node: FormKitNode) => {
  124. node.on('input', ({ payload: value, origin: node }) => {
  125. emit('changed', value, node.name)
  126. })
  127. }
  128. const localFormKitPlugins = computed(() => {
  129. return [changedValuePlugin, ...(props.formKitPlugins || [])]
  130. })
  131. const formConfig = computed(() => {
  132. return {
  133. validationVisibility: props.validationVisibility,
  134. }
  135. })
  136. // Define the additional component library for the used components which are not form fields.
  137. const additionalComponentLibrary = {
  138. FormLayout: markRaw(FormLayout) as ConcreteComponent,
  139. }
  140. // Define the static schema, which will be filled with the real fields from the `schemaData`.
  141. const staticSchema: FormKitSchemaNode[] = []
  142. const schemaData = reactive<ReactiveFormSchemData>({
  143. fields: {},
  144. })
  145. const updateSchemaDataField = (field: FormSchemaField) => {
  146. const newField = {
  147. ...field,
  148. show: field.show ?? true,
  149. }
  150. if (schemaData.fields[field.name]) {
  151. schemaData.fields[field.name] = Object.assign(
  152. schemaData.fields[field.name],
  153. newField,
  154. )
  155. } else {
  156. schemaData.fields[field.name] = newField
  157. }
  158. }
  159. const buildStaticSchema = (schema: FormSchemaNode[]) => {
  160. const buildFormKitField = (
  161. field: FormSchemaField,
  162. ): FormKitSchemaComponent => {
  163. return {
  164. $cmp: 'FormKit',
  165. if: `$fields.${field.name}.show`,
  166. bind: `$fields.${field.name}`,
  167. props: {
  168. type: field.type,
  169. key: field.name,
  170. formId,
  171. value: props.initialValues?.[field.name] ?? field.value,
  172. },
  173. }
  174. }
  175. const getLayoutType = (
  176. layoutItem: FormSchemaLayout,
  177. ): FormKitSchemaDOMNode | FormKitSchemaComponent => {
  178. if ('component' in layoutItem) {
  179. return {
  180. $cmp: layoutItem.component,
  181. props: layoutItem.props,
  182. }
  183. }
  184. return {
  185. $el: layoutItem.element,
  186. attrs: layoutItem.attrs,
  187. }
  188. }
  189. schema.forEach((node) => {
  190. if ((node as FormSchemaLayout).isLayout) {
  191. const layoutItem = node as FormSchemaLayout
  192. if (typeof layoutItem.children === 'string') {
  193. staticSchema.push({
  194. ...getLayoutType(layoutItem),
  195. children: layoutItem.children,
  196. })
  197. } else {
  198. const childrens = layoutItem.children.map((childNode) => {
  199. if (typeof childNode === 'string') {
  200. return childNode
  201. }
  202. if ((childNode as FormSchemaLayout).isLayout) {
  203. return {
  204. ...getLayoutType(childNode as FormSchemaLayout),
  205. children: childNode.children as
  206. | string
  207. | FormKitSchemaNode[]
  208. | FormKitSchemaCondition,
  209. }
  210. }
  211. updateSchemaDataField(childNode as FormSchemaField)
  212. return buildFormKitField(childNode as FormSchemaField)
  213. })
  214. staticSchema.push({
  215. ...getLayoutType(layoutItem),
  216. children: childrens,
  217. })
  218. }
  219. } else {
  220. const field = node as FormSchemaField
  221. // TODO: maybe we can also add better support for Group and List fields, when this bug is fixed:
  222. // https://github.com/formkit/formkit/issues/91
  223. staticSchema.push(buildFormKitField(field))
  224. updateSchemaDataField(field)
  225. }
  226. })
  227. }
  228. const coreWorkflowChanges = ref<Record<string, FormSchemaField>>({})
  229. // TODO: coreWorkflowChanges should be filled from the server call...
  230. const localChangeFields = computed(() => {
  231. if (props.formName) return coreWorkflowChanges.value
  232. return props.changeFields
  233. })
  234. // If something changed in the change fields, we need to update the current schemaData
  235. watch(
  236. localChangeFields,
  237. (newChangeFields) => {
  238. Object.keys(newChangeFields).forEach((fieldName) => {
  239. const field = {
  240. ...newChangeFields[fieldName],
  241. name: fieldName,
  242. }
  243. updateSchemaDataField(field)
  244. nextTick(() => {
  245. if (field.value !== values.value[fieldName]) {
  246. formNode.at(fieldName)?.input(field.value)
  247. }
  248. })
  249. })
  250. },
  251. { deep: true },
  252. )
  253. // TODO: maybe we should react on schema changes and rebuild the static schema with a new form-id and re-rendering of
  254. // the complete form (= use the formId as the key for the whole form to trigger the re-rendering of the component...)
  255. if (props.formName) {
  256. // TODO: call the GraphQL-Query to fetch the schema.
  257. setTimeout(() => {
  258. buildStaticSchema(toRef(props, 'schema').value)
  259. }, 4000)
  260. } else if (props.schema) {
  261. // localSchema.value = toRef(props, 'schema').value
  262. buildStaticSchema(toRef(props, 'schema').value)
  263. }
  264. </script>