pdyform 2.0.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -74
- package/package.json +9 -3
- package/packages/core/dist/chunk-6F4PWJZI.js +0 -0
- package/packages/core/dist/chunk-KA6QUMVR.js +158 -0
- package/packages/core/dist/chunk-REHKL5VH.js +76 -0
- package/packages/core/dist/formState.cjs +132 -78
- package/packages/core/dist/formState.d.cts +14 -10
- package/packages/core/dist/formState.d.ts +14 -10
- package/packages/core/dist/formState.js +4 -12
- package/packages/core/dist/index.cjs +138 -78
- package/packages/core/dist/index.d.cts +4 -3
- package/packages/core/dist/index.d.ts +4 -3
- package/packages/core/dist/index.js +11 -12
- package/packages/core/dist/types.d.cts +17 -5
- package/packages/core/dist/types.d.ts +17 -5
- package/packages/core/dist/types.js +1 -0
- package/packages/core/dist/utils.cjs +78 -19
- package/packages/core/dist/utils.d.cts +17 -5
- package/packages/core/dist/utils.d.ts +17 -5
- package/packages/core/dist/utils.js +7 -1
- package/packages/react/dist/index.cjs +1 -461
- package/packages/react/dist/index.d.cts +33 -2
- package/packages/react/dist/index.d.ts +33 -2
- package/packages/react/dist/index.js +1 -411
- package/packages/vue/dist/index.d.ts +51 -39
- package/packages/vue/dist/index.js +1 -28
- package/packages/vue/dist/index.mjs +630 -6527
- package/eslint.config.mjs +0 -53
- package/example/README.md +0 -36
- package/example/react-demo/index.html +0 -12
- package/example/react-demo/node_modules/.bin/browserslist +0 -17
- package/example/react-demo/node_modules/.bin/tsc +0 -17
- package/example/react-demo/node_modules/.bin/tsserver +0 -17
- package/example/react-demo/node_modules/.bin/vite +0 -17
- package/example/react-demo/node_modules/.vite/deps/@radix-ui_react-checkbox.js +0 -300
- package/example/react-demo/node_modules/.vite/deps/@radix-ui_react-checkbox.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/@radix-ui_react-label.js +0 -194
- package/example/react-demo/node_modules/.vite/deps/@radix-ui_react-label.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/@radix-ui_react-radio-group.js +0 -530
- package/example/react-demo/node_modules/.vite/deps/@radix-ui_react-radio-group.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/@radix-ui_react-select.js +0 -4808
- package/example/react-demo/node_modules/.vite/deps/@radix-ui_react-select.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/_metadata.json +0 -115
- package/example/react-demo/node_modules/.vite/deps/chunk-3D5PZ6F6.js +0 -49
- package/example/react-demo/node_modules/.vite/deps/chunk-3D5PZ6F6.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/chunk-5Q2RBQLA.js +0 -127
- package/example/react-demo/node_modules/.vite/deps/chunk-5Q2RBQLA.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/chunk-G3PMV62Z.js +0 -36
- package/example/react-demo/node_modules/.vite/deps/chunk-G3PMV62Z.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/chunk-GX7YZ5KV.js +0 -370
- package/example/react-demo/node_modules/.vite/deps/chunk-GX7YZ5KV.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/chunk-PUFJGYAC.js +0 -928
- package/example/react-demo/node_modules/.vite/deps/chunk-PUFJGYAC.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/chunk-SIU35MPB.js +0 -21
- package/example/react-demo/node_modules/.vite/deps/chunk-SIU35MPB.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/chunk-TOMGVNQP.js +0 -1906
- package/example/react-demo/node_modules/.vite/deps/chunk-TOMGVNQP.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/chunk-YYN6DZAU.js +0 -21628
- package/example/react-demo/node_modules/.vite/deps/chunk-YYN6DZAU.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/chunk-ZE5VSJFE.js +0 -144
- package/example/react-demo/node_modules/.vite/deps/chunk-ZE5VSJFE.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/class-variance-authority.js +0 -51
- package/example/react-demo/node_modules/.vite/deps/class-variance-authority.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/clsx.js +0 -10
- package/example/react-demo/node_modules/.vite/deps/clsx.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/lucide-react.js +0 -29725
- package/example/react-demo/node_modules/.vite/deps/lucide-react.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/package.json +0 -3
- package/example/react-demo/node_modules/.vite/deps/react-dom.js +0 -7
- package/example/react-demo/node_modules/.vite/deps/react-dom.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/react-dom_client.js +0 -39
- package/example/react-demo/node_modules/.vite/deps/react-dom_client.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/react.js +0 -6
- package/example/react-demo/node_modules/.vite/deps/react.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/react_jsx-dev-runtime.js +0 -913
- package/example/react-demo/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/react_jsx-runtime.js +0 -7
- package/example/react-demo/node_modules/.vite/deps/react_jsx-runtime.js.map +0 -7
- package/example/react-demo/node_modules/.vite/deps/tailwind-merge.js +0 -2534
- package/example/react-demo/node_modules/.vite/deps/tailwind-merge.js.map +0 -7
- package/example/react-demo/package.json +0 -23
- package/example/react-demo/postcss.config.mjs +0 -6
- package/example/react-demo/src/App.tsx +0 -64
- package/example/react-demo/src/main.tsx +0 -10
- package/example/react-demo/src/styles.css +0 -102
- package/example/react-demo/tailwind.config.mjs +0 -50
- package/example/react-demo/tsconfig.json +0 -16
- package/example/react-demo/vite.config.ts +0 -14
- package/example/shared/defaultSchema.ts +0 -68
- package/example/vue-demo/index.html +0 -12
- package/example/vue-demo/node_modules/.bin/tsc +0 -17
- package/example/vue-demo/node_modules/.bin/tsserver +0 -17
- package/example/vue-demo/node_modules/.bin/vite +0 -17
- package/example/vue-demo/node_modules/.vite/deps/_metadata.json +0 -46
- package/example/vue-demo/node_modules/.vite/deps/chunk-PZ5AY32C.js +0 -10
- package/example/vue-demo/node_modules/.vite/deps/chunk-PZ5AY32C.js.map +0 -7
- package/example/vue-demo/node_modules/.vite/deps/chunk-TCXBSQ4M.js +0 -12877
- package/example/vue-demo/node_modules/.vite/deps/chunk-TCXBSQ4M.js.map +0 -7
- package/example/vue-demo/node_modules/.vite/deps/clsx.js +0 -22
- package/example/vue-demo/node_modules/.vite/deps/clsx.js.map +0 -7
- package/example/vue-demo/node_modules/.vite/deps/lucide-vue-next.js +0 -29720
- package/example/vue-demo/node_modules/.vite/deps/lucide-vue-next.js.map +0 -7
- package/example/vue-demo/node_modules/.vite/deps/package.json +0 -3
- package/example/vue-demo/node_modules/.vite/deps/radix-vue.js +0 -24321
- package/example/vue-demo/node_modules/.vite/deps/radix-vue.js.map +0 -7
- package/example/vue-demo/node_modules/.vite/deps/tailwind-merge.js +0 -2534
- package/example/vue-demo/node_modules/.vite/deps/tailwind-merge.js.map +0 -7
- package/example/vue-demo/node_modules/.vite/deps/vue.js +0 -348
- package/example/vue-demo/node_modules/.vite/deps/vue.js.map +0 -7
- package/example/vue-demo/package.json +0 -20
- package/example/vue-demo/postcss.config.mjs +0 -6
- package/example/vue-demo/src/App.vue +0 -61
- package/example/vue-demo/src/env.d.ts +0 -1
- package/example/vue-demo/src/main.ts +0 -5
- package/example/vue-demo/src/style.css +0 -102
- package/example/vue-demo/tailwind.config.mjs +0 -50
- package/example/vue-demo/tsconfig.json +0 -15
- package/example/vue-demo/vite.config.ts +0 -14
- package/packages/core/dist/chunk-TP3IHKWV.js +0 -69
- package/packages/core/dist/chunk-WEDHXOHH.js +0 -102
- package/packages/core/node_modules/.bin/api-extractor +0 -17
- package/packages/core/node_modules/.bin/esbuild +0 -14
- package/packages/core/node_modules/.bin/jiti +0 -17
- package/packages/core/node_modules/.bin/tsc +0 -17
- package/packages/core/node_modules/.bin/tsserver +0 -17
- package/packages/core/node_modules/.bin/tsup +0 -17
- package/packages/core/node_modules/.bin/tsup-node +0 -17
- package/packages/core/node_modules/.bin/vitest +0 -17
- package/packages/core/node_modules/.vite/vitest/results.json +0 -1
- package/packages/core/package.json +0 -37
- package/packages/core/src/formState.ts +0 -79
- package/packages/core/src/index.ts +0 -3
- package/packages/core/src/types.ts +0 -42
- package/packages/core/src/utils.ts +0 -104
- package/packages/core/test/formState.test.ts +0 -71
- package/packages/core/test/utils.test.ts +0 -178
- package/packages/core/tsconfig.json +0 -15
- package/packages/core/tsup.config.ts +0 -9
- package/packages/react/node_modules/.bin/api-extractor +0 -17
- package/packages/react/node_modules/.bin/browserslist +0 -17
- package/packages/react/node_modules/.bin/esbuild +0 -14
- package/packages/react/node_modules/.bin/jiti +0 -17
- package/packages/react/node_modules/.bin/tsc +0 -17
- package/packages/react/node_modules/.bin/tsserver +0 -17
- package/packages/react/node_modules/.bin/tsup +0 -17
- package/packages/react/node_modules/.bin/tsup-node +0 -17
- package/packages/react/node_modules/.bin/vite +0 -17
- package/packages/react/node_modules/.bin/vitest +0 -17
- package/packages/react/node_modules/.vite/vitest/results.json +0 -1
- package/packages/react/package.json +0 -57
- package/packages/react/postcss.config.mjs +0 -6
- package/packages/react/src/DynamicForm.tsx +0 -69
- package/packages/react/src/FormFieldRenderer.tsx +0 -50
- package/packages/react/src/components/Checkbox.tsx +0 -28
- package/packages/react/src/components/CheckboxRenderer.tsx +0 -37
- package/packages/react/src/components/Input.tsx +0 -24
- package/packages/react/src/components/InputRenderer.tsx +0 -25
- package/packages/react/src/components/Label.tsx +0 -24
- package/packages/react/src/components/RadioGroup.tsx +0 -42
- package/packages/react/src/components/RadioRenderer.tsx +0 -29
- package/packages/react/src/components/Select.tsx +0 -93
- package/packages/react/src/components/SelectRenderer.tsx +0 -27
- package/packages/react/src/components/Textarea.tsx +0 -23
- package/packages/react/src/components/TextareaRenderer.tsx +0 -17
- package/packages/react/src/components/index.ts +0 -55
- package/packages/react/src/components/types.ts +0 -17
- package/packages/react/src/index.tsx +0 -3
- package/packages/react/src/utils.ts +0 -7
- package/packages/react/tailwind.config.mjs +0 -10
- package/packages/react/test/DynamicForm.test.tsx +0 -25
- package/packages/react/test/FormFieldRenderer.test.tsx +0 -127
- package/packages/react/tsconfig.json +0 -15
- package/packages/react/vitest.config.ts +0 -16
- package/packages/vue/node_modules/.bin/rollup +0 -17
- package/packages/vue/node_modules/.bin/tsc +0 -17
- package/packages/vue/node_modules/.bin/tsserver +0 -17
- package/packages/vue/node_modules/.bin/vite +0 -17
- package/packages/vue/node_modules/.bin/vitest +0 -17
- package/packages/vue/node_modules/.bin/vue-tsc +0 -17
- package/packages/vue/node_modules/.vite/vitest/results.json +0 -1
- package/packages/vue/node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts +0 -118
- package/packages/vue/node_modules/.vue-global-types/vue_3.5_false.d.ts +0 -128
- package/packages/vue/package.json +0 -53
- package/packages/vue/postcss.config.mjs +0 -6
- package/packages/vue/src/DynamicForm.vue +0 -63
- package/packages/vue/src/FormFieldRenderer.vue +0 -70
- package/packages/vue/src/components/Checkbox.vue +0 -28
- package/packages/vue/src/components/CheckboxRenderer.vue +0 -35
- package/packages/vue/src/components/Input.vue +0 -21
- package/packages/vue/src/components/InputRenderer.vue +0 -24
- package/packages/vue/src/components/Label.vue +0 -21
- package/packages/vue/src/components/RadioGroup.vue +0 -30
- package/packages/vue/src/components/RadioGroupItem.vue +0 -26
- package/packages/vue/src/components/RadioRenderer.vue +0 -24
- package/packages/vue/src/components/Select.vue +0 -40
- package/packages/vue/src/components/SelectContent.vue +0 -38
- package/packages/vue/src/components/SelectItem.vue +0 -43
- package/packages/vue/src/components/SelectRenderer.vue +0 -30
- package/packages/vue/src/components/SelectTrigger.vue +0 -27
- package/packages/vue/src/components/Textarea.vue +0 -19
- package/packages/vue/src/components/TextareaRenderer.vue +0 -18
- package/packages/vue/src/components/index.ts +0 -24
- package/packages/vue/src/env.d.ts +0 -7
- package/packages/vue/src/fieldComponentMap.ts +0 -34
- package/packages/vue/src/index.ts +0 -4
- package/packages/vue/src/utils.ts +0 -6
- package/packages/vue/tailwind.config.mjs +0 -10
- package/packages/vue/test/DynamicForm.test.ts +0 -19
- package/packages/vue/test/FormFieldRenderer.test.ts +0 -133
- package/packages/vue/tsconfig.json +0 -16
- package/packages/vue/vite.config.ts +0 -29
- package/packages/vue/vitest.config.ts +0 -16
- package/pnpm-workspace.yaml +0 -4
- package/tsconfig.json +0 -21
- package/turbo.json +0 -25
package/README.md
CHANGED
|
@@ -15,49 +15,64 @@ A high-performance, schema-driven dynamic form system with **React** and **Vue**
|
|
|
15
15
|
### 🌟 Features
|
|
16
16
|
|
|
17
17
|
- **Schema-Driven**: Define your forms using a simple and intuitive JSON/JS schema.
|
|
18
|
-
- **Framework Agnostic Core**: Core logic is entirely framework-free, making it extremely lightweight.
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
18
|
+
- **Framework Agnostic Core**: Core logic is entirely framework-free, making it extremely lightweight and portable.
|
|
19
|
+
- **Conditional Logic**: Supports dynamic `hidden` and `disabled` states based on form values (supports both boolean and functions).
|
|
20
|
+
- **React & Vue Support**: Out-of-the-box UI components built on top of modern UI libraries (Radix UI / Shadcn).
|
|
21
|
+
- **Type Safety**: Built with TypeScript for excellent developer experience and catch errors early.
|
|
22
|
+
- **High Performance**: Optimized rendering using store-based state management (Zustand) to minimize re-renders.
|
|
22
23
|
|
|
23
24
|
### 📦 Packages
|
|
24
25
|
|
|
25
26
|
The monorepo contains the following packages:
|
|
26
27
|
|
|
27
|
-
- `pdyform-core`: Framework-agnostic logic, utilities, and schema parser.
|
|
28
|
-
- `pdyform-react`: React components
|
|
29
|
-
- `pdyform-vue`: Vue components
|
|
28
|
+
- `pdyform-core`: Framework-agnostic logic, validation utilities, and schema parser.
|
|
29
|
+
- `pdyform-react`: React components and hooks (`useForm`).
|
|
30
|
+
- `pdyform-vue`: Vue components and composables (`useForm`).
|
|
30
31
|
|
|
31
32
|
### 🚀 Installation
|
|
32
33
|
|
|
33
34
|
```bash
|
|
34
|
-
|
|
35
|
+
# Install core and framework-specific package
|
|
36
|
+
pnpm add pdyform-core pdyform-react # For React
|
|
35
37
|
# or
|
|
36
|
-
pnpm add pdyform
|
|
37
|
-
# or
|
|
38
|
-
yarn add pdyform
|
|
38
|
+
pnpm add pdyform-core pdyform-vue # For Vue
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
### 💻 Usage
|
|
42
42
|
|
|
43
|
-
This package provides a unified entry point. You can import framework-specific components directly from the main package:
|
|
44
|
-
|
|
45
43
|
#### React
|
|
46
44
|
|
|
47
45
|
```tsx
|
|
48
46
|
import React from 'react';
|
|
49
|
-
import { DynamicForm } from 'pdyform
|
|
50
|
-
import type { FormSchema } from 'pdyform
|
|
47
|
+
import { DynamicForm } from 'pdyform-react';
|
|
48
|
+
import type { FormSchema } from 'pdyform-core';
|
|
51
49
|
|
|
52
50
|
const schema: FormSchema = {
|
|
51
|
+
title: "Login",
|
|
53
52
|
fields: [
|
|
54
|
-
{
|
|
55
|
-
|
|
53
|
+
{
|
|
54
|
+
name: 'username',
|
|
55
|
+
label: 'Username',
|
|
56
|
+
type: 'text',
|
|
57
|
+
validations: [{ type: 'required', message: 'Username is required' }]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'isAdmin',
|
|
61
|
+
label: 'Is Admin?',
|
|
62
|
+
type: 'switch'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'adminToken',
|
|
66
|
+
label: 'Admin Token',
|
|
67
|
+
type: 'password',
|
|
68
|
+
// Conditional logic: only show if isAdmin is true
|
|
69
|
+
hidden: (values) => !values.isAdmin
|
|
70
|
+
}
|
|
56
71
|
]
|
|
57
72
|
};
|
|
58
73
|
|
|
59
74
|
export default function App() {
|
|
60
|
-
return <DynamicForm schema={schema} onSubmit={console.log} />;
|
|
75
|
+
return <DynamicForm schema={schema} onSubmit={(values) => console.log(values)} />;
|
|
61
76
|
}
|
|
62
77
|
```
|
|
63
78
|
|
|
@@ -65,17 +80,25 @@ export default function App() {
|
|
|
65
80
|
|
|
66
81
|
```vue
|
|
67
82
|
<script setup lang="ts">
|
|
68
|
-
import { DynamicForm } from 'pdyform
|
|
69
|
-
import type { FormSchema } from 'pdyform
|
|
83
|
+
import { DynamicForm } from 'pdyform-vue';
|
|
84
|
+
import type { FormSchema } from 'pdyform-core';
|
|
70
85
|
|
|
71
86
|
const schema: FormSchema = {
|
|
87
|
+
title: "Profile",
|
|
72
88
|
fields: [
|
|
73
|
-
{
|
|
74
|
-
|
|
89
|
+
{
|
|
90
|
+
name: 'email',
|
|
91
|
+
label: 'Email',
|
|
92
|
+
type: 'email',
|
|
93
|
+
validations: [
|
|
94
|
+
{ type: 'required', message: 'Email is required' },
|
|
95
|
+
{ type: 'email', message: 'Invalid email format' }
|
|
96
|
+
]
|
|
97
|
+
}
|
|
75
98
|
]
|
|
76
99
|
};
|
|
77
100
|
|
|
78
|
-
const handleSubmit = (
|
|
101
|
+
const handleSubmit = (values: any) => console.log(values);
|
|
79
102
|
</script>
|
|
80
103
|
|
|
81
104
|
<template>
|
|
@@ -87,54 +110,70 @@ const handleSubmit = (data: any) => console.log(data);
|
|
|
87
110
|
|
|
88
111
|
## 中文说明
|
|
89
112
|
|
|
90
|
-
一个高性能、基于 Schema 驱动的动态表单系统,同时支持 **React** 和 **Vue**。它提供了一个与框架无关的核心逻辑层用于 Schema
|
|
113
|
+
一个高性能、基于 Schema 驱动的动态表单系统,同时支持 **React** 和 **Vue**。它提供了一个与框架无关的核心逻辑层用于 Schema 解析和表单校验,允许在不同的框架中无缝集成并保持一致的配置体验。
|
|
91
114
|
|
|
92
115
|
### 🌟 特性
|
|
93
116
|
|
|
94
117
|
- **Schema 驱动**: 使用简单直观的 JSON/JS 对象定义你的表单。
|
|
95
|
-
- **框架无关核心**:
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
118
|
+
- **框架无关核心**: 核心逻辑完全独立于 UI 框架,极其轻量且易于移植。
|
|
119
|
+
- **联动逻辑**: 支持基于表单实时数值动态控制字段的 `hidden`(隐藏)和 `disabled`(禁用)状态(支持布尔值或函数)。
|
|
120
|
+
- **支持 React & Vue**: 提供基于现代 UI 库(Radix UI / Shadcn)的开箱即用组件。
|
|
121
|
+
- **类型安全**: 全量 TypeScript 编写,提供极佳的开发体验。
|
|
122
|
+
- **高性能**: 基于 Zustand 状态管理库优化渲染逻辑,最小化不必要的组件重绘。
|
|
99
123
|
|
|
100
124
|
### 📦 包结构
|
|
101
125
|
|
|
102
|
-
此 Monorepo 包含以下几个核心子包:
|
|
103
|
-
|
|
104
126
|
- `pdyform-core`: 与框架无关的核心表单逻辑、校验工具和 Schema 解析器。
|
|
105
|
-
- `pdyform-react`: 基于
|
|
106
|
-
- `pdyform-vue`: 基于
|
|
127
|
+
- `pdyform-react`: 基于 React 的动态表单组件与 `useForm` Hook。
|
|
128
|
+
- `pdyform-vue`: 基于 Vue 的动态表单组件与 `useForm` 组合式 API。
|
|
107
129
|
|
|
108
130
|
### 🚀 安装
|
|
109
131
|
|
|
110
132
|
```bash
|
|
111
|
-
|
|
112
|
-
#
|
|
113
|
-
pnpm add pdyform
|
|
133
|
+
# 安装核心包和对应的框架包
|
|
134
|
+
pnpm add pdyform-core pdyform-react # React 项目
|
|
114
135
|
# 或
|
|
115
|
-
|
|
136
|
+
pnpm add pdyform-core pdyform-vue # Vue 项目
|
|
116
137
|
```
|
|
117
138
|
|
|
118
139
|
### 💻 基本使用
|
|
119
140
|
|
|
120
|
-
`pdyform` 提供了统一的导出入口。你可以直接从主包中按需引入对应框架的组件和核心类型:
|
|
121
|
-
|
|
122
141
|
#### React 示例
|
|
123
142
|
|
|
124
143
|
```tsx
|
|
125
144
|
import React from 'react';
|
|
126
|
-
import { DynamicForm } from 'pdyform
|
|
127
|
-
import type { FormSchema } from 'pdyform
|
|
145
|
+
import { DynamicForm } from 'pdyform-react';
|
|
146
|
+
import type { FormSchema } from 'pdyform-core';
|
|
128
147
|
|
|
129
148
|
const schema: FormSchema = {
|
|
130
149
|
fields: [
|
|
131
|
-
{
|
|
132
|
-
|
|
150
|
+
{
|
|
151
|
+
name: 'username',
|
|
152
|
+
label: '用户名',
|
|
153
|
+
type: 'text',
|
|
154
|
+
validations: [{ type: 'required', message: '请输入用户名' }]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'type',
|
|
158
|
+
label: '用户类型',
|
|
159
|
+
type: 'select',
|
|
160
|
+
options: [
|
|
161
|
+
{ label: '个人', value: 'personal' },
|
|
162
|
+
{ label: '企业', value: 'enterprise' }
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'companyName',
|
|
167
|
+
label: '公司名称',
|
|
168
|
+
type: 'text',
|
|
169
|
+
// 联动逻辑:仅当用户类型为 'enterprise' 时显示
|
|
170
|
+
hidden: (values) => values.type !== 'enterprise'
|
|
171
|
+
}
|
|
133
172
|
]
|
|
134
173
|
};
|
|
135
174
|
|
|
136
175
|
export default function App() {
|
|
137
|
-
return <DynamicForm schema={schema} onSubmit={console.log} />;
|
|
176
|
+
return <DynamicForm schema={schema} onSubmit={(values) => console.log(values)} />;
|
|
138
177
|
}
|
|
139
178
|
```
|
|
140
179
|
|
|
@@ -142,17 +181,21 @@ export default function App() {
|
|
|
142
181
|
|
|
143
182
|
```vue
|
|
144
183
|
<script setup lang="ts">
|
|
145
|
-
import { DynamicForm } from 'pdyform
|
|
146
|
-
import type { FormSchema } from 'pdyform
|
|
184
|
+
import { DynamicForm } from 'pdyform-vue';
|
|
185
|
+
import type { FormSchema } from 'pdyform-core';
|
|
147
186
|
|
|
148
187
|
const schema: FormSchema = {
|
|
149
188
|
fields: [
|
|
150
|
-
{
|
|
151
|
-
|
|
189
|
+
{
|
|
190
|
+
name: 'nickname',
|
|
191
|
+
label: '昵称',
|
|
192
|
+
type: 'text',
|
|
193
|
+
validations: [{ type: 'required', message: '昵称不能为空' }]
|
|
194
|
+
}
|
|
152
195
|
]
|
|
153
196
|
};
|
|
154
197
|
|
|
155
|
-
const handleSubmit = (
|
|
198
|
+
const handleSubmit = (values: any) => console.log(values);
|
|
156
199
|
</script>
|
|
157
200
|
|
|
158
201
|
<template>
|
|
@@ -165,39 +208,20 @@ const handleSubmit = (data: any) => console.log(data);
|
|
|
165
208
|
## 🛠️ Development / 本地开发
|
|
166
209
|
|
|
167
210
|
```bash
|
|
168
|
-
#
|
|
211
|
+
# 安装依赖
|
|
169
212
|
pnpm install
|
|
170
213
|
|
|
171
|
-
#
|
|
214
|
+
# 编译所有包
|
|
172
215
|
pnpm run build:all
|
|
173
216
|
|
|
174
|
-
#
|
|
217
|
+
# 运行所有单元测试
|
|
175
218
|
pnpm run test:all
|
|
176
219
|
```
|
|
177
220
|
|
|
178
221
|
## Examples / 示例工程
|
|
179
222
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
- `example/react-demo`:React 渲染与 schema 调试
|
|
183
|
-
- `example/vue-demo`:Vue 渲染与 schema 调试
|
|
184
|
-
- `example/shared/defaultSchema.ts`:共享默认 schema
|
|
185
|
-
|
|
186
|
-
在根目录运行:
|
|
187
|
-
|
|
188
|
-
```bash
|
|
189
|
-
# React + Vue 同时启动(推荐)
|
|
190
|
-
pnpm run dev:example
|
|
191
|
-
|
|
192
|
-
# 单独启动 React demo
|
|
193
|
-
pnpm run dev:example:react
|
|
194
|
-
|
|
195
|
-
# 单独启动 Vue demo
|
|
196
|
-
pnpm run dev:example:vue
|
|
197
|
-
|
|
198
|
-
# 构建示例工程
|
|
199
|
-
pnpm run build:example:react
|
|
200
|
-
pnpm run build:example:vue
|
|
201
|
-
```
|
|
223
|
+
项目内置了 `example/` 目录用于开发调试,支持实时编辑 Schema 并查看渲染效果:
|
|
202
224
|
|
|
203
|
-
|
|
225
|
+
- `pnpm run dev:example`:同时启动 React 和 Vue 的 Demo 预览。
|
|
226
|
+
- `pnpm run dev:example:react`:仅启动 React Demo。
|
|
227
|
+
- `pnpm run dev:example:vue`:仅启动 Vue Demo。
|
package/package.json
CHANGED
|
@@ -9,11 +9,17 @@
|
|
|
9
9
|
"json-schema",
|
|
10
10
|
"form-builder"
|
|
11
11
|
],
|
|
12
|
+
"files": [
|
|
13
|
+
"README.md",
|
|
14
|
+
"packages/core/dist/**",
|
|
15
|
+
"packages/react/dist/**",
|
|
16
|
+
"packages/vue/dist/**"
|
|
17
|
+
],
|
|
12
18
|
"exports": {
|
|
13
19
|
"./core": {
|
|
14
20
|
"types": "./packages/core/dist/index.d.ts",
|
|
15
|
-
"import": "./packages/core/dist/index.
|
|
16
|
-
"require": "./packages/core/dist/index.
|
|
21
|
+
"import": "./packages/core/dist/index.js",
|
|
22
|
+
"require": "./packages/core/dist/index.cjs"
|
|
17
23
|
},
|
|
18
24
|
"./react": {
|
|
19
25
|
"types": "./packages/react/dist/index.d.ts",
|
|
@@ -51,7 +57,7 @@
|
|
|
51
57
|
"vitest": "^1.0.0",
|
|
52
58
|
"vue": "^3.5.0"
|
|
53
59
|
},
|
|
54
|
-
"version": "2.0
|
|
60
|
+
"version": "2.2.0",
|
|
55
61
|
"scripts": {
|
|
56
62
|
"build:all": "turbo run build",
|
|
57
63
|
"dev:all": "turbo run dev",
|
|
File without changes
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/utils.ts
|
|
2
|
+
function parseNumberish(value) {
|
|
3
|
+
if (typeof value === "number") return Number.isNaN(value) ? null : value;
|
|
4
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
5
|
+
const parsed = Number(value);
|
|
6
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
7
|
+
}
|
|
8
|
+
var defaultErrorMessages = {
|
|
9
|
+
required: "{label} is required",
|
|
10
|
+
min: "{label} must be at least {value}",
|
|
11
|
+
max: "{label} must be at most {value}",
|
|
12
|
+
email: "Invalid email address",
|
|
13
|
+
pattern: "Invalid format",
|
|
14
|
+
custom: "Invalid value"
|
|
15
|
+
};
|
|
16
|
+
function formatMessage(template, field, rule) {
|
|
17
|
+
return template.replace("{label}", field.label).replace("{value}", String(rule.value || ""));
|
|
18
|
+
}
|
|
19
|
+
function get(obj, path, defaultValue) {
|
|
20
|
+
if (!path) return defaultValue;
|
|
21
|
+
const keys = path.split(/[.[\]]/).filter(Boolean);
|
|
22
|
+
let result = obj;
|
|
23
|
+
for (const key of keys) {
|
|
24
|
+
if (result === null || result === void 0) return defaultValue;
|
|
25
|
+
result = result[key];
|
|
26
|
+
}
|
|
27
|
+
return result === void 0 ? defaultValue : result;
|
|
28
|
+
}
|
|
29
|
+
function set(obj, path, value) {
|
|
30
|
+
if (Object(obj) !== obj) return obj;
|
|
31
|
+
const keys = path.split(/[.[\]]/).filter(Boolean);
|
|
32
|
+
const newObj = { ...obj };
|
|
33
|
+
let current = newObj;
|
|
34
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
35
|
+
const key = keys[i];
|
|
36
|
+
const nextKey = keys[i + 1];
|
|
37
|
+
const isNextKeyIndex = /^\d+$/.test(nextKey);
|
|
38
|
+
if (!(key in current) || current[key] === null || typeof current[key] !== "object") {
|
|
39
|
+
current[key] = isNextKeyIndex ? [] : {};
|
|
40
|
+
} else {
|
|
41
|
+
current[key] = Array.isArray(current[key]) ? [...current[key]] : { ...current[key] };
|
|
42
|
+
}
|
|
43
|
+
current = current[key];
|
|
44
|
+
}
|
|
45
|
+
current[keys[keys.length - 1]] = value;
|
|
46
|
+
return newObj;
|
|
47
|
+
}
|
|
48
|
+
function normalizeFieldValue(field, value) {
|
|
49
|
+
if (field.type !== "number") return value;
|
|
50
|
+
if (value === "" || value === void 0 || value === null) return "";
|
|
51
|
+
const numericValue = parseNumberish(value);
|
|
52
|
+
return numericValue === null ? value : numericValue;
|
|
53
|
+
}
|
|
54
|
+
async function validateField(value, field, customMessages) {
|
|
55
|
+
if (!field.validations) return null;
|
|
56
|
+
const messages = { ...defaultErrorMessages, ...customMessages };
|
|
57
|
+
for (const rule of field.validations) {
|
|
58
|
+
switch (rule.type) {
|
|
59
|
+
case "required":
|
|
60
|
+
if (value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0) {
|
|
61
|
+
return rule.message || formatMessage(messages.required, field, rule);
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case "min":
|
|
65
|
+
if (field.type === "number") {
|
|
66
|
+
const numericValue = parseNumberish(value);
|
|
67
|
+
if (numericValue !== null && numericValue < rule.value) {
|
|
68
|
+
const template = field.type === "number" ? messages.min : typeof value === "string" ? "{label} must be at least {value} characters" : messages.min;
|
|
69
|
+
return rule.message || formatMessage(template, field, rule);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === "number" && value < rule.value) {
|
|
74
|
+
return rule.message || formatMessage(messages.min, field, rule);
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === "string" && value.length < rule.value) {
|
|
77
|
+
const template = "{label} must be at least {value} characters";
|
|
78
|
+
return rule.message || formatMessage(template, field, rule);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
case "max":
|
|
82
|
+
if (field.type === "number") {
|
|
83
|
+
const numericValue = parseNumberish(value);
|
|
84
|
+
if (numericValue !== null && numericValue > rule.value) {
|
|
85
|
+
return rule.message || formatMessage(messages.max, field, rule);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (typeof value === "number" && value > rule.value) {
|
|
90
|
+
return rule.message || formatMessage(messages.max, field, rule);
|
|
91
|
+
}
|
|
92
|
+
if (typeof value === "string" && value.length > rule.value) {
|
|
93
|
+
const template = "{label} must be at most {value} characters";
|
|
94
|
+
return rule.message || formatMessage(template, field, rule);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case "email": {
|
|
98
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
99
|
+
if (value && !emailRegex.test(value)) {
|
|
100
|
+
return rule.message || formatMessage(messages.email, field, rule);
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case "pattern":
|
|
105
|
+
if (value && rule.value && !new RegExp(rule.value).test(value)) {
|
|
106
|
+
return rule.message || formatMessage(messages.pattern, field, rule);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case "custom":
|
|
110
|
+
if (rule.validator) {
|
|
111
|
+
const result = await rule.validator(value);
|
|
112
|
+
if (typeof result === "string") return result;
|
|
113
|
+
if (result === false) return rule.message || formatMessage(messages.custom, field, rule);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
async function validateFieldByName(fields, name, value, resolver, allValues, customMessages) {
|
|
121
|
+
if (resolver && allValues) {
|
|
122
|
+
const resolverErrors = await resolver(allValues);
|
|
123
|
+
if (resolverErrors[name]) return resolverErrors[name];
|
|
124
|
+
}
|
|
125
|
+
const field = fields.find((f) => f.name === name);
|
|
126
|
+
if (!field) return null;
|
|
127
|
+
return await validateField(value, field, customMessages);
|
|
128
|
+
}
|
|
129
|
+
async function validateForm(fields, values, resolver, customMessages) {
|
|
130
|
+
let errors = {};
|
|
131
|
+
if (resolver) {
|
|
132
|
+
errors = await resolver(values);
|
|
133
|
+
}
|
|
134
|
+
const validationPromises = fields.map(async (field) => {
|
|
135
|
+
if (errors[field.name]) return;
|
|
136
|
+
const error = await validateField(get(values, field.name), field, customMessages);
|
|
137
|
+
if (error) errors[field.name] = error;
|
|
138
|
+
});
|
|
139
|
+
await Promise.all(validationPromises);
|
|
140
|
+
return errors;
|
|
141
|
+
}
|
|
142
|
+
function getDefaultValues(fields) {
|
|
143
|
+
return fields.reduce((acc, field) => {
|
|
144
|
+
acc[field.name] = field.defaultValue !== void 0 ? field.defaultValue : field.type === "checkbox" ? [] : "";
|
|
145
|
+
return acc;
|
|
146
|
+
}, {});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
defaultErrorMessages,
|
|
151
|
+
get,
|
|
152
|
+
set,
|
|
153
|
+
normalizeFieldValue,
|
|
154
|
+
validateField,
|
|
155
|
+
validateFieldByName,
|
|
156
|
+
validateForm,
|
|
157
|
+
getDefaultValues
|
|
158
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
get,
|
|
3
|
+
getDefaultValues,
|
|
4
|
+
normalizeFieldValue,
|
|
5
|
+
set,
|
|
6
|
+
validateFieldByName,
|
|
7
|
+
validateForm
|
|
8
|
+
} from "./chunk-KA6QUMVR.js";
|
|
9
|
+
|
|
10
|
+
// src/formState.ts
|
|
11
|
+
import { createStore } from "zustand/vanilla";
|
|
12
|
+
function createFormStore(fields, resolver, errorMessages) {
|
|
13
|
+
return createStore()((set2, getStore) => ({
|
|
14
|
+
values: getDefaultValues(fields),
|
|
15
|
+
errors: {},
|
|
16
|
+
validatingFields: [],
|
|
17
|
+
isSubmitting: false,
|
|
18
|
+
setFieldValue: async (name, rawValue) => {
|
|
19
|
+
const field = fields.find((f) => f.name === name);
|
|
20
|
+
const normalizedValue = field ? normalizeFieldValue(field, rawValue) : rawValue;
|
|
21
|
+
set2((state) => ({
|
|
22
|
+
values: set(state.values, name, normalizedValue),
|
|
23
|
+
validatingFields: [...state.validatingFields, name]
|
|
24
|
+
}));
|
|
25
|
+
try {
|
|
26
|
+
const currentValues = getStore().values;
|
|
27
|
+
const error = await validateFieldByName(fields, name, normalizedValue, resolver, currentValues, errorMessages);
|
|
28
|
+
set2((state) => ({
|
|
29
|
+
errors: { ...state.errors, [name]: error || "" },
|
|
30
|
+
validatingFields: state.validatingFields.filter((f) => f !== name)
|
|
31
|
+
}));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
set2((state) => ({
|
|
34
|
+
validatingFields: state.validatingFields.filter((f) => f !== name)
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
setFieldBlur: async (name) => {
|
|
39
|
+
set2((state) => ({
|
|
40
|
+
validatingFields: [...state.validatingFields, name]
|
|
41
|
+
}));
|
|
42
|
+
try {
|
|
43
|
+
const currentValues = getStore().values;
|
|
44
|
+
const value = get(currentValues, name);
|
|
45
|
+
const error = await validateFieldByName(fields, name, value, resolver, currentValues, errorMessages);
|
|
46
|
+
set2((state) => ({
|
|
47
|
+
errors: { ...state.errors, [name]: error || "" },
|
|
48
|
+
validatingFields: state.validatingFields.filter((f) => f !== name)
|
|
49
|
+
}));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
set2((state) => ({
|
|
52
|
+
validatingFields: state.validatingFields.filter((f) => f !== name)
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
setSubmitting: (isSubmitting) => set2({ isSubmitting }),
|
|
57
|
+
runSubmitValidation: async () => {
|
|
58
|
+
set2({ isSubmitting: true });
|
|
59
|
+
const state = getStore();
|
|
60
|
+
const errors = await validateForm(fields, state.values, resolver, errorMessages);
|
|
61
|
+
const hasError = Object.keys(errors).length > 0;
|
|
62
|
+
set2({
|
|
63
|
+
errors,
|
|
64
|
+
isSubmitting: false
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
state: getStore(),
|
|
68
|
+
hasError
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
createFormStore
|
|
76
|
+
};
|