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 }) }