@fidt/dynamic-form 0.0.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 ADDED
@@ -0,0 +1,458 @@
1
+ # @fidt/dynamic-form
2
+
3
+ Thư viện dynamic form cho Vue 3 với hỗ trợ validation tích hợp (Zod, ArkType).
4
+
5
+ ## ✨ Tính năng
6
+
7
+ - 🎯 **Dynamic Form Generation** - Tạo form từ schema JSON
8
+ - 📐 **Flexible Layout** - Hỗ trợ grid layout đa cột và nested fields
9
+ - ✅ **Built-in Validation** - Validation tích hợp với Zod/ArkType
10
+ - 🎨 **UI Component Agnostic** - Tương thích với bất kỳ UI component nào
11
+ - 📦 **Type-safe** - Fully typed với TypeScript
12
+ - 🔄 **Reactive** - Tích hợp Vue reactivity system
13
+
14
+ ## 📦 Cài đặt
15
+
16
+ ```bash
17
+ npm install @fidt/dynamic-form
18
+ # hoặc
19
+ pnpm add @fidt/dynamic-form
20
+ ```
21
+
22
+ ## 🚀 Sử dụng cơ bản
23
+
24
+ ### 1. Import và đăng ký
25
+
26
+ ```typescript
27
+ import DymikForm from '@fidt/dynamic-form'
28
+ import '@fidt/dynamic-form/dist/dynamic-form.css'
29
+
30
+ // Đăng ký global (optional)
31
+ app.use(DymikForm)
32
+
33
+ // Hoặc import trực tiếp trong component
34
+ import { DynamicForm, FormModel } from '@fidt/dynamic-form'
35
+ ```
36
+
37
+ ### 2. Tạo form đơn giản
38
+
39
+ ```vue
40
+ <script setup lang="ts">
41
+ import { reactive } from 'vue'
42
+ import { DynamicForm, FormModel } from '@fidt/dynamic-form'
43
+
44
+ const form = reactive(
45
+ new FormModel({
46
+ name: 'contact-form',
47
+ description: 'Form liên hệ đơn giản',
48
+ fields: [
49
+ {
50
+ name: 'fullName',
51
+ label: 'Họ và tên',
52
+ type: 'FInput',
53
+ required: true,
54
+ required_text: 'Vui lòng nhập họ tên',
55
+ props: { placeholder: 'Nhập họ và tên' },
56
+ validation_rules: [
57
+ { type: 'minLength', value: 2, message: 'Tên phải có ít nhất 2 ký tự' }
58
+ ]
59
+ },
60
+ {
61
+ name: 'email',
62
+ label: 'Email',
63
+ type: 'FInput',
64
+ required: true,
65
+ props: { placeholder: 'your@email.com' },
66
+ validation_rules: [
67
+ { type: 'email', message: 'Email không hợp lệ' }
68
+ ]
69
+ },
70
+ {
71
+ name: 'submit',
72
+ type: 'FButton',
73
+ role: 'submit',
74
+ props: { label: 'Gửi', order: 'primary' }
75
+ }
76
+ ]
77
+ })
78
+ )
79
+
80
+ function onSubmit(values: Record<string, unknown>) {
81
+ console.log('Form data:', values)
82
+ }
83
+ </script>
84
+
85
+ <template>
86
+ <DynamicForm :form="form" @submit="onSubmit" />
87
+ </template>
88
+ ```
89
+
90
+ ## 📐 Layout nâng cao
91
+
92
+ ### Grid Layout với nhiều cột
93
+
94
+ ```typescript
95
+ const form = reactive(
96
+ new FormModel({
97
+ name: 'registration-form',
98
+ columns: 3, // Số cột trong grid
99
+ layout: [
100
+ ['fullName', 'email', 'phone'], // Hàng 1: 3 fields
101
+ [['idCard', 'issueDate'], 'address', null], // Hàng 2: nested fields, 1 field, 1 ô trống
102
+ [null, null, 'submit'], // Hàng 3: button căn phải
103
+ ],
104
+ fields: {
105
+ fullName: {
106
+ label: 'Họ và tên',
107
+ type: 'FInput',
108
+ required: true,
109
+ props: { placeholder: 'Nhập họ tên' }
110
+ },
111
+ email: {
112
+ label: 'Email',
113
+ type: 'FInput',
114
+ required: true,
115
+ validation_rules: [{ type: 'email', message: 'Email không hợp lệ' }]
116
+ },
117
+ phone: {
118
+ label: 'Số điện thoại',
119
+ type: 'FInput',
120
+ props: { placeholder: '0123456789' }
121
+ },
122
+ idCard: {
123
+ label: 'CCCD',
124
+ type: 'FInput',
125
+ required: true,
126
+ props: { placeholder: 'Số CCCD' }
127
+ },
128
+ issueDate: {
129
+ label: 'Ngày cấp',
130
+ type: 'FInput',
131
+ props: { type: 'date' }
132
+ },
133
+ address: {
134
+ label: 'Địa chỉ',
135
+ type: 'FInput',
136
+ props: { placeholder: 'Địa chỉ hiện tại' }
137
+ },
138
+ submit: {
139
+ type: 'FButton',
140
+ role: 'submit',
141
+ props: { label: 'Đăng ký', order: 'primary' }
142
+ }
143
+ }
144
+ })
145
+ )
146
+ ```
147
+
148
+ ### Giải thích Layout
149
+
150
+ - **`columns`**: Số cột trong grid (mặc định: auto)
151
+ - **`layout`**: Mảng 2D định nghĩa vị trí các field
152
+ - `'fieldName'` - Field đơn chiếm 1 ô
153
+ - `['field1', 'field2']` - Nhiều field nested trong 1 ô
154
+ - `null` - Ô trống (dùng để căn chỉnh)
155
+
156
+ ### Format Fields
157
+
158
+ Có 2 cách định nghĩa fields:
159
+
160
+ **1. Array format** (truyền thống):
161
+ ```typescript
162
+ fields: [
163
+ {
164
+ name: 'email',
165
+ label: 'Email',
166
+ type: 'FInput',
167
+ // ...
168
+ }
169
+ ]
170
+ ```
171
+
172
+ **2. Object format** (recommended với layout):
173
+ ```typescript
174
+ fields: {
175
+ email: {
176
+ label: 'Email',
177
+ type: 'FInput',
178
+ // ...
179
+ }
180
+ // name tự động lấy từ key
181
+ }
182
+ ```
183
+
184
+ ## ✅ Validation
185
+
186
+ ### Các loại validation được hỗ trợ
187
+
188
+ ```typescript
189
+ validation_rules: [
190
+ // String validation
191
+ { type: 'string', message: 'Phải là chuỗi' },
192
+ { type: 'minLength', value: 3, message: 'Tối thiểu 3 ký tự' },
193
+ { type: 'maxLength', value: 50, message: 'Tối đa 50 ký tự' },
194
+
195
+ // Number validation
196
+ { type: 'number', message: 'Phải là số' },
197
+ { type: 'min', value: 18, message: 'Tối thiểu 18' },
198
+ { type: 'max', value: 100, message: 'Tối đa 100' },
199
+
200
+ // Special formats
201
+ { type: 'email', message: 'Email không hợp lệ' },
202
+ { type: 'url', message: 'URL không hợp lệ' },
203
+ { type: 'regex', value: /^\d{9,12}$/, message: 'CCCD không đúng định dạng' },
204
+
205
+ // Custom validation
206
+ {
207
+ type: 'custom',
208
+ value: (value, formValues) => value !== formValues.password,
209
+ message: 'Mật khẩu không khớp'
210
+ }
211
+ ]
212
+ ```
213
+
214
+ ### Custom Validator
215
+
216
+ ```typescript
217
+ import { ValidatorUtils } from '@fidt/dynamic-form'
218
+
219
+ ValidatorUtils.addCustomValidator('isAdult', (rule, value) => {
220
+ const age = parseInt(value)
221
+ return age >= 18
222
+ })
223
+
224
+ // Sử dụng
225
+ validation_rules: [
226
+ { type: 'isAdult', message: 'Phải từ 18 tuổi trở lên' }
227
+ ]
228
+ ```
229
+
230
+ ## 🎯 FormModel API
231
+
232
+ ### Properties
233
+
234
+ ```typescript
235
+ interface FormItem {
236
+ name: string // Tên form
237
+ description?: string // Mô tả
238
+ id?: string // ID duy nhất
239
+ fields: FormField[] // Danh sách fields
240
+ columns?: number // Số cột grid
241
+ layout?: (string | null | string[])[][] // Layout grid
242
+ css_classes?: string // CSS classes tùy chỉnh
243
+ disabled?: boolean // Disable toàn bộ form
244
+ invalid?: boolean // Trạng thái invalid (readonly)
245
+ }
246
+ ```
247
+
248
+ ### Methods
249
+
250
+ ```typescript
251
+ // Validation
252
+ form.validate(): boolean // Validate toàn bộ form
253
+ form.validateField(name, value): boolean // Validate 1 field
254
+
255
+ // Get/Set values
256
+ form.getFormValue(): Record<string, any> // Lấy tất cả giá trị
257
+ form.setFormValue(values: Record<string, any>): void // Set nhiều giá trị
258
+ form.setFieldValue(name: string, value: any): void // Set 1 giá trị
259
+
260
+ // Errors
261
+ form.getFormErrors(): Record<string, string> // Lấy tất cả lỗi
262
+ form.getFormError(name: string): string | undefined // Lấy lỗi của 1 field
263
+ form.setFormError(name: string, error: string): void // Set lỗi cho 1 field
264
+
265
+ // Reset
266
+ form.reset(): void // Reset form về trạng thái ban đầu
267
+ ```
268
+
269
+ ## 🎨 Events
270
+
271
+ ```vue
272
+ <DynamicForm
273
+ :form="form"
274
+ @submit="onSubmit"
275
+ @value-change="onValueChange"
276
+ @loading="onLoading"
277
+ @submit-result="onSubmitResult"
278
+ />
279
+ ```
280
+
281
+ ### Event handlers
282
+
283
+ ```typescript
284
+ function onSubmit(values: Record<string, unknown>) {
285
+ // Được gọi khi form submit và validation pass
286
+ console.log('Submit data:', values)
287
+ }
288
+
289
+ function onValueChange(values: Record<string, unknown>) {
290
+ // Được gọi mỗi khi có field thay đổi
291
+ console.log('Current form values:', values)
292
+ }
293
+
294
+ function onLoading(isLoading: boolean) {
295
+ // Trạng thái loading khi submit
296
+ console.log('Loading:', isLoading)
297
+ }
298
+
299
+ function onSubmitResult(result: any) {
300
+ // Kết quả sau khi submit (nếu có saulFunctionName)
301
+ console.log('Result:', result)
302
+ }
303
+ ```
304
+
305
+ ## 🎛️ Field Configuration
306
+
307
+ ### FormField Interface
308
+
309
+ ```typescript
310
+ interface FormField {
311
+ name: string // Tên field (unique)
312
+ label?: string // Label hiển thị
313
+ type: string // Component type (VD: 'FInput', 'FButton')
314
+ role?: 'submit' | 'reset' | 'field' // Vai trò của field
315
+ required?: boolean // Bắt buộc
316
+ disabled?: boolean // Disable
317
+ required_text?: string // Thông báo lỗi khi required
318
+ props: any // Props truyền cho component
319
+ error?: string // Error message (readonly)
320
+ classes?: string // CSS classes
321
+ value?: any // Giá trị hiện tại
322
+ validation_rules?: ValidationRule[] // Quy tắc validation
323
+ }
324
+ ```
325
+
326
+ ### Ví dụ Field types
327
+
328
+ ```typescript
329
+ // Input
330
+ {
331
+ name: 'username',
332
+ type: 'FInput',
333
+ props: { placeholder: 'Enter username', type: 'text' }
334
+ }
335
+
336
+ // Button
337
+ {
338
+ name: 'submit',
339
+ type: 'FButton',
340
+ role: 'submit',
341
+ props: { label: 'Submit', order: 'primary', size: 'large' }
342
+ }
343
+
344
+ // Select (tùy component library)
345
+ {
346
+ name: 'country',
347
+ type: 'FSelect',
348
+ props: {
349
+ options: [
350
+ { label: 'Vietnam', value: 'vn' },
351
+ { label: 'USA', value: 'us' }
352
+ ]
353
+ }
354
+ }
355
+ ```
356
+
357
+ ## 🎨 Custom Styling
358
+
359
+ ### CSS Classes
360
+
361
+ ```scss
362
+ .dymik-form {
363
+ // Form container
364
+
365
+ &.layout-mode {
366
+ // Grid layout mode
367
+ display: grid;
368
+ grid-template-columns: repeat(var(--columns), 1fr);
369
+ gap: 1rem;
370
+ }
371
+
372
+ .field {
373
+ // Field wrapper
374
+
375
+ label {
376
+ .required {
377
+ color: red;
378
+ }
379
+ }
380
+
381
+ .error {
382
+ color: red;
383
+ font-size: 0.875rem;
384
+ }
385
+ }
386
+
387
+ .field-group {
388
+ // Nested fields container
389
+
390
+ .nested-fields {
391
+ display: flex;
392
+ gap: 0.5rem;
393
+ }
394
+ }
395
+ }
396
+ ```
397
+
398
+ ### Custom CSS Classes
399
+
400
+ ```typescript
401
+ const form = new FormModel({
402
+ name: 'my-form',
403
+ css_classes: 'custom-form theme-dark', // Form-level classes
404
+ fields: [
405
+ {
406
+ name: 'email',
407
+ classes: 'highlighted-field', // Field-level classes
408
+ // ...
409
+ }
410
+ ]
411
+ })
412
+ ```
413
+
414
+ ## 🔌 Tích hợp với UI Components
415
+
416
+ Dynamic Form hoàn toàn component-agnostic. Bạn có thể sử dụng với bất kỳ UI library nào:
417
+
418
+ ```typescript
419
+ // Với FIDT UI Components
420
+ {
421
+ type: 'FInput',
422
+ props: { placeholder: 'Enter text' }
423
+ }
424
+
425
+ // Với Element Plus
426
+ {
427
+ type: 'el-input',
428
+ props: { placeholder: 'Enter text' }
429
+ }
430
+
431
+ // Với Ant Design Vue
432
+ {
433
+ type: 'a-input',
434
+ props: { placeholder: 'Enter text' }
435
+ }
436
+
437
+ // Hoặc custom component của bạn
438
+ {
439
+ type: 'MyCustomInput',
440
+ props: { /* custom props */ }
441
+ }
442
+ ```
443
+
444
+ **Lưu ý**: Component phải hỗ trợ `v-model` và emit `value-change` event.
445
+
446
+ ## 📚 Ví dụ hoàn chỉnh
447
+
448
+ Xem thêm các ví dụ trong thư mục `examples/`:
449
+ - [ContactForm.vue](./examples/with-fidt-ui-component/src/pages/ContactForm.vue) - Form liên hệ đơn giản
450
+ - [ThreeColumnForm.vue](./examples/with-fidt-ui-component/src/pages/ThreeColumnForm.vue) - Form đăng ký 3 cột với nested fields
451
+
452
+ ## 🤝 Contributing
453
+
454
+ Contributions, issues và feature requests đều được chào đón!
455
+
456
+ ## 📝 License
457
+
458
+ MIT © fidt
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@fidt/dynamic-form",
3
+ "version": "0.0.0",
4
+ "description": "A dynamic form library for Vue 3 with built-in validation support (Zod, ArkType)",
5
+ "keywords": [
6
+ "vue",
7
+ "vue3",
8
+ "form",
9
+ "dynamic-form",
10
+ "validation",
11
+ "zod",
12
+ "arktype"
13
+ ],
14
+ "author": "fidt",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": ""
19
+ },
20
+ "homepage": "",
21
+ "module": "dist/index.es.js",
22
+ "types": "dist/index.d.ts",
23
+ "files": [
24
+ "dist",
25
+ "src",
26
+ "package.json",
27
+ "README.md"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.es.js"
33
+ },
34
+ "./dist/dynamic-form.css": "./dist/dynamic-form.css"
35
+ },
36
+ "scripts": {
37
+ "build": "vite build"
38
+ },
39
+ "peerDependencies": {
40
+ "vue": "^3.5.13"
41
+ },
42
+ "dependencies": {
43
+ "@standard-schema/spec": "^1.0.0",
44
+ "arktype": "^2.1.20",
45
+ "zod": "^3.24.2"
46
+ },
47
+ "sideEffects": [
48
+ "*.vue",
49
+ "*.scss"
50
+ ],
51
+ "devDependencies": {
52
+ "@vitejs/plugin-vue": "^5.2.1",
53
+ "sass-embedded": "^1.98.0",
54
+ "typescript": "^5.0.0",
55
+ "vite": "^6.2.0",
56
+ "vite-plugin-dts": "^4.5.3",
57
+ "vitest": "^1.0.0"
58
+ }
59
+ }
@@ -0,0 +1,229 @@
1
+ <template>
2
+ <div class="dymik-form" :class="[props.form.css_classes, { 'layout-mode': hasLayout }]"
3
+ :style="hasLayout && props.form.columns ? { '--columns': props.form.columns } : {}">
4
+
5
+ <!-- Layout-based rendering -->
6
+ <template v-if="hasLayout">
7
+ <template v-for="(row, rowIndex) in props.form.layout" :key="`row-${rowIndex}`">
8
+ <div v-for="(cell, colIndex) in row" :key="`${rowIndex}-${colIndex}`"
9
+ class="field" :class="Array.isArray(cell) ? 'field-group' : getFieldByName(cell as string | null)?.classes">
10
+
11
+ <!-- Nested fields (array of field names) -->
12
+ <template v-if="Array.isArray(cell)">
13
+ <div class="nested-fields">
14
+ <div v-for="fieldName in cell" :key="fieldName"
15
+ class="nested-field" :class="getFieldByName(fieldName)?.classes">
16
+ <template v-if="getFieldByName(fieldName)">
17
+ <label v-if="getFieldByName(fieldName)!.label" :for="getFieldByName(fieldName)!.name">
18
+ {{ getFieldByName(fieldName)!.label }}
19
+ <span v-if="getFieldByName(fieldName)!.required" class="required">*</span>
20
+ </label>
21
+ <component
22
+ v-model="getFieldByName(fieldName)!.value"
23
+ :is="getFieldByName(fieldName)!.type"
24
+ v-bind="getFieldByName(fieldName)!.props"
25
+ :key="getFieldByName(fieldName)!.name"
26
+ :invalid="!!getFieldByName(fieldName)!.error"
27
+ @value-change="(value: any) => onValueChanged(getFieldByName(fieldName)!.name, value)"
28
+ @input="(event: any) => onNativeInput(getFieldByName(fieldName)!.name, event)"
29
+ :disabled="form.disabled || getFieldByName(fieldName)!.disabled"
30
+ @click="(event: any) => onFieldClick(getFieldByName(fieldName)!, event)" />
31
+ <span v-if="!!getFieldByName(fieldName)!.error" class="error">
32
+ {{ getFieldByName(fieldName)!.error }}
33
+ </span>
34
+ </template>
35
+ </div>
36
+ </div>
37
+ </template>
38
+
39
+ <!-- Single field -->
40
+ <template v-else-if="getFieldByName(cell as string | null)">
41
+ <label v-if="getFieldByName(cell as string | null)!.label" :for="getFieldByName(cell as string | null)!.name">
42
+ {{ getFieldByName(cell as string | null)!.label }}
43
+ <span v-if="getFieldByName(cell as string | null)!.required" class="required">*</span>
44
+ </label>
45
+ <component
46
+ v-model="getFieldByName(cell as string | null)!.value"
47
+ :is="getFieldByName(cell as string | null)!.type"
48
+ v-bind="getFieldByName(cell as string | null)!.props"
49
+ :key="getFieldByName(cell as string | null)!.name"
50
+ :invalid="!!getFieldByName(cell as string | null)!.error"
51
+ @value-change="(value: any) => onValueChanged(getFieldByName(cell as string | null)!.name, value)"
52
+ @input="(event: any) => onNativeInput(getFieldByName(cell as string | null)!.name, event)"
53
+ :disabled="form.disabled || getFieldByName(cell as string | null)!.disabled"
54
+ @click="(event: any) => onFieldClick(getFieldByName(cell as string | null)!, event)" />
55
+ <span v-if="!!getFieldByName(cell as string | null)!.error" class="error">
56
+ {{ getFieldByName(cell as string | null)!.error }}
57
+ </span>
58
+ </template>
59
+ </div>
60
+ </template>
61
+ </template>
62
+
63
+ <!-- Legacy array-based rendering -->
64
+ <template v-else>
65
+ <div class="field" :class="field.classes" v-for="field of props.form.fields" :key="field.name">
66
+ <label v-if="field.label" :for="field.name">
67
+ {{ field.label }}
68
+ <span v-if="field.required" class="required">*</span>
69
+ </label>
70
+ <component v-model="field.value" :is="field.type" v-bind="field.props" :key="field.name"
71
+ :invalid="!!field.error" @value-change="(value: any) => onValueChanged(field.name, value)"
72
+ @input="(event: any) => onNativeInput(field.name, event)"
73
+ :disabled="form.disabled || field.disabled" @click="(event: any) => onFieldClick(field, event)" />
74
+ <span v-if="!!field.error" class="error">{{ field.error }}</span>
75
+ </div>
76
+ </template>
77
+ </div>
78
+ </template>
79
+ <script setup lang="ts">
80
+ import { ref, computed } from 'vue';
81
+ import FormModel from '../models/form.model';
82
+ import type { FormField } from '../interfaces';
83
+
84
+ const props = defineProps<{ form: FormModel }>();
85
+ const emit = defineEmits(['submit', 'value-change', 'loading', 'submit-result']);
86
+ const loading = ref(false);
87
+
88
+ const hasLayout = computed(() => {
89
+ return !!(props.form.layout && props.form.layout.length > 0);
90
+ });
91
+
92
+ function getFieldByName(fieldName: string | null): FormField | undefined {
93
+ if (!fieldName) return undefined;
94
+ return props.form.fields.find((f: FormField) => f.name === fieldName);
95
+ }
96
+
97
+ function onNativeInput(fieldName: string, event: any) {
98
+ if (event instanceof Event && event.target) {
99
+ onValueChanged(fieldName, (event.target as HTMLInputElement).value);
100
+ }
101
+ }
102
+
103
+ function onValueChanged(fieldName: string, value: any) {
104
+ const field = props.form.fields.find((f: FormField) => f.name === fieldName);
105
+
106
+ if (field) {
107
+ field.value = value;
108
+
109
+ emit('value-change', props.form.getFormValue());
110
+ }
111
+
112
+ props.form.validateField(fieldName, value);
113
+ }
114
+
115
+ async function onFieldClick(field: FormField, event: Event) {
116
+ if (field.role === 'submit') {
117
+ const isValid = props.form.validate();
118
+
119
+ if (!isValid) {
120
+ event.preventDefault();
121
+ return;
122
+ }
123
+
124
+ event.preventDefault();
125
+
126
+ if (props.form.saulFunctionName) {
127
+ loading.value = true;
128
+ emit('loading', true);
129
+
130
+ try {
131
+ await props.form.submitToEndpoint();
132
+ emit('submit-result', { message: 'Form submitted successfully!', type: 'success' });
133
+ } catch (error) {
134
+ console.error('Error:', error);
135
+ emit('submit-result', { message: 'Failed to submit form.', type: 'error' });
136
+ } finally {
137
+ loading.value = false;
138
+ emit('loading', false);
139
+ }
140
+ }
141
+
142
+ emit('submit', props.form.getFormValue());
143
+ }
144
+ }
145
+ </script>
146
+ <style scoped lang="scss">
147
+ .dymik-form {
148
+ display: flex;
149
+ gap: 16px;
150
+ flex-wrap: wrap;
151
+ padding: 1rem;
152
+
153
+ &.layout-mode {
154
+ display: grid;
155
+ grid-template-columns: repeat(var(--columns, 1), 1fr);
156
+ gap: 16px;
157
+
158
+ @media (max-width: 1023px) {
159
+ grid-template-columns: repeat(2, 1fr);
160
+ }
161
+
162
+ @media (max-width: 767px) {
163
+ grid-template-columns: 1fr;
164
+ }
165
+ }
166
+
167
+ .field {
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 0.5rem;
171
+
172
+ &.field-group {
173
+ gap: 0;
174
+ }
175
+
176
+ .nested-fields {
177
+ display: flex;
178
+ gap: 8px;
179
+ width: 100%;
180
+ }
181
+
182
+ .nested-field {
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: 0.5rem;
186
+ flex: 1;
187
+ min-width: 0;
188
+ }
189
+
190
+ .required {
191
+ color: red;
192
+ margin-left: 0.25rem;
193
+ }
194
+
195
+ .error {
196
+ color: red;
197
+ font-size: 0.875rem;
198
+ margin-top: 0.25rem;
199
+ }
200
+
201
+ }
202
+
203
+ .loading-content {
204
+ display: flex;
205
+ justify-content: center;
206
+ align-items: center;
207
+ font-size: 1.5rem;
208
+ color: white;
209
+ }
210
+ }
211
+ </style>
212
+
213
+ <style lang="scss">
214
+ .dymik-form {
215
+ .field {
216
+ &.full_width {
217
+ width: 100%;
218
+ }
219
+
220
+ &.half_width {
221
+ width: calc(50% - 8px);
222
+ }
223
+
224
+ &.third_width {
225
+ width: calc(33.333% - 11px);
226
+ }
227
+ }
228
+ }
229
+ </style>
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export * from './interfaces';
2
+ export * from './models/form.model';
3
+ export * from './utils/validator';
4
+
5
+ export { default as FormModel } from './models/form.model';
6
+ export { default as ValidatorUtils } from './utils/validator';
7
+
8
+ import DynamicForm from './components/DynamicForm.vue';
9
+
10
+
11
+ export default {
12
+ DynamicForm,
13
+ install(app: any) {
14
+ app.component('DynamicForm', DynamicForm);
15
+ },
16
+ };
@@ -0,0 +1,64 @@
1
+ import type { StandardSchemaV1 as StandardSchema } from "@standard-schema/spec";
2
+
3
+ export interface FormListItem {
4
+ name: string;
5
+ description?: string;
6
+ id: string;
7
+ }
8
+
9
+ export interface FormItem {
10
+ name: string;
11
+ description?: string;
12
+ id?: string;
13
+ fields: FormField[] | Record<string, Omit<FormField, 'name'>>;
14
+ columns?: number;
15
+ layout?: (string | null | string[])[][];
16
+ css_classes?: string;
17
+ saulFunctionName?: string;
18
+ invalid?: boolean;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ export interface FormField {
23
+ label?: string;
24
+ name: string;
25
+ type: string;
26
+ role?: 'submit' | 'reset' | 'field';
27
+ required?: boolean;
28
+ disabled?: boolean;
29
+ required_text?: string;
30
+ props: any;
31
+ error?: string;
32
+ classes?: string;
33
+ value?: any;
34
+ validation_rules?: ValidationRule[];
35
+ // Ensure ValidationRule is defined or imported
36
+ }
37
+
38
+ export interface ValidationRule {
39
+ type:
40
+ | 'string'
41
+ | 'number'
42
+ | 'boolean'
43
+ | 'date'
44
+ | 'regex'
45
+ | 'min'
46
+ | 'max'
47
+ | 'minLength'
48
+ | 'maxLength'
49
+ | 'email'
50
+ | 'url'
51
+ | 'custom';
52
+ message?: string;
53
+ value?: string | number | boolean | ((value: any, formValue: any) => boolean);
54
+ }
55
+
56
+
57
+ export interface IValidatorLib {
58
+
59
+ schemas: Record<ValidationRule['type'], () => StandardSchema> | {};
60
+
61
+ validate(rule: ValidationRule, value: any): boolean;
62
+
63
+ schemaFactory: (type: ValidationRule['type'], ruleValue: any) => StandardSchema;
64
+ }
@@ -0,0 +1,195 @@
1
+ import type { FormField, FormItem } from "../interfaces";
2
+ import ValidatorUtils from "../utils/validator";
3
+
4
+ export default class FormModel implements FormItem {
5
+ name: string;
6
+ description?: string | undefined;
7
+ id?: string;
8
+ fields: FormField[];
9
+ columns?: number;
10
+ layout?: (string | null | string[])[][];
11
+ css_classes?: string | undefined;
12
+ saulFunctionName?: string | undefined;
13
+ invalid: boolean;
14
+ disabled?: boolean;
15
+
16
+ constructor(form: FormItem) {
17
+ this.name = form.name;
18
+ this.id = form.id;
19
+ this.description = form.description;
20
+
21
+ // Normalize fields from object to array if needed
22
+ if (Array.isArray(form.fields)) {
23
+ this.fields = form.fields;
24
+ } else {
25
+ // Convert object format to array format
26
+ this.fields = Object.entries(form.fields).map(([name, field]) => ({
27
+ ...field,
28
+ name,
29
+ })) as FormField[];
30
+ }
31
+
32
+ // Initialize field.value from props.modelValue if value is not explicitly set
33
+ for (const field of this.fields) {
34
+ if (field.value === undefined && field.props?.modelValue !== undefined) {
35
+ field.value = field.props.modelValue;
36
+ }
37
+ }
38
+
39
+ this.columns = form.columns;
40
+ this.layout = form.layout;
41
+ this.css_classes = form.css_classes;
42
+ this.saulFunctionName = form.saulFunctionName;
43
+ this.invalid = false;
44
+ this.disabled = form.disabled || false;
45
+
46
+ // Validate layout if provided
47
+ if (this.layout) {
48
+ this.validateLayout();
49
+ }
50
+ }
51
+
52
+ public validateField(name: string, value: any): boolean {
53
+ const field = this.fields.find((field) => field.name === name);
54
+ if (field) {
55
+ field.error = ''; // Reset error message
56
+
57
+
58
+ if (field.required && !value) {
59
+ field.error = field.required_text || `${field.label} is required`;
60
+ this.invalid = true;
61
+ return false;
62
+ }
63
+
64
+ if (field.validation_rules) {
65
+
66
+ if (!field.required && !value) {
67
+ field.error = '';
68
+ return true;
69
+ }
70
+
71
+ for (const rule of field.validation_rules) {
72
+ const fieldValid = ValidatorUtils.validate(rule, value, this.getFormValue());
73
+
74
+ if (!fieldValid) {
75
+ field.error = rule.message || `Invalid value for ${field.label}`;
76
+ this.invalid = true;
77
+ return false;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ return field?.error ? false : true;
83
+ }
84
+
85
+ public validate(): boolean {
86
+ this.invalid = false;
87
+ for (const field of this.fields) {
88
+ if (!this.validateField(field.name, field.value)) {
89
+ this.invalid = true;
90
+ }
91
+ }
92
+ return !this.invalid;
93
+ }
94
+
95
+ public getFormValue(): Record<string, any> {
96
+ const formValue: Record<string, any> = {};
97
+ for (const field of this.fields) {
98
+ if (field.role === 'submit' || field.role === 'reset' || field.value === undefined) {
99
+ continue;
100
+ }
101
+ formValue[field.name] = field.value;
102
+ }
103
+ return formValue;
104
+ }
105
+
106
+ public setFormValue(value: Record<string, any>): void {
107
+ for (const field of this.fields) {
108
+ if (value.hasOwnProperty(field.name)) {
109
+ field.value = value[field.name];
110
+ }
111
+ }
112
+ }
113
+
114
+ public setFieldValue(name: string, value: any): void {
115
+ const field = this.fields.find((field) => field.name === name);
116
+ if (field) {
117
+ field.value = value;
118
+ }
119
+ }
120
+
121
+ public reset(): void {
122
+ for (const field of this.fields) {
123
+ field.value = undefined;
124
+ field.error = '';
125
+ }
126
+
127
+ this.invalid = false;
128
+ }
129
+
130
+ public async submitToEndpoint(): Promise<any> {
131
+ if (this.saulFunctionName) {
132
+
133
+ const formData = this.getFormValue();
134
+
135
+ console.log(`Submitting form to ${this.saulFunctionName} with data:`, formData);
136
+ // TODO: SaulFunctionCall Here
137
+
138
+ this.reset();
139
+
140
+ // if (!response.ok) {
141
+ // throw new Error('Failed to submit form');
142
+ // }
143
+ }
144
+ }
145
+
146
+ public getFormErrors(): Record<string, string> {
147
+ const errors: Record<string, string> = {};
148
+ for (const field of this.fields) {
149
+ if (field.error) {
150
+ errors[field.name] = field.error;
151
+ }
152
+ }
153
+ return errors;
154
+ }
155
+
156
+ public getFormError(name: string): string | undefined {
157
+ const field = this.fields.find((field) => field.name === name);
158
+ return field?.error;
159
+ }
160
+
161
+ public setFormError(name: string, error: string): void {
162
+ const field = this.fields.find((field) => field.name === name);
163
+ if (field) {
164
+ field.error = error;
165
+ this.invalid = true;
166
+ }
167
+ }
168
+
169
+ public validateLayout(): void {
170
+ if (!this.layout) return;
171
+
172
+ const fieldNames = new Set(this.fields.map(f => f.name));
173
+ const missingFields: string[] = [];
174
+
175
+ for (const row of this.layout) {
176
+ for (const cell of row) {
177
+ // Handle nested arrays (multiple fields in one cell)
178
+ if (Array.isArray(cell)) {
179
+ for (const fieldName of cell) {
180
+ if (fieldName && !fieldNames.has(fieldName)) {
181
+ missingFields.push(fieldName);
182
+ }
183
+ }
184
+ } else if (cell && !fieldNames.has(cell)) {
185
+ missingFields.push(cell);
186
+ }
187
+ }
188
+ }
189
+
190
+ if (missingFields.length > 0) {
191
+ console.warn(`[FormModel] Layout references missing fields: ${missingFields.join(', ')}`);
192
+ }
193
+ }
194
+
195
+ }
@@ -0,0 +1,199 @@
1
+ import ValidatorUtils from "./index";
2
+ import type { ValidationRule } from "@/interfaces";
3
+ import { describe, expect, it, beforeEach } from "vitest";
4
+
5
+
6
+ describe("ValidatorUtils", () => {
7
+ beforeEach(() => {
8
+ ValidatorUtils.setLib("zod");
9
+ ValidatorUtils.customValidators = {};
10
+ });
11
+
12
+ it("should validate using the default library (zod)", () => {
13
+ const rule: ValidationRule = { type: "string" };
14
+ const value = "test";
15
+ expect(ValidatorUtils.validate(rule, value, {})).toBe(true);
16
+ });
17
+
18
+ it.skip("should switch to a different library and validate", () => {
19
+ ValidatorUtils.setLib("ark_type");
20
+ const rule: ValidationRule = { type: "string" };
21
+ const value = "test";
22
+ expect(ValidatorUtils.validate(rule, value, {})).toBe(true);
23
+ });
24
+
25
+ it("should throw an error when switching to an unsupported library", () => {
26
+ expect(() => ValidatorUtils.setLib("unsupported_lib")).toThrow(
27
+ 'Validator library "unsupported_lib" is not supported.'
28
+ );
29
+ });
30
+
31
+ it("should add a custom validator and validate using it", () => {
32
+ ValidatorUtils.customValidators["isEven"] = (value) => value % 2 === 0;
33
+
34
+ const rule: ValidationRule = { type: "custom", value: "isEven" };
35
+ expect(ValidatorUtils.validate(rule, 4, {})).toBe(true);
36
+ expect(ValidatorUtils.validate(rule, 3, {})).toBe(false);
37
+ });
38
+
39
+ it("should throw an error if a custom validator is not found", () => {
40
+ const rule: ValidationRule = { type: "custom", value: "nonExistentValidator" };
41
+ expect(() => ValidatorUtils.validate(rule, 4, {})).toThrow(
42
+ 'Custom validation function "nonExistentValidator" not found.'
43
+ );
44
+ });
45
+
46
+ it("should validate string schema", () => {
47
+ const rule: ValidationRule = { type: "string" };
48
+ expect(ValidatorUtils.validate(rule, "test", {})).toBe(true);
49
+ expect(ValidatorUtils.validate(rule, 123, {})).toBe(false);
50
+ });
51
+
52
+ it("should validate number schema", () => {
53
+ const rule: ValidationRule = { type: "number" };
54
+ expect(ValidatorUtils.validate(rule, 123, {})).toBe(true);
55
+ expect(ValidatorUtils.validate(rule, "test", {})).toBe(false);
56
+ });
57
+
58
+ it("should validate boolean schema", () => {
59
+ const rule: ValidationRule = { type: "boolean" };
60
+ expect(ValidatorUtils.validate(rule, true, {})).toBe(true);
61
+ expect(ValidatorUtils.validate(rule, "true", {})).toBe(false);
62
+ });
63
+
64
+ it("should validate email schema", () => {
65
+ const rule: ValidationRule = { type: "email" };
66
+ expect(ValidatorUtils.validate(rule, "test@example.com", {})).toBe(true);
67
+ expect(ValidatorUtils.validate(rule, "invalid-email", {})).toBe(false);
68
+ });
69
+
70
+ it("should validate url schema", () => {
71
+ const rule: ValidationRule = { type: "url" };
72
+ expect(ValidatorUtils.validate(rule, "https://example.com", {})).toBe(true);
73
+ expect(ValidatorUtils.validate(rule, "invalid-url", {})).toBe(false);
74
+ });
75
+
76
+ it("should validate minLength schema", () => {
77
+ const rule: ValidationRule = { type: "minLength", value: 5 };
78
+ expect(ValidatorUtils.validate(rule, "12345", {})).toBe(true);
79
+ expect(ValidatorUtils.validate(rule, "123", {})).toBe(false);
80
+ });
81
+
82
+ it("should validate maxLength schema", () => {
83
+ const rule: ValidationRule = { type: "maxLength", value: 5 };
84
+ expect(ValidatorUtils.validate(rule, "12345", {})).toBe(true);
85
+ expect(ValidatorUtils.validate(rule, "123456", {})).toBe(false);
86
+ });
87
+
88
+ it("should validate min schema", () => {
89
+ const rule: ValidationRule = { type: "min", value: 10 };
90
+ expect(ValidatorUtils.validate(rule, 15, {})).toBe(true);
91
+ expect(ValidatorUtils.validate(rule, 5, {})).toBe(false);
92
+ });
93
+
94
+ it("should validate max schema", () => {
95
+ const rule: ValidationRule = { type: "max", value: 10 };
96
+ expect(ValidatorUtils.validate(rule, 5, {})).toBe(true);
97
+ expect(ValidatorUtils.validate(rule, 15, {})).toBe(false);
98
+ });
99
+
100
+ it("should validate regex schema", () => {
101
+ const rule: ValidationRule = { type: "regex", value: "^test$" };
102
+ expect(ValidatorUtils.validate(rule, "test", {})).toBe(true);
103
+ expect(ValidatorUtils.validate(rule, "not-test", {})).toBe(false);
104
+ });
105
+
106
+ it("should validate date schema", () => {
107
+ const rule: ValidationRule = { type: "date" };
108
+ expect(ValidatorUtils.validate(rule, new Date().toISOString(), {})).toBe(true);
109
+ expect(ValidatorUtils.validate(rule, "invalid-date", {})).toBe(false);
110
+ });
111
+
112
+ it("should validate custom schema", () => {
113
+ ValidatorUtils.customValidators["isPositive"] = (value) => value > 0;
114
+ const rule: ValidationRule = { type: "custom", value: "isPositive" };
115
+ expect(ValidatorUtils.validate(rule, 5, {})).toBe(true);
116
+ expect(ValidatorUtils.validate(rule, -5, {})).toBe(false);
117
+ });
118
+ });
119
+
120
+ describe("ValidatorUtils with ArkType library", () => {
121
+ beforeEach(() => {
122
+ ValidatorUtils.setLib("ark_type");
123
+ });
124
+
125
+ it("should validate string schema", () => {
126
+ const rule: ValidationRule = { type: "string" };
127
+
128
+ expect(ValidatorUtils.validate(rule, "test", {})).toBe(true);
129
+ expect(ValidatorUtils.validate(rule, 123, {})).toBe(false);
130
+ });
131
+
132
+ it("should validate number schema", () => {
133
+ const rule: ValidationRule = { type: "number" };
134
+ expect(ValidatorUtils.validate(rule, 123, {})).toBe(true);
135
+ expect(ValidatorUtils.validate(rule, "test", {})).toBe(false);
136
+ });
137
+
138
+ it("should validate boolean schema", () => {
139
+ const rule: ValidationRule = { type: "boolean" };
140
+ expect(ValidatorUtils.validate(rule, true, {})).toBe(true);
141
+ expect(ValidatorUtils.validate(rule, "true", {})).toBe(false);
142
+ });
143
+
144
+ it("should validate email schema", () => {
145
+ const rule: ValidationRule = { type: "email" };
146
+ expect(ValidatorUtils.validate(rule, "test@example.com", {})).toBe(true);
147
+ expect(ValidatorUtils.validate(rule, "invalid-email", {})).toBe(false);
148
+ });
149
+
150
+ it("should validate url schema", () => {
151
+ const rule: ValidationRule = { type: "url" };
152
+ expect(ValidatorUtils.validate(rule, "https://example.com", {})).toBe(true);
153
+ expect(ValidatorUtils.validate(rule, "invalid-url", {})).toBe(false);
154
+ });
155
+
156
+ it("should validate minLength schema", () => {
157
+ const rule: ValidationRule = { type: "minLength", value: 5 };
158
+ expect(ValidatorUtils.validate(rule, "12345", {})).toBe(true);
159
+ expect(ValidatorUtils.validate(rule, "123", {})).toBe(false);
160
+ });
161
+
162
+ it("should validate maxLength schema", () => {
163
+ const rule: ValidationRule = { type: "maxLength", value: 5 };
164
+ expect(ValidatorUtils.validate(rule, "12345", {})).toBe(true);
165
+ expect(ValidatorUtils.validate(rule, "123456", {})).toBe(false);
166
+ });
167
+
168
+ it("should validate min schema", () => {
169
+ const rule: ValidationRule = { type: "min", value: 10 };
170
+ expect(ValidatorUtils.validate(rule, 15, {})).toBe(true);
171
+ expect(ValidatorUtils.validate(rule, 5, {})).toBe(false);
172
+ });
173
+
174
+ it("should validate max schema", () => {
175
+ const rule: ValidationRule = { type: "max", value: 10 };
176
+ expect(ValidatorUtils.validate(rule, 5, {})).toBe(true);
177
+ expect(ValidatorUtils.validate(rule, 15, {})).toBe(false);
178
+ });
179
+
180
+ it("should validate regex schema", () => {
181
+ const rule: ValidationRule = { type: "regex", value: "^test$" };
182
+ expect(ValidatorUtils.validate(rule, "test", {})).toBe(true);
183
+ expect(ValidatorUtils.validate(rule, "not-test", {})).toBe(false);
184
+ });
185
+
186
+ it("should validate date schema", () => {
187
+ const rule: ValidationRule = { type: "date" };
188
+ expect(ValidatorUtils.validate(rule, new Date().toISOString(), {})).toBe(true);
189
+ expect(ValidatorUtils.validate(rule, "invalid-date", {})).toBe(false);
190
+ });
191
+
192
+
193
+ it("should validate custom schema", () => {
194
+ ValidatorUtils.customValidators["isPositive"] = (value) => value > 0;
195
+ const rule: ValidationRule = { type: "custom", value: "isPositive" };
196
+ expect(ValidatorUtils.validate(rule, 5, {})).toBe(true);
197
+ expect(ValidatorUtils.validate(rule, -5, {})).toBe(false);
198
+ });
199
+ });
@@ -0,0 +1,42 @@
1
+ import type { IValidatorLib, ValidationRule } from "../../interfaces";
2
+ import ArkTypeValidatorLib from "./libs/arktype";
3
+ import ZodValidatorLib from "./libs/zod";
4
+
5
+ export default class ValidatorUtils {
6
+ private static lib: string = 'ark_type';
7
+
8
+ public static customValidators: Record<string, (value: any, formValue: any) => boolean> = {};
9
+
10
+ private static validatorLibs: { [key: string]: IValidatorLib } = {
11
+ zod: new ZodValidatorLib(),
12
+ ark_type: new ArkTypeValidatorLib(),
13
+ };
14
+
15
+ public static addLib(name: string, lib: IValidatorLib) {
16
+ this.validatorLibs[name] = lib;
17
+ }
18
+
19
+ public static setLib(lib: string) {
20
+ if (this.validatorLibs[lib]) {
21
+ this.lib = lib;
22
+ } else {
23
+ throw new Error(`Validator library "${lib}" is not supported.`);
24
+ }
25
+ }
26
+
27
+ static validate(rule: ValidationRule, value: any, formValue: any): boolean {
28
+
29
+ if (rule.type === 'custom') {
30
+ // Resolve the custom validation function from the map
31
+ const customValidation = this.customValidators[rule.value as string];
32
+
33
+ if (customValidation) {
34
+ return customValidation(value, formValue);
35
+ } else {
36
+ throw new Error(`Custom validation function "${rule.value}" not found.`);
37
+ }
38
+ }
39
+
40
+ return this.validatorLibs[this.lib].validate(rule, value);
41
+ }
42
+ }
@@ -0,0 +1,45 @@
1
+ import { ArkErrors, type, Type } from "arktype";
2
+ import type { IValidatorLib, ValidationRule } from "@/interfaces";
3
+
4
+ export default class ArktypeValidatorLib implements IValidatorLib {
5
+ schemas: Record<ValidationRule['type'], (ruleValue?: any) => Type<any>> = {} as Record<ValidationRule['type'], () => Type>;
6
+
7
+ validate({ type: ruleType, value: ruleValue }: ValidationRule, value: any): boolean {
8
+ const schema = this.schemaFactory(ruleType, ruleValue);
9
+ const result = schema(value);
10
+
11
+ return !(result instanceof ArkErrors);
12
+ }
13
+
14
+ schemaFactory(validationType: ValidationRule['type'], ruleValue: any): Type<any> {
15
+ this.schemas = {
16
+ string: () => type("string"),
17
+ number: () => type("number"),
18
+ boolean: () => type("boolean"),
19
+ email: () => type("string.email"),
20
+ url: () => type("string.url"),
21
+ minLength: () => type(`string >= ${Number(ruleValue)}`),
22
+ maxLength: () => type(`string <= ${Number(ruleValue)}`),
23
+ min: () => type(`number >= ${Number(ruleValue)}`),
24
+ max: () => type(`number <= ${Number(ruleValue)}`),
25
+ regex: () => type(`string & /${ruleValue}/`),
26
+ date: () => type("string.date"),
27
+ custom: () => {
28
+ if (typeof ruleValue === "function") {
29
+ return type((val: any) => ({
30
+ data: val,
31
+ problems: ruleValue(val) ? undefined : [{ path: [], message: "Custom validation failed" }],
32
+ }));
33
+ } else {
34
+ throw new Error("Invalid rule value for custom validation. Expected a function.");
35
+ }
36
+ },
37
+ };
38
+
39
+ const schemaGenerator = this.schemas[validationType];
40
+ if (!schemaGenerator) {
41
+ throw new Error(`Unsupported rule type: ${type}`);
42
+ }
43
+ return schemaGenerator(ruleValue);
44
+ }
45
+ }
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import type { IValidatorLib, ValidationRule } from "@/interfaces";
3
+
4
+ export default class ZodValidatorLib implements IValidatorLib {
5
+
6
+ schemas: Record<ValidationRule['type'], () => z.ZodTypeAny> = {} as Record<ValidationRule['type'], () => z.ZodTypeAny>;
7
+
8
+ validate({ type, value: ruleValue }: ValidationRule, value: any): boolean {
9
+ try {
10
+ const schema = this.schemaFactory(type, ruleValue);
11
+ // Zod does not support date validation directly, so we need to convert the value to a Date object if the type is "date"
12
+ schema.parse(type === "date" ? new Date(value) : value);
13
+
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ schemaFactory(type: ValidationRule['type'], ruleValue: any) {
21
+ this.schemas = {
22
+ string: () => z.string(),
23
+ number: () => z.number(),
24
+ boolean: () => z.boolean(),
25
+ email: () => z.string().email(),
26
+ url: () => z.string().url(),
27
+ minLength: () => z.string().min(Number(ruleValue)),
28
+ maxLength: () => z.string().max(Number(ruleValue)),
29
+ min: () => z.number().min(Number(ruleValue)),
30
+ max: () => z.number().max(Number(ruleValue)),
31
+ regex: () => {
32
+ if (typeof ruleValue === "string") {
33
+ return z.string().regex(new RegExp(ruleValue));
34
+ } else {
35
+ throw new Error("Invalid rule value for regex validation. Expected a string.");
36
+ }
37
+ },
38
+ date: () => z.date(),
39
+ custom: () => {
40
+ if (typeof ruleValue === "function") {
41
+ return z.custom(ruleValue);
42
+ } else {
43
+ throw new Error("Invalid rule value for custom validation. Expected a function.");
44
+ }
45
+ },
46
+ };
47
+
48
+ const schema = this.schemas[type]();
49
+ if (!schema) throw new Error(`Unsupported rule type: ${type}`);
50
+
51
+ return schema;
52
+ }
53
+ }