diff --git a/packages/entities/entities-plugins/src/components/free-form/shared/EnhancedInput.vue b/packages/entities/entities-plugins/src/components/free-form/shared/EnhancedInput.vue
index 31fd8598a2..877296c502 100644
--- a/packages/entities/entities-plugins/src/components/free-form/shared/EnhancedInput.vue
+++ b/packages/entities/entities-plugins/src/components/free-form/shared/EnhancedInput.vue
@@ -47,20 +47,20 @@ watch(() => props.modelValue, (newValue) => {
innerValue.value = newValue
})
-const { formConfig } = useFormShared()
+const { config } = useFormShared()
const handleInput = (value: string) => {
if (value === innerValue.value) return
innerValue.value = value
- if (!formConfig.updateOnChange) {
+ if (!config.value.updateOnChange) {
emit('update:modelValue', value)
}
}
const handleChange = () => {
- if (formConfig.updateOnChange) {
+ if (config.value.updateOnChange) {
emit('update:modelValue', innerValue.value!)
}
}
diff --git a/packages/entities/entities-plugins/src/components/free-form/shared/FieldRenderer.vue b/packages/entities/entities-plugins/src/components/free-form/shared/FieldRenderer.vue
index 97891c81d4..f9407fc5f3 100644
--- a/packages/entities/entities-plugins/src/components/free-form/shared/FieldRenderer.vue
+++ b/packages/entities/entities-plugins/src/components/free-form/shared/FieldRenderer.vue
@@ -3,9 +3,9 @@
diff --git a/packages/entities/entities-plugins/src/components/free-form/shared/Form.vue b/packages/entities/entities-plugins/src/components/free-form/shared/Form.vue
index 92b953f7f4..e42f581e91 100644
--- a/packages/entities/entities-plugins/src/components/free-form/shared/Form.vue
+++ b/packages/entities/entities-plugins/src/components/free-form/shared/Form.vue
@@ -24,14 +24,11 @@ export type Props = Record> = {
diff --git a/packages/entities/entities-plugins/src/components/free-form/shared/composables.ts b/packages/entities/entities-plugins/src/components/free-form/shared/composables.ts
index fd008e6bb8..ee91e06819 100644
--- a/packages/entities/entities-plugins/src/components/free-form/shared/composables.ts
+++ b/packages/entities/entities-plugins/src/components/free-form/shared/composables.ts
@@ -1,20 +1,97 @@
-import { computed, inject, provide, ref, toRef, toValue, useAttrs, useSlots, watch, type ComputedRef, type MaybeRefOrGetter, type Slot } from 'vue'
+import { computed, inject, provide, reactive, ref, toRef, toValue, useAttrs, useSlots, watch, type ComputedRef, type InjectionKey, type MaybeRefOrGetter, type Slot } from 'vue'
import { marked } from 'marked'
import * as utils from './utils'
import type { LabelAttributes, SelectItem } from '@kong/kongponents'
import type { ArrayFieldSchema, ArrayLikeFieldSchema, FormSchema, RecordFieldSchema, UnionFieldSchema } from '../../../types/plugins/form-schema'
-import { get, set, uniqueId } from 'lodash-es'
+import { cloneDeep, get, isFunction, omit, set, uniqueId } from 'lodash-es'
import type { MatchMap } from './FieldRenderer.vue'
import type { FormConfig, ResetLabelPathRule } from './types'
import { upperFirst } from 'lodash-es'
+import { createInjectionState } from '@vueuse/core'
+
+export const [provideFormShared, useOptionalFormShared] = createInjectionState(
+ function createFormShared = Record>(
+ schema: FormSchema | UnionFieldSchema,
+ propsData?: ComputedRef,
+ propsConfig?: FormConfig,
+ onChange?: (newData: T) => void,
+ ) {
+ const schemaHelpers = useSchemaHelpers(schema)
+ const fieldRendererRegistry: MatchMap = new Map()
+
+ const innerData = reactive({} as T)
+ const config = toRef(() => propsConfig ?? {})
+
+ let innerDataInit = false
+
+ function resetFormData(newData: T) {
+ Object.keys(innerData).forEach((key) => {
+ delete (innerData as any)[key]
+ })
+ Object.assign(innerData, newData)
+ }
+
+ /**
+ * Initialize the inner data based on the provided props data or schema defaults
+ */
+ function initInnerData(propsData: T | undefined) {
+ let dataValue: T
+
+ if (!propsData || !hasValue(toValue(propsData))) {
+ dataValue = schemaHelpers.getDefault()
+ } else {
+ dataValue = cloneDeep(toValue(propsData))
+ }
+
+ if (isFunction(config.value.prepareFormData)) {
+ resetFormData(config.value.prepareFormData(dataValue))
+ } else {
+ resetFormData(dataValue)
+ }
+
+ innerDataInit = true
+ }
+
+ function hasValue(data: T | undefined): boolean {
+ if (isFunction(config.value.hasValue)) {
+ return config.value.hasValue(data)
+ }
+ return !!data
+ }
-export const DATA_INJECTION_KEY = Symbol('free-form-data')
-export const SCHEMA_INJECTION_KEY = Symbol('free-form-schema')
-export const FIELD_PATH_KEY = Symbol('free-form-field-path')
-export const FIELD_RENDERER_SLOTS = Symbol('free-form-field-renderer-slots')
-export const FIELD_RENDERER_MATCHERS_MAP = Symbol('free-form-field-renderer-matchers-map')
-export const FORM_CONFIG = Symbol('free-form-config')
-export const FIELD_RESET_LABEL_PATH_SETTING = Symbol('free-form-field-reset-label-path-setting')
+ // Sync the inner data when the props data changes
+ watch(() => propsData?.value, newData => {
+ initInnerData(newData)
+ }, { deep: true, immediate: true })
+
+ // Emit changes when the inner data changes
+ watch(innerData, (newVal) => {
+ if (!innerDataInit) return
+ onChange?.(toValue(newVal))
+ }, { deep: true, immediate: true })
+
+ // Init form level field renderer slots
+ const slots = useSlots()
+ provide(FIELD_RENDERER_SLOTS, omit(slots, 'default', FIELD_RENDERERS))
+
+ return {
+ formData: innerData,
+ setFormData: resetFormData,
+ schema,
+ ...schemaHelpers,
+ config,
+ fieldRendererRegistry,
+ addEventListener,
+ }
+ },
+)
+
+export const FIELD_PATH_KEY = Symbol('free-form-field-path') as InjectionKey>
+export const FIELD_RENDERER_SLOTS = Symbol('free-form-field-renderer-slots') as InjectionKey>>
+export const FIELD_RESET_LABEL_PATH_SETTING = Symbol('free-form-field-reset-label-path-setting') as InjectionKey>
export const FIELD_RENDERERS = 'free-form-field-renderers-slot' as const
@@ -220,6 +297,7 @@ export function useSchemaHelpers(schema: MaybeRefOrGetter() {
- const formData = inject(DATA_INJECTION_KEY)
- const schemaHelpers = inject>(SCHEMA_INJECTION_KEY)
- const formConfig = inject(FORM_CONFIG, {})
-
- if (!formData) {
- throw new Error('useFormShared() called without form data provider.')
+export function useFormShared = Record>() {
+ const store = useOptionalFormShared()
+ if (!store) {
+ throw new Error('useFormShared() called without provider.')
}
-
- if (!schemaHelpers) {
- throw new Error('useFormShared() called without schema provider.')
+ // `createInjectionState` does not support generics, so we need to cast here
+ return store as ReturnType & {
+ formData: T
+ config: ComputedRef>
+ onChange?: (newData: T) => void
}
-
- return { formData, formConfig, ...schemaHelpers }
}
export const useFieldPath = (name: MaybeRefOrGetter) => {
- const inheritedPath = inject>(FIELD_PATH_KEY, computed(() => ''))
+ const inheritedPath = inject(FIELD_PATH_KEY, computed(() => ''))
const fieldPath = computed(() => {
const nameValue = toValue(name)
@@ -269,11 +344,9 @@ export const useFieldPath = (name: MaybeRefOrGetter) => {
}
export const useFieldRenderer = (path: MaybeRefOrGetter) => {
- const { getSchema } = useFormShared()
+ const { getSchema, fieldRendererRegistry } = useFormShared()
const { default: defaultSlot, ...slots } = useSlots()
- const inheritSlots = inject>>(FIELD_RENDERER_SLOTS)
-
- const matchMap = inject(FIELD_RENDERER_MATCHERS_MAP)!
+ const inheritSlots = inject(FIELD_RENDERER_SLOTS)
const mergedSlots = computed(() => {
const inheritSlotsValue = toValue(inheritSlots)
@@ -297,7 +370,7 @@ export const useFieldRenderer = (path: MaybeRefOrGetter) => {
if (matchedByPath) return matchedByPath
// todo(zehao): priority
- for (const [matcher, slot] of matchMap) {
+ for (const [matcher, slot] of fieldRendererRegistry) {
if (matcher({ path: pathValue, schema: getSchema(pathValue)! })) {
return slot
}
@@ -400,9 +473,8 @@ export function useFieldLabel(
) {
const pathValue = toValue(fieldPath)
const fieldName = utils.getName(pathValue)
- const { formConfig } = useFormShared()
+ const { config, getSchema } = useFormShared()
const parentLabelPath = useLabelPath(fieldName, resetLabelPathRule)
- const { getSchema } = useFormShared()
const ancestors = useFieldAncestors(fieldPath)
return computed(() => {
@@ -418,7 +490,7 @@ export function useFieldLabel(
? '' // hide the label when it is a child of Array
: defaultLabelFormatter(realPath)
- return formConfig.transformLabel ? formConfig.transformLabel(res, pathValue) : res
+ return config.value.transformLabel ? config.value.transformLabel(res, pathValue) : res
})
}