@htlkg/components 0.0.1
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/dist/composables/index.js +388 -0
- package/dist/composables/index.js.map +1 -0
- package/package.json +41 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useForm.test.ts +229 -0
- package/src/composables/useForm.ts +130 -0
- package/src/composables/useFormValidation.test.ts +189 -0
- package/src/composables/useFormValidation.ts +83 -0
- package/src/composables/useModal.property.test.ts +164 -0
- package/src/composables/useModal.ts +43 -0
- package/src/composables/useNotifications.test.ts +166 -0
- package/src/composables/useNotifications.ts +81 -0
- package/src/composables/useTable.property.test.ts +198 -0
- package/src/composables/useTable.ts +134 -0
- package/src/composables/useTabs.property.test.ts +247 -0
- package/src/composables/useTabs.ts +101 -0
- package/src/data/Chart.demo.vue +340 -0
- package/src/data/Chart.md +525 -0
- package/src/data/Chart.vue +133 -0
- package/src/data/DataList.md +80 -0
- package/src/data/DataList.test.ts +69 -0
- package/src/data/DataList.vue +46 -0
- package/src/data/SearchableSelect.md +107 -0
- package/src/data/SearchableSelect.vue +124 -0
- package/src/data/Table.demo.vue +296 -0
- package/src/data/Table.md +588 -0
- package/src/data/Table.property.test.ts +548 -0
- package/src/data/Table.test.ts +562 -0
- package/src/data/Table.unit.test.ts +544 -0
- package/src/data/Table.vue +321 -0
- package/src/data/index.ts +5 -0
- package/src/domain/BrandCard.md +81 -0
- package/src/domain/BrandCard.vue +63 -0
- package/src/domain/BrandSelector.md +84 -0
- package/src/domain/BrandSelector.vue +65 -0
- package/src/domain/ProductBadge.md +60 -0
- package/src/domain/ProductBadge.vue +47 -0
- package/src/domain/UserAvatar.md +84 -0
- package/src/domain/UserAvatar.vue +60 -0
- package/src/domain/domain-components.property.test.ts +449 -0
- package/src/domain/index.ts +4 -0
- package/src/forms/DateRange.demo.vue +273 -0
- package/src/forms/DateRange.md +337 -0
- package/src/forms/DateRange.vue +110 -0
- package/src/forms/JsonSchemaForm.demo.vue +549 -0
- package/src/forms/JsonSchemaForm.md +112 -0
- package/src/forms/JsonSchemaForm.property.test.ts +817 -0
- package/src/forms/JsonSchemaForm.test.ts +601 -0
- package/src/forms/JsonSchemaForm.unit.test.ts +801 -0
- package/src/forms/JsonSchemaForm.vue +615 -0
- package/src/forms/index.ts +3 -0
- package/src/index.ts +17 -0
- package/src/navigation/Breadcrumbs.demo.vue +142 -0
- package/src/navigation/Breadcrumbs.md +102 -0
- package/src/navigation/Breadcrumbs.test.ts +69 -0
- package/src/navigation/Breadcrumbs.vue +58 -0
- package/src/navigation/Stepper.demo.vue +337 -0
- package/src/navigation/Stepper.md +174 -0
- package/src/navigation/Stepper.vue +146 -0
- package/src/navigation/Tabs.demo.vue +293 -0
- package/src/navigation/Tabs.md +163 -0
- package/src/navigation/Tabs.test.ts +176 -0
- package/src/navigation/Tabs.vue +104 -0
- package/src/navigation/index.ts +5 -0
- package/src/overlays/Alert.demo.vue +377 -0
- package/src/overlays/Alert.md +248 -0
- package/src/overlays/Alert.test.ts +166 -0
- package/src/overlays/Alert.vue +70 -0
- package/src/overlays/Drawer.md +140 -0
- package/src/overlays/Drawer.test.ts +92 -0
- package/src/overlays/Drawer.vue +76 -0
- package/src/overlays/Modal.demo.vue +149 -0
- package/src/overlays/Modal.md +385 -0
- package/src/overlays/Modal.test.ts +128 -0
- package/src/overlays/Modal.vue +86 -0
- package/src/overlays/Notification.md +150 -0
- package/src/overlays/Notification.test.ts +96 -0
- package/src/overlays/Notification.vue +58 -0
- package/src/overlays/index.ts +4 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { useForm } from './useForm';
|
|
3
|
+
|
|
4
|
+
describe('useForm composable', () => {
|
|
5
|
+
it('initializes with provided values', () => {
|
|
6
|
+
const initialValues = { name: 'John', email: 'john@example.com' };
|
|
7
|
+
const { values } = useForm({ initialValues });
|
|
8
|
+
|
|
9
|
+
expect(values.value).toEqual(initialValues);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('sets field value correctly', () => {
|
|
13
|
+
const { values, setFieldValue } = useForm({
|
|
14
|
+
initialValues: { name: '', email: '' }
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
setFieldValue('name', 'Jane');
|
|
18
|
+
expect(values.value.name).toBe('Jane');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('sets field error correctly', () => {
|
|
22
|
+
const { errors, setFieldError } = useForm({
|
|
23
|
+
initialValues: { name: '' }
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
setFieldError('name', 'Name is required');
|
|
27
|
+
expect(errors.value.name).toBe('Name is required');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('sets field touched correctly', () => {
|
|
31
|
+
const { touched, setFieldTouched } = useForm({
|
|
32
|
+
initialValues: { name: '' }
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
setFieldTouched('name', true);
|
|
36
|
+
expect(touched.value.name).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('validates field with rules', async () => {
|
|
40
|
+
const { validateField, errors } = useForm({
|
|
41
|
+
initialValues: { name: '' },
|
|
42
|
+
validationRules: {
|
|
43
|
+
name: [
|
|
44
|
+
{
|
|
45
|
+
validate: (value) => value.length > 0,
|
|
46
|
+
message: 'Name is required'
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const isValid = await validateField('name');
|
|
53
|
+
expect(isValid).toBe(false);
|
|
54
|
+
expect(errors.value.name).toBe('Name is required');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('clears error when validation passes', async () => {
|
|
58
|
+
const { values, validateField, errors, setFieldValue } = useForm({
|
|
59
|
+
initialValues: { name: '' },
|
|
60
|
+
validationRules: {
|
|
61
|
+
name: [
|
|
62
|
+
{
|
|
63
|
+
validate: (value) => value.length > 0,
|
|
64
|
+
message: 'Name is required'
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// First validation should fail
|
|
71
|
+
await validateField('name');
|
|
72
|
+
expect(errors.value.name).toBe('Name is required');
|
|
73
|
+
|
|
74
|
+
// Set valid value
|
|
75
|
+
setFieldValue('name', 'John');
|
|
76
|
+
await validateField('name');
|
|
77
|
+
expect(errors.value.name).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('validates entire form', async () => {
|
|
81
|
+
const { validateForm, errors } = useForm({
|
|
82
|
+
initialValues: { name: '', email: '' },
|
|
83
|
+
validationRules: {
|
|
84
|
+
name: [
|
|
85
|
+
{
|
|
86
|
+
validate: (value) => value.length > 0,
|
|
87
|
+
message: 'Name is required'
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
email: [
|
|
91
|
+
{
|
|
92
|
+
validate: (value) => value.includes('@'),
|
|
93
|
+
message: 'Invalid email'
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const isValid = await validateForm();
|
|
100
|
+
expect(isValid).toBe(false);
|
|
101
|
+
expect(errors.value.name).toBe('Name is required');
|
|
102
|
+
expect(errors.value.email).toBe('Invalid email');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('handles form submission', async () => {
|
|
106
|
+
const onSubmit = vi.fn();
|
|
107
|
+
const { handleSubmit, values, setFieldValue } = useForm({
|
|
108
|
+
initialValues: { name: '' },
|
|
109
|
+
onSubmit
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
setFieldValue('name', 'John');
|
|
113
|
+
await handleSubmit();
|
|
114
|
+
|
|
115
|
+
expect(onSubmit).toHaveBeenCalledWith({ name: 'John' });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('prevents submission when validation fails', async () => {
|
|
119
|
+
const onSubmit = vi.fn();
|
|
120
|
+
const { handleSubmit } = useForm({
|
|
121
|
+
initialValues: { name: '' },
|
|
122
|
+
validationRules: {
|
|
123
|
+
name: [
|
|
124
|
+
{
|
|
125
|
+
validate: (value) => value.length > 0,
|
|
126
|
+
message: 'Name is required'
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
onSubmit
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await handleSubmit();
|
|
134
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('marks all fields as touched on submit', async () => {
|
|
138
|
+
const { handleSubmit, touched } = useForm({
|
|
139
|
+
initialValues: { name: '', email: '' }
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await handleSubmit();
|
|
143
|
+
expect(touched.value.name).toBe(true);
|
|
144
|
+
expect(touched.value.email).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('resets form to initial values', () => {
|
|
148
|
+
const initialValues = { name: 'John', email: 'john@example.com' };
|
|
149
|
+
const { values, setFieldValue, resetForm } = useForm({ initialValues });
|
|
150
|
+
|
|
151
|
+
setFieldValue('name', 'Jane');
|
|
152
|
+
expect(values.value.name).toBe('Jane');
|
|
153
|
+
|
|
154
|
+
resetForm();
|
|
155
|
+
expect(values.value).toEqual(initialValues);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('clears errors and touched on reset', () => {
|
|
159
|
+
const { errors, touched, setFieldError, setFieldTouched, resetForm } = useForm({
|
|
160
|
+
initialValues: { name: '' }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
setFieldError('name', 'Error');
|
|
164
|
+
setFieldTouched('name', true);
|
|
165
|
+
|
|
166
|
+
resetForm();
|
|
167
|
+
expect(errors.value).toEqual({});
|
|
168
|
+
expect(touched.value).toEqual({});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('computes isValid correctly', () => {
|
|
172
|
+
const { isValid, setFieldError } = useForm({
|
|
173
|
+
initialValues: { name: '' }
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(isValid.value).toBe(true);
|
|
177
|
+
|
|
178
|
+
setFieldError('name', 'Error');
|
|
179
|
+
expect(isValid.value).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('handles async validation rules', async () => {
|
|
183
|
+
const { validateField, errors } = useForm({
|
|
184
|
+
initialValues: { username: '' },
|
|
185
|
+
validationRules: {
|
|
186
|
+
username: [
|
|
187
|
+
{
|
|
188
|
+
validate: async (value) => {
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
190
|
+
return value === 'available';
|
|
191
|
+
},
|
|
192
|
+
message: 'Username not available'
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const isValid = await validateField('username');
|
|
199
|
+
expect(isValid).toBe(false);
|
|
200
|
+
expect(errors.value.username).toBe('Username not available');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('prevents concurrent submissions', async () => {
|
|
204
|
+
const onSubmit = vi.fn(async () => {
|
|
205
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const { handleSubmit, isSubmitting } = useForm({
|
|
209
|
+
initialValues: { name: 'John' },
|
|
210
|
+
onSubmit
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Start first submission
|
|
214
|
+
const promise1 = handleSubmit();
|
|
215
|
+
|
|
216
|
+
// Wait a tick for the isSubmitting flag to be set
|
|
217
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
218
|
+
expect(isSubmitting.value).toBe(true);
|
|
219
|
+
|
|
220
|
+
// Try second submission while first is in progress
|
|
221
|
+
await handleSubmit();
|
|
222
|
+
|
|
223
|
+
// Wait for first to complete
|
|
224
|
+
await promise1;
|
|
225
|
+
|
|
226
|
+
// Should only be called once
|
|
227
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ref, computed, type Ref, type ComputedRef } from 'vue';
|
|
2
|
+
|
|
3
|
+
export interface ValidationRule<T = any> {
|
|
4
|
+
validate: (value: T) => boolean | Promise<boolean>;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FieldConfig {
|
|
9
|
+
rules?: ValidationRule[];
|
|
10
|
+
initialValue?: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseFormOptions<T extends Record<string, any>> {
|
|
14
|
+
initialValues: T;
|
|
15
|
+
validationRules?: Partial<Record<keyof T, ValidationRule[]>>;
|
|
16
|
+
onSubmit?: (values: T) => void | Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseFormReturn<T extends Record<string, any>> {
|
|
20
|
+
values: Ref<T>;
|
|
21
|
+
errors: Ref<Partial<Record<keyof T, string>>>;
|
|
22
|
+
touched: Ref<Partial<Record<keyof T, boolean>>>;
|
|
23
|
+
isSubmitting: Ref<boolean>;
|
|
24
|
+
isValid: ComputedRef<boolean>;
|
|
25
|
+
setFieldValue: (field: keyof T, value: any) => void;
|
|
26
|
+
setFieldError: (field: keyof T, error: string) => void;
|
|
27
|
+
setFieldTouched: (field: keyof T, touched: boolean) => void;
|
|
28
|
+
validateField: (field: keyof T) => Promise<boolean>;
|
|
29
|
+
validateForm: () => Promise<boolean>;
|
|
30
|
+
handleSubmit: (event?: Event) => Promise<void>;
|
|
31
|
+
resetForm: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useForm<T extends Record<string, any>>(
|
|
35
|
+
options: UseFormOptions<T>
|
|
36
|
+
): UseFormReturn<T> {
|
|
37
|
+
const values = ref<T>({ ...options.initialValues }) as Ref<T>;
|
|
38
|
+
const errors = ref<Partial<Record<keyof T, string>>>({});
|
|
39
|
+
const touched = ref<Partial<Record<keyof T, boolean>>>({});
|
|
40
|
+
const isSubmitting = ref(false);
|
|
41
|
+
|
|
42
|
+
const isValid = computed(() => Object.keys(errors.value).length === 0);
|
|
43
|
+
|
|
44
|
+
function setFieldValue(field: keyof T, value: any) {
|
|
45
|
+
values.value[field] = value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setFieldError(field: keyof T, error: string) {
|
|
49
|
+
errors.value[field] = error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function setFieldTouched(field: keyof T, isTouched: boolean) {
|
|
53
|
+
touched.value[field] = isTouched;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function validateField(field: keyof T): Promise<boolean> {
|
|
57
|
+
const rules = options.validationRules?.[field];
|
|
58
|
+
if (!rules || rules.length === 0) return true;
|
|
59
|
+
|
|
60
|
+
const value = values.value[field];
|
|
61
|
+
|
|
62
|
+
for (const rule of rules) {
|
|
63
|
+
const isValid = await rule.validate(value);
|
|
64
|
+
if (!isValid) {
|
|
65
|
+
setFieldError(field, rule.message);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Clear error if validation passed
|
|
71
|
+
delete errors.value[field];
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function validateForm(): Promise<boolean> {
|
|
76
|
+
const fields = Object.keys(values.value) as Array<keyof T>;
|
|
77
|
+
const validationResults = await Promise.all(
|
|
78
|
+
fields.map(field => validateField(field))
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return validationResults.every(result => result);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function handleSubmit(event?: Event) {
|
|
85
|
+
if (event) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (isSubmitting.value) return;
|
|
90
|
+
|
|
91
|
+
// Mark all fields as touched
|
|
92
|
+
for (const field of Object.keys(values.value) as Array<keyof T>) {
|
|
93
|
+
setFieldTouched(field, true);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate form
|
|
97
|
+
const isFormValid = await validateForm();
|
|
98
|
+
if (!isFormValid) return;
|
|
99
|
+
|
|
100
|
+
// Submit form
|
|
101
|
+
isSubmitting.value = true;
|
|
102
|
+
try {
|
|
103
|
+
await options.onSubmit?.(values.value);
|
|
104
|
+
} finally {
|
|
105
|
+
isSubmitting.value = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resetForm() {
|
|
110
|
+
values.value = { ...options.initialValues };
|
|
111
|
+
errors.value = {};
|
|
112
|
+
touched.value = {};
|
|
113
|
+
isSubmitting.value = false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
values,
|
|
118
|
+
errors,
|
|
119
|
+
touched,
|
|
120
|
+
isSubmitting,
|
|
121
|
+
isValid,
|
|
122
|
+
setFieldValue,
|
|
123
|
+
setFieldError,
|
|
124
|
+
setFieldTouched,
|
|
125
|
+
validateField,
|
|
126
|
+
validateForm,
|
|
127
|
+
handleSubmit,
|
|
128
|
+
resetForm
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
required,
|
|
4
|
+
minLength,
|
|
5
|
+
maxLength,
|
|
6
|
+
email,
|
|
7
|
+
pattern,
|
|
8
|
+
min,
|
|
9
|
+
max,
|
|
10
|
+
custom,
|
|
11
|
+
useFormValidation
|
|
12
|
+
} from './useFormValidation';
|
|
13
|
+
|
|
14
|
+
describe('useFormValidation composable', () => {
|
|
15
|
+
describe('required', () => {
|
|
16
|
+
it('validates non-empty strings', () => {
|
|
17
|
+
const rule = required();
|
|
18
|
+
expect(rule.validate('test')).toBe(true);
|
|
19
|
+
expect(rule.validate('')).toBe(false);
|
|
20
|
+
expect(rule.validate(' ')).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('validates non-empty arrays', () => {
|
|
24
|
+
const rule = required();
|
|
25
|
+
expect(rule.validate([1, 2, 3])).toBe(true);
|
|
26
|
+
expect(rule.validate([])).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('validates null and undefined', () => {
|
|
30
|
+
const rule = required();
|
|
31
|
+
expect(rule.validate(null)).toBe(false);
|
|
32
|
+
expect(rule.validate(undefined)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('uses custom message', () => {
|
|
36
|
+
const rule = required('Custom message');
|
|
37
|
+
expect(rule.message).toBe('Custom message');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('minLength', () => {
|
|
42
|
+
it('validates minimum length', () => {
|
|
43
|
+
const rule = minLength(5);
|
|
44
|
+
expect(rule.validate('hello')).toBe(true);
|
|
45
|
+
expect(rule.validate('hi')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('allows empty values', () => {
|
|
49
|
+
const rule = minLength(5);
|
|
50
|
+
expect(rule.validate('')).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('uses custom message', () => {
|
|
54
|
+
const rule = minLength(5, 'Too short');
|
|
55
|
+
expect(rule.message).toBe('Too short');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('maxLength', () => {
|
|
60
|
+
it('validates maximum length', () => {
|
|
61
|
+
const rule = maxLength(5);
|
|
62
|
+
expect(rule.validate('hello')).toBe(true);
|
|
63
|
+
expect(rule.validate('hello world')).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('allows empty values', () => {
|
|
67
|
+
const rule = maxLength(5);
|
|
68
|
+
expect(rule.validate('')).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('uses custom message', () => {
|
|
72
|
+
const rule = maxLength(5, 'Too long');
|
|
73
|
+
expect(rule.message).toBe('Too long');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('email', () => {
|
|
78
|
+
it('validates email format', () => {
|
|
79
|
+
const rule = email();
|
|
80
|
+
expect(rule.validate('test@example.com')).toBe(true);
|
|
81
|
+
expect(rule.validate('invalid')).toBe(false);
|
|
82
|
+
expect(rule.validate('test@')).toBe(false);
|
|
83
|
+
expect(rule.validate('@example.com')).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('allows empty values', () => {
|
|
87
|
+
const rule = email();
|
|
88
|
+
expect(rule.validate('')).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('uses custom message', () => {
|
|
92
|
+
const rule = email('Invalid email format');
|
|
93
|
+
expect(rule.message).toBe('Invalid email format');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('pattern', () => {
|
|
98
|
+
it('validates against regex pattern', () => {
|
|
99
|
+
const rule = pattern(/^\d{3}-\d{3}-\d{4}$/);
|
|
100
|
+
expect(rule.validate('123-456-7890')).toBe(true);
|
|
101
|
+
expect(rule.validate('1234567890')).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('allows empty values', () => {
|
|
105
|
+
const rule = pattern(/^\d+$/);
|
|
106
|
+
expect(rule.validate('')).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('uses custom message', () => {
|
|
110
|
+
const rule = pattern(/^\d+$/, 'Must be numbers only');
|
|
111
|
+
expect(rule.message).toBe('Must be numbers only');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('min', () => {
|
|
116
|
+
it('validates minimum value', () => {
|
|
117
|
+
const rule = min(10);
|
|
118
|
+
expect(rule.validate(10)).toBe(true);
|
|
119
|
+
expect(rule.validate(15)).toBe(true);
|
|
120
|
+
expect(rule.validate(5)).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('allows null and undefined', () => {
|
|
124
|
+
const rule = min(10);
|
|
125
|
+
expect(rule.validate(null)).toBe(true);
|
|
126
|
+
expect(rule.validate(undefined)).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('uses custom message', () => {
|
|
130
|
+
const rule = min(10, 'Too small');
|
|
131
|
+
expect(rule.message).toBe('Too small');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('max', () => {
|
|
136
|
+
it('validates maximum value', () => {
|
|
137
|
+
const rule = max(10);
|
|
138
|
+
expect(rule.validate(10)).toBe(true);
|
|
139
|
+
expect(rule.validate(5)).toBe(true);
|
|
140
|
+
expect(rule.validate(15)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('allows null and undefined', () => {
|
|
144
|
+
const rule = max(10);
|
|
145
|
+
expect(rule.validate(null)).toBe(true);
|
|
146
|
+
expect(rule.validate(undefined)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('uses custom message', () => {
|
|
150
|
+
const rule = max(10, 'Too large');
|
|
151
|
+
expect(rule.message).toBe('Too large');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('custom', () => {
|
|
156
|
+
it('validates with custom function', () => {
|
|
157
|
+
const rule = custom((value) => value === 'valid', 'Must be valid');
|
|
158
|
+
expect(rule.validate('valid')).toBe(true);
|
|
159
|
+
expect(rule.validate('invalid')).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('supports async validation', async () => {
|
|
163
|
+
const rule = custom(
|
|
164
|
+
async (value) => {
|
|
165
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
166
|
+
return value === 'valid';
|
|
167
|
+
},
|
|
168
|
+
'Must be valid'
|
|
169
|
+
);
|
|
170
|
+
expect(await rule.validate('valid')).toBe(true);
|
|
171
|
+
expect(await rule.validate('invalid')).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('useFormValidation', () => {
|
|
176
|
+
it('returns all validation functions', () => {
|
|
177
|
+
const validation = useFormValidation();
|
|
178
|
+
|
|
179
|
+
expect(validation.required).toBeDefined();
|
|
180
|
+
expect(validation.minLength).toBeDefined();
|
|
181
|
+
expect(validation.maxLength).toBeDefined();
|
|
182
|
+
expect(validation.email).toBeDefined();
|
|
183
|
+
expect(validation.pattern).toBeDefined();
|
|
184
|
+
expect(validation.min).toBeDefined();
|
|
185
|
+
expect(validation.max).toBeDefined();
|
|
186
|
+
expect(validation.custom).toBeDefined();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ValidationRule } from './useForm';
|
|
2
|
+
|
|
3
|
+
// Common validation rules
|
|
4
|
+
export const required = (message = 'This field is required'): ValidationRule => ({
|
|
5
|
+
validate: (value: any) => {
|
|
6
|
+
if (value === null || value === undefined) return false;
|
|
7
|
+
if (typeof value === 'string') return value.trim().length > 0;
|
|
8
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
9
|
+
return true;
|
|
10
|
+
},
|
|
11
|
+
message
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const minLength = (min: number, message?: string): ValidationRule => ({
|
|
15
|
+
validate: (value: string) => {
|
|
16
|
+
if (!value) return true; // Let required handle empty values
|
|
17
|
+
return value.length >= min;
|
|
18
|
+
},
|
|
19
|
+
message: message || `Must be at least ${min} characters`
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const maxLength = (max: number, message?: string): ValidationRule => ({
|
|
23
|
+
validate: (value: string) => {
|
|
24
|
+
if (!value) return true;
|
|
25
|
+
return value.length <= max;
|
|
26
|
+
},
|
|
27
|
+
message: message || `Must be at most ${max} characters`
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const email = (message = 'Invalid email address'): ValidationRule => ({
|
|
31
|
+
validate: (value: string) => {
|
|
32
|
+
if (!value) return true;
|
|
33
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
34
|
+
return emailRegex.test(value);
|
|
35
|
+
},
|
|
36
|
+
message
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const pattern = (regex: RegExp, message = 'Invalid format'): ValidationRule => ({
|
|
40
|
+
validate: (value: string) => {
|
|
41
|
+
if (!value) return true;
|
|
42
|
+
return regex.test(value);
|
|
43
|
+
},
|
|
44
|
+
message
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const min = (minValue: number, message?: string): ValidationRule => ({
|
|
48
|
+
validate: (value: number) => {
|
|
49
|
+
if (value === null || value === undefined) return true;
|
|
50
|
+
return value >= minValue;
|
|
51
|
+
},
|
|
52
|
+
message: message || `Must be at least ${minValue}`
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const max = (maxValue: number, message?: string): ValidationRule => ({
|
|
56
|
+
validate: (value: number) => {
|
|
57
|
+
if (value === null || value === undefined) return true;
|
|
58
|
+
return value <= maxValue;
|
|
59
|
+
},
|
|
60
|
+
message: message || `Must be at most ${maxValue}`
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const custom = (
|
|
64
|
+
validator: (value: any) => boolean | Promise<boolean>,
|
|
65
|
+
message: string
|
|
66
|
+
): ValidationRule => ({
|
|
67
|
+
validate: validator,
|
|
68
|
+
message
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Composable for form validation
|
|
72
|
+
export function useFormValidation() {
|
|
73
|
+
return {
|
|
74
|
+
required,
|
|
75
|
+
minLength,
|
|
76
|
+
maxLength,
|
|
77
|
+
email,
|
|
78
|
+
pattern,
|
|
79
|
+
min,
|
|
80
|
+
max,
|
|
81
|
+
custom
|
|
82
|
+
};
|
|
83
|
+
}
|