pdyform 2.1.0 → 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 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
- - **React & Vue Support**: Out-of-the-box UI components for React (built on Shadcn UI) and Vue (built on Shadcn-vue).
20
- - **High Performance**: Optimized rendering to prevent unnecessary re-renders on complex forms.
21
- - **100% Test Coverage**: Core logic and UI components are fully unit-tested with a 100% pass rate.
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 based on Shadcn UI.
29
- - `pdyform-vue`: Vue components based on Shadcn-vue.
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
- npm install pdyform
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/react';
50
- import type { FormSchema } from 'pdyform/core';
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
- { name: 'username', label: 'Username', type: 'text', required: true },
55
- { name: 'email', label: 'Email', type: 'text', required: true, rules: { pattern: '\\S+@\\S+\\.\\S+' } }
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/vue';
69
- import type { FormSchema } from 'pdyform/core';
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
- { name: 'username', label: 'Username', type: 'text', required: true },
74
- { name: 'email', label: 'Email', type: 'text', required: true, rules: { pattern: '\\S+@\\S+\\.\\S+' } }
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 = (data: any) => console.log(data);
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
- - **框架无关核心**: 核心逻辑完全独立于任何 UI 框架,极其轻量化。
96
- - **支持 React & Vue**: 开箱即用的 UI 组件,React 版本基于 Shadcn UI,Vue 版本基于 Shadcn-vue。
97
- - **高性能**: 渲染优化机制,避免复杂表单场景下的无效重渲染。
98
- - **可靠的测试覆盖率**: 核心逻辑与 UI 层均经过严格的单元测试,用例通过率 100%。
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`: 基于 Shadcn UI React 动态表单组件。
106
- - `pdyform-vue`: 基于 Shadcn-vue Vue 动态表单组件。
127
+ - `pdyform-react`: 基于 React 的动态表单组件与 `useForm` Hook。
128
+ - `pdyform-vue`: 基于 Vue 的动态表单组件与 `useForm` 组合式 API。
107
129
 
108
130
  ### 🚀 安装
109
131
 
110
132
  ```bash
111
- npm install pdyform
112
- #
113
- pnpm add pdyform
133
+ # 安装核心包和对应的框架包
134
+ pnpm add pdyform-core pdyform-react # React 项目
114
135
  # 或
115
- yarn add pdyform
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/react';
127
- import type { FormSchema } from 'pdyform/core';
145
+ import { DynamicForm } from 'pdyform-react';
146
+ import type { FormSchema } from 'pdyform-core';
128
147
 
129
148
  const schema: FormSchema = {
130
149
  fields: [
131
- { name: 'username', label: '用户名', type: 'text', required: true },
132
- { name: 'email', label: '邮箱', type: 'text', required: true, rules: { pattern: '\\S+@\\S+\\.\\S+' } }
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/vue';
146
- import type { FormSchema } from 'pdyform/core';
184
+ import { DynamicForm } from 'pdyform-vue';
185
+ import type { FormSchema } from 'pdyform-core';
147
186
 
148
187
  const schema: FormSchema = {
149
188
  fields: [
150
- { name: 'username', label: '用户名', type: 'text', required: true },
151
- { name: 'email', label: '邮箱', type: 'text', required: true, rules: { pattern: '\\S+@\\S+\\.\\S+' } }
189
+ {
190
+ name: 'nickname',
191
+ label: '昵称',
192
+ type: 'text',
193
+ validations: [{ type: 'required', message: '昵称不能为空' }]
194
+ }
152
195
  ]
153
196
  };
154
197
 
155
- const handleSubmit = (data: any) => console.log(data);
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
- # Install dependencies / 安装依赖
211
+ # 安装依赖
169
212
  pnpm install
170
213
 
171
- # Build all packages / 编译所有包
214
+ # 编译所有包
172
215
  pnpm run build:all
173
216
 
174
- # Run all tests / 运行所有单元测试
217
+ # 运行所有单元测试
175
218
  pnpm run test:all
176
219
  ```
177
220
 
178
221
  ## Examples / 示例工程
179
222
 
180
- 新增 `example/` 目录用于本地开发调试(实时编辑 schema,验证 React/Vue 渲染与提交结果):
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
- 更多说明见 `example/README.md`。
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
@@ -57,7 +57,7 @@
57
57
  "vitest": "^1.0.0",
58
58
  "vue": "^3.5.0"
59
59
  },
60
- "version": "2.1.0",
60
+ "version": "2.2.0",
61
61
  "scripts": {
62
62
  "build:all": "turbo run build",
63
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
+ };