更新最新代码
This commit is contained in:
189
packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts
Normal file
189
packages/@core/ui-kit/form-ui/__tests__/form-api.test.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { FormApi } from '../src/form-api';
|
||||
|
||||
describe('formApi', () => {
|
||||
let formApi: FormApi;
|
||||
|
||||
beforeEach(() => {
|
||||
formApi = new FormApi();
|
||||
});
|
||||
|
||||
it('should initialize with default state', () => {
|
||||
expect(formApi.state).toEqual(
|
||||
expect.objectContaining({
|
||||
actionWrapperClass: '',
|
||||
collapsed: false,
|
||||
collapsedRows: 1,
|
||||
commonConfig: {},
|
||||
handleReset: undefined,
|
||||
handleSubmit: undefined,
|
||||
layout: 'horizontal',
|
||||
resetButtonOptions: {},
|
||||
schema: [],
|
||||
showCollapseButton: false,
|
||||
showDefaultActions: true,
|
||||
submitButtonOptions: {},
|
||||
wrapperClass: 'grid-cols-1',
|
||||
}),
|
||||
);
|
||||
expect(formApi.isMounted).toBe(false);
|
||||
});
|
||||
|
||||
it('should mount form actions', async () => {
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
resetForm: vi.fn(),
|
||||
setFieldValue: vi.fn(),
|
||||
setValues: vi.fn(),
|
||||
submitForm: vi.fn(),
|
||||
validate: vi.fn(),
|
||||
values: { name: 'test' },
|
||||
};
|
||||
|
||||
await formApi.mount(formActions);
|
||||
expect(formApi.isMounted).toBe(true);
|
||||
expect(formApi.form).toEqual(formActions);
|
||||
});
|
||||
|
||||
it('should get values from form', async () => {
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
values: { name: 'test' },
|
||||
};
|
||||
|
||||
await formApi.mount(formActions);
|
||||
const values = await formApi.getValues();
|
||||
expect(values).toEqual({ name: 'test' });
|
||||
});
|
||||
|
||||
it('should set field value', async () => {
|
||||
const setFieldValueMock = vi.fn();
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
setFieldValue: setFieldValueMock,
|
||||
values: { name: 'test' },
|
||||
};
|
||||
|
||||
await formApi.mount(formActions);
|
||||
await formApi.setFieldValue('name', 'new value');
|
||||
expect(setFieldValueMock).toHaveBeenCalledWith(
|
||||
'name',
|
||||
'new value',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should reset form', async () => {
|
||||
const resetFormMock = vi.fn();
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
resetForm: resetFormMock,
|
||||
values: { name: 'test' },
|
||||
};
|
||||
|
||||
await formApi.mount(formActions);
|
||||
await formApi.resetForm();
|
||||
expect(resetFormMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call handleSubmit on submit', async () => {
|
||||
const handleSubmitMock = vi.fn();
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
submitForm: vi.fn().mockResolvedValue(true),
|
||||
values: { name: 'test' },
|
||||
};
|
||||
|
||||
const state = {
|
||||
handleSubmit: handleSubmitMock,
|
||||
};
|
||||
|
||||
formApi.setState(state);
|
||||
await formApi.mount(formActions);
|
||||
|
||||
const result = await formApi.submitForm();
|
||||
expect(formActions.submitForm).toHaveBeenCalled();
|
||||
expect(handleSubmitMock).toHaveBeenCalledWith({ name: 'test' });
|
||||
expect(result).toEqual({ name: 'test' });
|
||||
});
|
||||
|
||||
it('should unmount form and reset state', () => {
|
||||
formApi.unmount();
|
||||
expect(formApi.isMounted).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate form', async () => {
|
||||
const validateMock = vi.fn().mockResolvedValue(true);
|
||||
const formActions: any = {
|
||||
meta: {},
|
||||
validate: validateMock,
|
||||
};
|
||||
|
||||
await formApi.mount(formActions);
|
||||
const isValid = await formApi.validate();
|
||||
expect(validateMock).toHaveBeenCalled();
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSchema', () => {
|
||||
let instance: FormApi;
|
||||
|
||||
beforeEach(() => {
|
||||
instance = new FormApi();
|
||||
instance.state = {
|
||||
schema: [
|
||||
{ component: 'text', fieldName: 'name' },
|
||||
{ component: 'number', fieldName: 'age', label: 'Age' },
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('should update the schema correctly when fieldName matches', () => {
|
||||
const newSchema = [
|
||||
{ component: 'text', fieldName: 'name' },
|
||||
{ component: 'number', fieldName: 'age', label: 'Age' },
|
||||
];
|
||||
|
||||
instance.updateSchema(newSchema);
|
||||
|
||||
expect(instance.state?.schema?.[0]?.component).toBe('text');
|
||||
expect(instance.state?.schema?.[1]?.label).toBe('Age');
|
||||
});
|
||||
|
||||
it('should log an error if fieldName is missing in some items', () => {
|
||||
const newSchema: any[] = [
|
||||
{ component: 'textarea', fieldName: 'name' },
|
||||
{ component: 'number' },
|
||||
];
|
||||
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
instance.updateSchema(newSchema);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'All items in the schema array must have a valid `fieldName` property to be updated',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not update schema if fieldName does not match', () => {
|
||||
const newSchema = [{ component: 'textarea', fieldName: 'unknown' }];
|
||||
|
||||
instance.updateSchema(newSchema);
|
||||
|
||||
expect(instance.state?.schema?.[0]?.component).toBe('text');
|
||||
expect(instance.state?.schema?.[1]?.component).toBe('number');
|
||||
});
|
||||
|
||||
it('should not update schema if updatedMap is empty', () => {
|
||||
const newSchema: any[] = [{ component: 'textarea' }];
|
||||
|
||||
instance.updateSchema(newSchema);
|
||||
|
||||
expect(instance.state?.schema?.[0]?.component).toBe('text');
|
||||
expect(instance.state?.schema?.[1]?.component).toBe('number');
|
||||
});
|
||||
});
|
||||
21
packages/@core/ui-kit/form-ui/build.config.ts
Normal file
21
packages/@core/ui-kit/form-ui/build.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
clean: true,
|
||||
declaration: true,
|
||||
entries: [
|
||||
{
|
||||
builder: 'mkdist',
|
||||
input: './src',
|
||||
loaders: ['vue'],
|
||||
pattern: ['**/*.vue'],
|
||||
},
|
||||
{
|
||||
builder: 'mkdist',
|
||||
format: 'esm',
|
||||
input: './src',
|
||||
loaders: ['js'],
|
||||
pattern: ['**/*.ts'],
|
||||
},
|
||||
],
|
||||
});
|
||||
1
packages/@core/ui-kit/form-ui/postcss.config.mjs
Normal file
1
packages/@core/ui-kit/form-ui/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
||||
87
packages/@core/ui-kit/form-ui/src/config.ts
Normal file
87
packages/@core/ui-kit/form-ui/src/config.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import type {
|
||||
BaseFormComponentType,
|
||||
FormCommonConfig,
|
||||
VbenFormAdapterOptions,
|
||||
} from './types';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import {
|
||||
VbenButton,
|
||||
VbenCheckbox,
|
||||
Input as VbenInput,
|
||||
VbenInputPassword,
|
||||
VbenPinInput,
|
||||
VbenSelect,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
import { globalShareState } from '@vben-core/shared/global-state';
|
||||
|
||||
import { defineRule } from 'vee-validate';
|
||||
|
||||
const DEFAULT_MODEL_PROP_NAME = 'modelValue';
|
||||
|
||||
export const DEFAULT_FORM_COMMON_CONFIG: FormCommonConfig = {};
|
||||
|
||||
export const COMPONENT_MAP: Record<BaseFormComponentType, Component> = {
|
||||
DefaultButton: h(VbenButton, { size: 'sm', variant: 'outline' }),
|
||||
PrimaryButton: h(VbenButton, { size: 'sm', variant: 'default' }),
|
||||
VbenCheckbox,
|
||||
VbenInput,
|
||||
VbenInputPassword,
|
||||
VbenPinInput,
|
||||
VbenSelect,
|
||||
};
|
||||
|
||||
export const COMPONENT_BIND_EVENT_MAP: Partial<
|
||||
Record<BaseFormComponentType, string>
|
||||
> = {
|
||||
VbenCheckbox: 'checked',
|
||||
};
|
||||
|
||||
export function setupVbenForm<
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
>(options: VbenFormAdapterOptions<T>) {
|
||||
const { config, defineRules } = options;
|
||||
|
||||
const {
|
||||
disabledOnChangeListener = true,
|
||||
disabledOnInputListener = true,
|
||||
emptyStateValue = undefined,
|
||||
} = (config || {}) as FormCommonConfig;
|
||||
|
||||
Object.assign(DEFAULT_FORM_COMMON_CONFIG, {
|
||||
disabledOnChangeListener,
|
||||
disabledOnInputListener,
|
||||
emptyStateValue,
|
||||
});
|
||||
|
||||
if (defineRules) {
|
||||
for (const key of Object.keys(defineRules)) {
|
||||
defineRule(key, defineRules[key as never]);
|
||||
}
|
||||
}
|
||||
|
||||
const baseModelPropName =
|
||||
config?.baseModelPropName ?? DEFAULT_MODEL_PROP_NAME;
|
||||
const modelPropNameMap = config?.modelPropNameMap as
|
||||
| Record<BaseFormComponentType, string>
|
||||
| undefined;
|
||||
|
||||
const components = globalShareState.getComponents();
|
||||
|
||||
for (const component of Object.keys(components)) {
|
||||
const key = component as BaseFormComponentType;
|
||||
COMPONENT_MAP[key] = components[component as never];
|
||||
|
||||
if (baseModelPropName !== DEFAULT_MODEL_PROP_NAME) {
|
||||
COMPONENT_BIND_EVENT_MAP[key] = baseModelPropName;
|
||||
}
|
||||
|
||||
// 覆盖特殊组件的modelPropName
|
||||
if (modelPropNameMap && modelPropNameMap[key]) {
|
||||
COMPONENT_BIND_EVENT_MAP[key] = modelPropNameMap[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
24
packages/@core/ui-kit/form-ui/src/form-render/context.ts
Normal file
24
packages/@core/ui-kit/form-ui/src/form-render/context.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { FormRenderProps } from '../types';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { createContext } from '@vben-core/shadcn-ui';
|
||||
|
||||
export const [injectRenderFormProps, provideFormRenderProps] =
|
||||
createContext<FormRenderProps>('FormRenderProps');
|
||||
|
||||
export const useFormContext = () => {
|
||||
const formRenderProps = injectRenderFormProps();
|
||||
|
||||
const isVertical = computed(() => formRenderProps.layout === 'vertical');
|
||||
|
||||
const componentMap = computed(() => formRenderProps.componentMap);
|
||||
const componentBindEventMap = computed(
|
||||
() => formRenderProps.componentBindEventMap,
|
||||
);
|
||||
return {
|
||||
componentBindEventMap,
|
||||
componentMap,
|
||||
isVertical,
|
||||
};
|
||||
};
|
||||
124
packages/@core/ui-kit/form-ui/src/form-render/dependencies.ts
Normal file
124
packages/@core/ui-kit/form-ui/src/form-render/dependencies.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type {
|
||||
FormItemDependencies,
|
||||
FormSchemaRuleType,
|
||||
MaybeComponentProps,
|
||||
} from '../types';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { isBoolean, isFunction } from '@vben-core/shared/utils';
|
||||
|
||||
import { useFormValues } from 'vee-validate';
|
||||
|
||||
import { injectRenderFormProps } from './context';
|
||||
|
||||
export default function useDependencies(
|
||||
getDependencies: () => FormItemDependencies | undefined,
|
||||
) {
|
||||
const values = useFormValues();
|
||||
|
||||
const formRenderProps = injectRenderFormProps();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const formApi = formRenderProps.form!;
|
||||
|
||||
if (!values) {
|
||||
throw new Error('useDependencies should be used within <VbenForm>');
|
||||
}
|
||||
|
||||
const isIf = ref(true);
|
||||
const isDisabled = ref(false);
|
||||
const isShow = ref(true);
|
||||
const isRequired = ref(false);
|
||||
const dynamicComponentProps = ref<MaybeComponentProps>({});
|
||||
const dynamicRules = ref<FormSchemaRuleType>();
|
||||
|
||||
const triggerFieldValues = computed(() => {
|
||||
// 该字段可能会被多个字段触发
|
||||
const triggerFields = getDependencies()?.triggerFields ?? [];
|
||||
return triggerFields.map((dep) => {
|
||||
return values.value[dep];
|
||||
});
|
||||
});
|
||||
|
||||
const resetConditionState = () => {
|
||||
isDisabled.value = false;
|
||||
isIf.value = true;
|
||||
isShow.value = true;
|
||||
isRequired.value = false;
|
||||
dynamicRules.value = undefined;
|
||||
dynamicComponentProps.value = {};
|
||||
};
|
||||
|
||||
watch(
|
||||
[triggerFieldValues, getDependencies],
|
||||
async ([_values, dependencies]) => {
|
||||
if (!dependencies || !dependencies?.triggerFields?.length) {
|
||||
return;
|
||||
}
|
||||
resetConditionState();
|
||||
const {
|
||||
componentProps,
|
||||
disabled,
|
||||
if: whenIf,
|
||||
required,
|
||||
rules,
|
||||
show,
|
||||
trigger,
|
||||
} = dependencies;
|
||||
|
||||
// 1. 优先判断if,如果if为false,则不渲染dom,后续判断也不再执行
|
||||
const formValues = values.value;
|
||||
|
||||
if (isFunction(whenIf)) {
|
||||
isIf.value = !!(await whenIf(formValues, formApi));
|
||||
// 不渲染
|
||||
if (!isIf.value) return;
|
||||
} else if (isBoolean(whenIf)) {
|
||||
isIf.value = whenIf;
|
||||
if (!isIf.value) return;
|
||||
}
|
||||
|
||||
// 2. 判断show,如果show为false,则隐藏
|
||||
if (isFunction(show)) {
|
||||
isShow.value = !!(await show(formValues, formApi));
|
||||
if (!isShow.value) return;
|
||||
} else if (isBoolean(show)) {
|
||||
isShow.value = show;
|
||||
if (!isShow.value) return;
|
||||
}
|
||||
|
||||
if (isFunction(componentProps)) {
|
||||
dynamicComponentProps.value = await componentProps(formValues, formApi);
|
||||
}
|
||||
|
||||
if (isFunction(rules)) {
|
||||
dynamicRules.value = await rules(formValues, formApi);
|
||||
}
|
||||
|
||||
if (isFunction(disabled)) {
|
||||
isDisabled.value = !!(await disabled(formValues, formApi));
|
||||
} else if (isBoolean(disabled)) {
|
||||
isDisabled.value = disabled;
|
||||
}
|
||||
|
||||
if (isFunction(required)) {
|
||||
isRequired.value = !!(await required(formValues, formApi));
|
||||
}
|
||||
|
||||
if (isFunction(trigger)) {
|
||||
await trigger(formValues, formApi);
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
|
||||
return {
|
||||
dynamicComponentProps,
|
||||
dynamicRules,
|
||||
isDisabled,
|
||||
isIf,
|
||||
isRequired,
|
||||
isShow,
|
||||
};
|
||||
}
|
||||
105
packages/@core/ui-kit/form-ui/src/form-render/expandable.ts
Normal file
105
packages/@core/ui-kit/form-ui/src/form-render/expandable.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { FormRenderProps } from '../types';
|
||||
|
||||
import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
|
||||
|
||||
import {
|
||||
breakpointsTailwind,
|
||||
useBreakpoints,
|
||||
useElementVisibility,
|
||||
} from '@vueuse/core';
|
||||
|
||||
/**
|
||||
* 动态计算行数
|
||||
*/
|
||||
export function useExpandable(props: FormRenderProps) {
|
||||
const wrapperRef = useTemplateRef<HTMLElement>('wrapperRef');
|
||||
const isVisible = useElementVisibility(wrapperRef);
|
||||
const rowMapping = ref<Record<number, number>>({});
|
||||
// 是否已经计算过一次
|
||||
const isCalculated = ref(false);
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
|
||||
const keepFormItemIndex = computed(() => {
|
||||
const rows = props.collapsedRows ?? 1;
|
||||
const mapping = rowMapping.value;
|
||||
let maxItem = 0;
|
||||
for (let index = 1; index <= rows; index++) {
|
||||
maxItem += mapping?.[index] ?? 0;
|
||||
}
|
||||
// 保持一行
|
||||
return maxItem - 1 || 1;
|
||||
});
|
||||
|
||||
watch(
|
||||
[
|
||||
() => props.showCollapseButton,
|
||||
() => breakpoints.active().value,
|
||||
() => props.schema?.length,
|
||||
() => isVisible.value,
|
||||
],
|
||||
async ([val]) => {
|
||||
if (val) {
|
||||
await nextTick();
|
||||
rowMapping.value = {};
|
||||
isCalculated.value = false;
|
||||
await calculateRowMapping();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
async function calculateRowMapping() {
|
||||
if (!props.showCollapseButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
if (!wrapperRef.value) {
|
||||
return;
|
||||
}
|
||||
// 小屏幕不计算
|
||||
// if (breakpoints.smaller('sm').value) {
|
||||
// // 保持一行
|
||||
// rowMapping.value = { 1: 2 };
|
||||
// return;
|
||||
// }
|
||||
|
||||
const formItems = [...wrapperRef.value.children];
|
||||
|
||||
const container = wrapperRef.value;
|
||||
const containerStyles = window.getComputedStyle(container);
|
||||
const rowHeights = containerStyles
|
||||
.getPropertyValue('grid-template-rows')
|
||||
.split(' ');
|
||||
|
||||
const containerRect = container?.getBoundingClientRect();
|
||||
|
||||
formItems.forEach((el) => {
|
||||
const itemRect = el.getBoundingClientRect();
|
||||
|
||||
// 计算元素在第几行
|
||||
const itemTop = itemRect.top - containerRect.top;
|
||||
let rowStart = 0;
|
||||
let cumulativeHeight = 0;
|
||||
|
||||
for (const [i, rowHeight] of rowHeights.entries()) {
|
||||
cumulativeHeight += Number.parseFloat(rowHeight);
|
||||
if (itemTop < cumulativeHeight) {
|
||||
rowStart = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rowStart > (props?.collapsedRows ?? 1)) {
|
||||
return;
|
||||
}
|
||||
rowMapping.value[rowStart] = (rowMapping.value[rowStart] ?? 0) + 1;
|
||||
isCalculated.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
calculateRowMapping();
|
||||
});
|
||||
|
||||
return { isCalculated, keepFormItemIndex, wrapperRef };
|
||||
}
|
||||
60
packages/@core/ui-kit/form-ui/src/form-render/helper.ts
Normal file
60
packages/@core/ui-kit/form-ui/src/form-render/helper.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type {
|
||||
AnyZodObject,
|
||||
ZodDefault,
|
||||
ZodEffects,
|
||||
ZodNumber,
|
||||
ZodString,
|
||||
ZodTypeAny,
|
||||
} from 'zod';
|
||||
|
||||
import { isObject, isString } from '@vben-core/shared/utils';
|
||||
|
||||
/**
|
||||
* Get the lowest level Zod type.
|
||||
* This will unpack optionals, refinements, etc.
|
||||
*/
|
||||
export function getBaseRules<
|
||||
ChildType extends AnyZodObject | ZodTypeAny = ZodTypeAny,
|
||||
>(schema: ChildType | ZodEffects<ChildType>): ChildType | null {
|
||||
if (!schema || isString(schema)) return null;
|
||||
if ('innerType' in schema._def)
|
||||
return getBaseRules(schema._def.innerType as ChildType);
|
||||
|
||||
if ('schema' in schema._def)
|
||||
return getBaseRules(schema._def.schema as ChildType);
|
||||
|
||||
return schema as ChildType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a "ZodDefault" in the Zod stack and return its value.
|
||||
*/
|
||||
export function getDefaultValueInZodStack(schema: ZodTypeAny): any {
|
||||
if (!schema || isString(schema)) {
|
||||
return;
|
||||
}
|
||||
const typedSchema = schema as unknown as ZodDefault<ZodNumber | ZodString>;
|
||||
|
||||
if (typedSchema._def.typeName === 'ZodDefault')
|
||||
return typedSchema._def.defaultValue();
|
||||
|
||||
if ('innerType' in typedSchema._def) {
|
||||
return getDefaultValueInZodStack(
|
||||
typedSchema._def.innerType as unknown as ZodTypeAny,
|
||||
);
|
||||
}
|
||||
if ('schema' in typedSchema._def) {
|
||||
return getDefaultValueInZodStack(
|
||||
(typedSchema._def as any).schema as ZodTypeAny,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isEventObjectLike(obj: any) {
|
||||
if (!obj || !isObject(obj)) {
|
||||
return false;
|
||||
}
|
||||
return Reflect.has(obj, 'target') && Reflect.has(obj, 'stopPropagation');
|
||||
}
|
||||
3
packages/@core/ui-kit/form-ui/src/form-render/index.ts
Normal file
3
packages/@core/ui-kit/form-ui/src/form-render/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as FormField } from './form-field.vue';
|
||||
export { default as FormLabel } from './form-label.vue';
|
||||
export { default as Form } from './form.vue';
|
||||
12
packages/@core/ui-kit/form-ui/src/index.ts
Normal file
12
packages/@core/ui-kit/form-ui/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { setupVbenForm } from './config';
|
||||
|
||||
export type {
|
||||
BaseFormComponentType,
|
||||
ExtendedFormApi,
|
||||
VbenFormProps,
|
||||
FormSchema as VbenFormSchema,
|
||||
} from './types';
|
||||
|
||||
export * from './use-vben-form';
|
||||
// export { default as VbenForm } from './vben-form.vue';
|
||||
export * as z from 'zod';
|
||||
50
packages/@core/ui-kit/form-ui/src/use-vben-form.ts
Normal file
50
packages/@core/ui-kit/form-ui/src/use-vben-form.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type {
|
||||
BaseFormComponentType,
|
||||
ExtendedFormApi,
|
||||
VbenFormProps,
|
||||
} from './types';
|
||||
|
||||
import { defineComponent, h, isReactive, onBeforeUnmount, watch } from 'vue';
|
||||
|
||||
import { useStore } from '@vben-core/shared/store';
|
||||
|
||||
import { FormApi } from './form-api';
|
||||
import VbenUseForm from './vben-use-form.vue';
|
||||
|
||||
export function useVbenForm<
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
>(options: VbenFormProps<T>) {
|
||||
const IS_REACTIVE = isReactive(options);
|
||||
const api = new FormApi(options);
|
||||
const extendedApi: ExtendedFormApi = api as never;
|
||||
extendedApi.useStore = (selector) => {
|
||||
return useStore(api.store, selector);
|
||||
};
|
||||
|
||||
const Form = defineComponent(
|
||||
(props: VbenFormProps, { attrs, slots }) => {
|
||||
onBeforeUnmount(() => {
|
||||
api.unmount();
|
||||
});
|
||||
api.setState({ ...props, ...attrs });
|
||||
return () =>
|
||||
h(VbenUseForm, { ...props, ...attrs, formApi: extendedApi }, slots);
|
||||
},
|
||||
{
|
||||
name: 'VbenUseForm',
|
||||
inheritAttrs: false,
|
||||
},
|
||||
);
|
||||
// Add reactivity support
|
||||
if (IS_REACTIVE) {
|
||||
watch(
|
||||
() => options.schema,
|
||||
() => {
|
||||
api.setState({ schema: options.schema });
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
}
|
||||
|
||||
return [Form, extendedApi] as const;
|
||||
}
|
||||
1
packages/@core/ui-kit/form-ui/tailwind.config.mjs
Normal file
1
packages/@core/ui-kit/form-ui/tailwind.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config';
|
||||
6
packages/@core/ui-kit/form-ui/tsconfig.json
Normal file
6
packages/@core/ui-kit/form-ui/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/web.json",
|
||||
"include": ["src", "__tests__"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user