@page-speed/forms 0.2.2 → 0.3.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 +504 -2
- package/dist/core.cjs +18 -4
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +18 -4
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +18 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/inputs.cjs +1279 -68
- package/dist/inputs.cjs.map +1 -1
- package/dist/inputs.d.cts +521 -4
- package/dist/inputs.d.ts +521 -4
- package/dist/inputs.js +1275 -68
- package/dist/inputs.js.map +1 -1
- package/dist/{types-Cw5CeZP-.d.cts → types-Dww52PeF.d.cts} +3 -0
- package/dist/{types-Cw5CeZP-.d.ts → types-Dww52PeF.d.ts} +3 -0
- package/dist/validation-rules.d.cts +1 -1
- package/dist/validation-rules.d.ts +1 -1
- package/dist/validation-utils.d.cts +1 -1
- package/dist/validation-utils.d.ts +1 -1
- package/dist/validation-valibot.d.cts +1 -1
- package/dist/validation-valibot.d.ts +1 -1
- package/dist/validation.d.cts +1 -1
- package/dist/validation.d.ts +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<img width="
|
|
1
|
+
<img width="896" height="330" alt="page-speed-forms" src="https://github.com/user-attachments/assets/6db9cf70-5488-472d-b1a7-b79cc74a8cf2" />
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# ⚡@page-speed/forms
|
|
4
4
|
|
|
5
5
|
Type-safe form state management and validation for React applications.
|
|
6
6
|
|
|
@@ -8,6 +8,12 @@ Type-safe form state management and validation for React applications.
|
|
|
8
8
|
|
|
9
9
|
OpenSite Page Speed Forms is a high-performance library designed to streamline form state management, validation, and submission handling in React applications. This library is part of OpenSite AI's open-source ecosystem, built for performance and open collaboration. By emphasizing type safety and modularity, it aligns with OpenSite's goal to create scalable, open, and developer-friendly performance tooling.
|
|
10
10
|
|
|
11
|
+
[](https://www.npmjs.com/package/@page-speed/forms)
|
|
12
|
+
[](https://www.npmjs.com/package/@page-speed/forms)
|
|
13
|
+
[](./LICENSE)
|
|
14
|
+
[](./tsconfig.json)
|
|
15
|
+
[](#tree-shaking)
|
|
16
|
+
|
|
11
17
|
Learn more at [OpenSite.ai Developers](https://opensite.ai/developers).
|
|
12
18
|
|
|
13
19
|
## Key Features
|
|
@@ -77,6 +83,502 @@ const form = useForm({
|
|
|
77
83
|
});
|
|
78
84
|
```
|
|
79
85
|
|
|
86
|
+
## Advanced Validation Features
|
|
87
|
+
|
|
88
|
+
### Cross-Field Validation
|
|
89
|
+
|
|
90
|
+
Validate fields that depend on other field values using the `crossFieldValidator` utility or by accessing `allValues` in your validator:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { useForm, crossFieldValidator } from '@page-speed/forms/validation';
|
|
94
|
+
|
|
95
|
+
// Method 1: Using crossFieldValidator helper
|
|
96
|
+
const form = useForm({
|
|
97
|
+
initialValues: { password: '', confirmPassword: '' },
|
|
98
|
+
validationSchema: {
|
|
99
|
+
confirmPassword: crossFieldValidator(
|
|
100
|
+
['password', 'confirmPassword'],
|
|
101
|
+
(values) => {
|
|
102
|
+
if (values.password !== values.confirmPassword) {
|
|
103
|
+
return 'Passwords must match';
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Method 2: Direct access to allValues
|
|
112
|
+
const form = useForm({
|
|
113
|
+
initialValues: { password: '', confirmPassword: '' },
|
|
114
|
+
validationSchema: {
|
|
115
|
+
confirmPassword: (value, allValues) => {
|
|
116
|
+
if (value !== allValues.password) {
|
|
117
|
+
return 'Passwords must match';
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Async Validation with Debouncing
|
|
126
|
+
|
|
127
|
+
Optimize async validators (like API calls) with built-in debouncing to prevent excessive requests:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { useForm, asyncValidator } from '@page-speed/forms/validation';
|
|
131
|
+
|
|
132
|
+
const checkUsernameAvailability = async (username: string) => {
|
|
133
|
+
const response = await fetch(`/api/check-username?username=${username}`);
|
|
134
|
+
const { available } = await response.json();
|
|
135
|
+
return available ? undefined : 'Username already taken';
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const form = useForm({
|
|
139
|
+
initialValues: { username: '' },
|
|
140
|
+
validationSchema: {
|
|
141
|
+
// Debounce async validation by 500ms
|
|
142
|
+
username: asyncValidator(
|
|
143
|
+
checkUsernameAvailability,
|
|
144
|
+
{ delay: 500, trailing: true }
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Debounce Options:**
|
|
151
|
+
- `delay`: Milliseconds to wait (default: 300ms)
|
|
152
|
+
- `leading`: Validate immediately on first change (default: false)
|
|
153
|
+
- `trailing`: Validate after delay expires (default: true)
|
|
154
|
+
|
|
155
|
+
The `asyncValidator` wrapper also includes automatic race condition prevention, ensuring only the latest validation result is used.
|
|
156
|
+
|
|
157
|
+
### Validation Rules Library
|
|
158
|
+
|
|
159
|
+
Use pre-built, tree-shakable validation rules for common scenarios:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import {
|
|
163
|
+
required,
|
|
164
|
+
email,
|
|
165
|
+
url,
|
|
166
|
+
phone,
|
|
167
|
+
minLength,
|
|
168
|
+
maxLength,
|
|
169
|
+
min,
|
|
170
|
+
max,
|
|
171
|
+
pattern,
|
|
172
|
+
matches,
|
|
173
|
+
oneOf,
|
|
174
|
+
creditCard,
|
|
175
|
+
postalCode,
|
|
176
|
+
alpha,
|
|
177
|
+
alphanumeric,
|
|
178
|
+
numeric,
|
|
179
|
+
integer,
|
|
180
|
+
compose
|
|
181
|
+
} from '@page-speed/forms/validation/rules';
|
|
182
|
+
|
|
183
|
+
const form = useForm({
|
|
184
|
+
initialValues: {
|
|
185
|
+
email: '',
|
|
186
|
+
password: '',
|
|
187
|
+
confirmPassword: '',
|
|
188
|
+
age: 0,
|
|
189
|
+
username: '',
|
|
190
|
+
cardNumber: ''
|
|
191
|
+
},
|
|
192
|
+
validationSchema: {
|
|
193
|
+
email: compose(
|
|
194
|
+
required({ message: 'Email is required' }),
|
|
195
|
+
email({ message: 'Invalid email format' })
|
|
196
|
+
),
|
|
197
|
+
password: compose(
|
|
198
|
+
required(),
|
|
199
|
+
minLength(8, { message: 'Password must be at least 8 characters' })
|
|
200
|
+
),
|
|
201
|
+
confirmPassword: matches('password', { message: 'Passwords must match' }),
|
|
202
|
+
age: compose(
|
|
203
|
+
required(),
|
|
204
|
+
numeric({ message: 'Age must be a number' }),
|
|
205
|
+
min(18, { message: 'Must be 18 or older' })
|
|
206
|
+
),
|
|
207
|
+
username: compose(
|
|
208
|
+
required(),
|
|
209
|
+
alphanumeric({ message: 'Only letters and numbers allowed' }),
|
|
210
|
+
minLength(3),
|
|
211
|
+
maxLength(20)
|
|
212
|
+
),
|
|
213
|
+
cardNumber: creditCard({ message: 'Invalid credit card number' })
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Available Validators:**
|
|
219
|
+
|
|
220
|
+
| Validator | Description | Example |
|
|
221
|
+
|-----------|-------------|---------|
|
|
222
|
+
| `required()` | Field must have a value | `required({ message: 'Required' })` |
|
|
223
|
+
| `email()` | Valid email format (RFC 5322) | `email()` |
|
|
224
|
+
| `url()` | Valid URL format | `url()` |
|
|
225
|
+
| `phone()` | US phone number format | `phone()` |
|
|
226
|
+
| `minLength(n)` | Minimum string/array length | `minLength(3)` |
|
|
227
|
+
| `maxLength(n)` | Maximum string/array length | `maxLength(100)` |
|
|
228
|
+
| `min(n)` | Minimum numeric value | `min(0)` |
|
|
229
|
+
| `max(n)` | Maximum numeric value | `max(100)` |
|
|
230
|
+
| `pattern(regex)` | Custom regex pattern | `pattern(/^[A-Z]+$/)` |
|
|
231
|
+
| `matches(field)` | Match another field | `matches('password')` |
|
|
232
|
+
| `oneOf(values)` | Value in allowed list | `oneOf(['a', 'b', 'c'])` |
|
|
233
|
+
| `creditCard()` | Valid credit card (Luhn) | `creditCard()` |
|
|
234
|
+
| `postalCode()` | US ZIP code format | `postalCode()` |
|
|
235
|
+
| `alpha()` | Alphabetic characters only | `alpha()` |
|
|
236
|
+
| `alphanumeric()` | Letters and numbers only | `alphanumeric()` |
|
|
237
|
+
| `numeric()` | Valid number | `numeric()` |
|
|
238
|
+
| `integer()` | Whole number | `integer()` |
|
|
239
|
+
| `compose(...)` | Combine multiple validators | `compose(required(), email())` |
|
|
240
|
+
|
|
241
|
+
### Custom Error Messages & Internationalization
|
|
242
|
+
|
|
243
|
+
Customize error messages globally for internationalization support:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { setErrorMessages } from '@page-speed/forms/validation/utils';
|
|
247
|
+
|
|
248
|
+
// Set custom messages (e.g., Spanish translations)
|
|
249
|
+
setErrorMessages({
|
|
250
|
+
required: 'Este campo es obligatorio',
|
|
251
|
+
email: 'Por favor ingrese un correo electrónico válido',
|
|
252
|
+
minLength: ({ min }) => `Debe tener al menos ${min} caracteres`,
|
|
253
|
+
maxLength: ({ max }) => `No debe exceder ${max} caracteres`,
|
|
254
|
+
phone: 'Por favor ingrese un número de teléfono válido'
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Use with validation rules
|
|
258
|
+
import { required, email, minLength } from '@page-speed/forms/validation/rules';
|
|
259
|
+
|
|
260
|
+
const form = useForm({
|
|
261
|
+
initialValues: { email: '', password: '' },
|
|
262
|
+
validationSchema: {
|
|
263
|
+
email: compose(required(), email()),
|
|
264
|
+
password: compose(required(), minLength(8))
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Message Template Functions:**
|
|
270
|
+
|
|
271
|
+
Error messages support template functions with parameter interpolation:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
setErrorMessages({
|
|
275
|
+
minLength: ({ min }) => `Must be at least ${min} characters`,
|
|
276
|
+
max: ({ max }) => `Cannot exceed ${max}`,
|
|
277
|
+
matches: ({ field }) => `Must match ${field}`
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Per-Field Custom Messages:**
|
|
282
|
+
|
|
283
|
+
Override global messages on a per-field basis:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const form = useForm({
|
|
287
|
+
initialValues: { email: '' },
|
|
288
|
+
validationSchema: {
|
|
289
|
+
email: required({ message: 'Please provide your email address' })
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Conditional Validation
|
|
295
|
+
|
|
296
|
+
Validate fields only when certain conditions are met:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { when, required, minLength } from '@page-speed/forms/validation';
|
|
300
|
+
|
|
301
|
+
const form = useForm({
|
|
302
|
+
initialValues: { accountType: 'personal', companyName: '' },
|
|
303
|
+
validationSchema: {
|
|
304
|
+
// Only require company name for business accounts
|
|
305
|
+
companyName: when(
|
|
306
|
+
(allValues) => allValues.accountType === 'business',
|
|
307
|
+
compose(
|
|
308
|
+
required({ message: 'Company name is required for business accounts' }),
|
|
309
|
+
minLength(3)
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Built-in Input Components
|
|
317
|
+
|
|
318
|
+
`@page-speed/forms` includes a comprehensive set of accessible, production-ready input components that work seamlessly with the form hooks.
|
|
319
|
+
|
|
320
|
+
### Basic Inputs
|
|
321
|
+
|
|
322
|
+
#### TextInput
|
|
323
|
+
Standard text input with support for various types (text, email, password, etc.):
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { TextInput } from '@page-speed/forms/inputs';
|
|
327
|
+
|
|
328
|
+
<Field name="email" label="Email">
|
|
329
|
+
{({ field }) => <TextInput {...field} type="email" placeholder="Enter email" />}
|
|
330
|
+
</Field>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### TextArea
|
|
334
|
+
Multi-line text input:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { TextArea } from '@page-speed/forms/inputs';
|
|
338
|
+
|
|
339
|
+
<Field name="description" label="Description">
|
|
340
|
+
{({ field }) => <TextArea {...field} rows={5} placeholder="Enter description" />}
|
|
341
|
+
</Field>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### Checkbox & CheckboxGroup
|
|
345
|
+
Single checkbox or group of checkboxes:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { Checkbox, CheckboxGroup } from '@page-speed/forms/inputs';
|
|
349
|
+
|
|
350
|
+
// Single checkbox
|
|
351
|
+
<Field name="terms" label="Terms">
|
|
352
|
+
{({ field }) => <Checkbox {...field} label="I agree to the terms" />}
|
|
353
|
+
</Field>
|
|
354
|
+
|
|
355
|
+
// Checkbox group
|
|
356
|
+
<Field name="interests" label="Interests">
|
|
357
|
+
{({ field }) => (
|
|
358
|
+
<CheckboxGroup
|
|
359
|
+
{...field}
|
|
360
|
+
options={[
|
|
361
|
+
{ label: 'Sports', value: 'sports' },
|
|
362
|
+
{ label: 'Music', value: 'music' },
|
|
363
|
+
{ label: 'Travel', value: 'travel' }
|
|
364
|
+
]}
|
|
365
|
+
/>
|
|
366
|
+
)}
|
|
367
|
+
</Field>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### Radio
|
|
371
|
+
Radio button group:
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
import { Radio } from '@page-speed/forms/inputs';
|
|
375
|
+
|
|
376
|
+
<Field name="plan" label="Select Plan">
|
|
377
|
+
{({ field }) => (
|
|
378
|
+
<Radio
|
|
379
|
+
{...field}
|
|
380
|
+
options={[
|
|
381
|
+
{ label: 'Basic', value: 'basic' },
|
|
382
|
+
{ label: 'Pro', value: 'pro' },
|
|
383
|
+
{ label: 'Enterprise', value: 'enterprise' }
|
|
384
|
+
]}
|
|
385
|
+
/>
|
|
386
|
+
)}
|
|
387
|
+
</Field>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
#### Select
|
|
391
|
+
Dropdown select with support for single and multi-select:
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { Select } from '@page-speed/forms/inputs';
|
|
395
|
+
|
|
396
|
+
// Single select
|
|
397
|
+
<Field name="country" label="Country">
|
|
398
|
+
{({ field }) => (
|
|
399
|
+
<Select
|
|
400
|
+
{...field}
|
|
401
|
+
options={[
|
|
402
|
+
{ label: 'United States', value: 'us' },
|
|
403
|
+
{ label: 'Canada', value: 'ca' },
|
|
404
|
+
{ label: 'United Kingdom', value: 'uk' }
|
|
405
|
+
]}
|
|
406
|
+
searchable
|
|
407
|
+
clearable
|
|
408
|
+
/>
|
|
409
|
+
)}
|
|
410
|
+
</Field>
|
|
411
|
+
|
|
412
|
+
// Multi-select
|
|
413
|
+
<Field name="skills" label="Skills">
|
|
414
|
+
{({ field }) => (
|
|
415
|
+
<Select
|
|
416
|
+
{...field}
|
|
417
|
+
multiple
|
|
418
|
+
options={[
|
|
419
|
+
{ label: 'JavaScript', value: 'js' },
|
|
420
|
+
{ label: 'TypeScript', value: 'ts' },
|
|
421
|
+
{ label: 'React', value: 'react' }
|
|
422
|
+
]}
|
|
423
|
+
searchable
|
|
424
|
+
/>
|
|
425
|
+
)}
|
|
426
|
+
</Field>
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Advanced Inputs
|
|
430
|
+
|
|
431
|
+
#### DatePicker
|
|
432
|
+
Date selection with calendar popup:
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
import { DatePicker } from '@page-speed/forms/inputs';
|
|
436
|
+
|
|
437
|
+
<Field name="birthdate" label="Birth Date">
|
|
438
|
+
{({ field }) => (
|
|
439
|
+
<DatePicker
|
|
440
|
+
{...field}
|
|
441
|
+
placeholder="Select date"
|
|
442
|
+
dateFormat="MM/dd/yyyy"
|
|
443
|
+
minDate={new Date(1900, 0, 1)}
|
|
444
|
+
maxDate={new Date()}
|
|
445
|
+
clearable
|
|
446
|
+
/>
|
|
447
|
+
)}
|
|
448
|
+
</Field>
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Props:**
|
|
452
|
+
- `dateFormat`: Date display format (default: "MM/dd/yyyy")
|
|
453
|
+
- `minDate`, `maxDate`: Restrict selectable dates
|
|
454
|
+
- `isDateDisabled`: Custom function to disable specific dates
|
|
455
|
+
- `clearable`: Show clear button
|
|
456
|
+
- `showTodayButton`: Show "Today" button
|
|
457
|
+
|
|
458
|
+
#### TimePicker
|
|
459
|
+
Time selection with hour/minute/period selectors:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { TimePicker } from '@page-speed/forms/inputs';
|
|
463
|
+
|
|
464
|
+
<Field name="appointmentTime" label="Appointment Time">
|
|
465
|
+
{({ field }) => (
|
|
466
|
+
<TimePicker
|
|
467
|
+
{...field}
|
|
468
|
+
placeholder="Select time"
|
|
469
|
+
use24Hour={false}
|
|
470
|
+
minuteStep={15}
|
|
471
|
+
clearable
|
|
472
|
+
/>
|
|
473
|
+
)}
|
|
474
|
+
</Field>
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Props:**
|
|
478
|
+
- `use24Hour`: Use 24-hour format (default: false)
|
|
479
|
+
- `minuteStep`: Minute increment (default: 1)
|
|
480
|
+
- `clearable`: Show clear button
|
|
481
|
+
|
|
482
|
+
#### DateRangePicker
|
|
483
|
+
Date range selection with start and end dates:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
import { DateRangePicker } from '@page-speed/forms/inputs';
|
|
487
|
+
|
|
488
|
+
<Field name="dateRange" label="Date Range">
|
|
489
|
+
{({ field }) => (
|
|
490
|
+
<DateRangePicker
|
|
491
|
+
{...field}
|
|
492
|
+
placeholder="Select date range"
|
|
493
|
+
separator=" - "
|
|
494
|
+
minDate={new Date()}
|
|
495
|
+
clearable
|
|
496
|
+
/>
|
|
497
|
+
)}
|
|
498
|
+
</Field>
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**Props:**
|
|
502
|
+
- `separator`: String between start and end dates (default: " - ")
|
|
503
|
+
- `minDate`, `maxDate`: Restrict selectable dates
|
|
504
|
+
- `isDateDisabled`: Custom function to disable specific dates
|
|
505
|
+
- `clearable`: Show clear button
|
|
506
|
+
|
|
507
|
+
#### RichTextEditor
|
|
508
|
+
WYSIWYG and Markdown editor with toolbar:
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
import { RichTextEditor } from '@page-speed/forms/inputs';
|
|
512
|
+
|
|
513
|
+
<Field name="content" label="Content">
|
|
514
|
+
{({ field }) => (
|
|
515
|
+
<RichTextEditor
|
|
516
|
+
{...field}
|
|
517
|
+
placeholder="Enter content..."
|
|
518
|
+
minHeight="200px"
|
|
519
|
+
maxHeight="600px"
|
|
520
|
+
allowModeSwitch
|
|
521
|
+
defaultMode="wysiwyg"
|
|
522
|
+
/>
|
|
523
|
+
)}
|
|
524
|
+
</Field>
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Props:**
|
|
528
|
+
- `defaultMode`: "wysiwyg" or "markdown" (default: "wysiwyg")
|
|
529
|
+
- `allowModeSwitch`: Enable mode toggle button
|
|
530
|
+
- `minHeight`, `maxHeight`: Editor height constraints
|
|
531
|
+
- `customButtons`: Add custom toolbar buttons
|
|
532
|
+
|
|
533
|
+
**Features:**
|
|
534
|
+
- WYSIWYG mode: Bold, Italic, Underline, Headings, Lists, Links
|
|
535
|
+
- Markdown mode: Direct markdown editing
|
|
536
|
+
- Automatic HTML ↔ Markdown conversion
|
|
537
|
+
|
|
538
|
+
#### FileInput
|
|
539
|
+
File upload with drag-and-drop, progress indicators, and image cropping:
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
import { FileInput } from '@page-speed/forms/inputs';
|
|
543
|
+
|
|
544
|
+
<Field name="avatar" label="Profile Picture">
|
|
545
|
+
{({ field }) => (
|
|
546
|
+
<FileInput
|
|
547
|
+
{...field}
|
|
548
|
+
accept="image/*"
|
|
549
|
+
maxSize={5 * 1024 * 1024} // 5MB
|
|
550
|
+
maxFiles={1}
|
|
551
|
+
showProgress
|
|
552
|
+
uploadProgress={uploadProgress}
|
|
553
|
+
enableCropping
|
|
554
|
+
cropAspectRatio={1}
|
|
555
|
+
onCropComplete={(file) => console.log('Cropped:', file)}
|
|
556
|
+
/>
|
|
557
|
+
)}
|
|
558
|
+
</Field>
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**Props:**
|
|
562
|
+
- `accept`: File type filter (e.g., "image/*", ".pdf")
|
|
563
|
+
- `multiple`: Allow multiple files
|
|
564
|
+
- `maxFiles`: Maximum number of files
|
|
565
|
+
- `maxSize`: Maximum file size in bytes
|
|
566
|
+
- `showPreview`: Show file previews
|
|
567
|
+
- `showProgress`: Display upload progress bars
|
|
568
|
+
- `uploadProgress`: Object mapping filenames to progress percentages
|
|
569
|
+
- `enableCropping`: Enable image cropping for image files
|
|
570
|
+
- `cropAspectRatio`: Crop aspect ratio (e.g., 16/9, 1 for square)
|
|
571
|
+
- `onCropComplete`: Callback when cropping is complete
|
|
572
|
+
|
|
573
|
+
**Features:**
|
|
574
|
+
- Drag-and-drop support
|
|
575
|
+
- File type and size validation
|
|
576
|
+
- Image previews with thumbnails
|
|
577
|
+
- Upload progress indicators with percentage
|
|
578
|
+
- Interactive image cropping with zoom
|
|
579
|
+
- Multiple file support
|
|
580
|
+
- Accessible file selection
|
|
581
|
+
|
|
80
582
|
## Performance Notes
|
|
81
583
|
|
|
82
584
|
Performance is a core facet of everything we build at OpenSite AI. The library is optimized for minimal re-renders and efficient form state updates, ensuring your applications remain responsive and fast.
|
package/dist/core.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var React2 = require('react');
|
|
4
4
|
var react = require('@legendapp/state/react');
|
|
5
|
+
var useMap = require('@opensite/hooks/core/useMap');
|
|
5
6
|
|
|
6
7
|
function _interopNamespace(e) {
|
|
7
8
|
if (e && e.__esModule) return e;
|
|
@@ -45,12 +46,20 @@ function useForm(options) {
|
|
|
45
46
|
hasValidated: {}
|
|
46
47
|
});
|
|
47
48
|
const validationInProgress = React2.useRef(/* @__PURE__ */ new Set());
|
|
49
|
+
const [, fieldMetadataActions] = useMap.useMap();
|
|
48
50
|
const validateField = React2.useCallback(
|
|
49
51
|
async (field) => {
|
|
50
52
|
const validators = validationSchema?.[field];
|
|
51
53
|
if (!validators) return void 0;
|
|
52
54
|
const fieldKey = String(field);
|
|
53
55
|
validationInProgress.current.add(fieldKey);
|
|
56
|
+
const currentMeta = fieldMetadataActions.get(fieldKey) || {
|
|
57
|
+
validationCount: 0
|
|
58
|
+
};
|
|
59
|
+
fieldMetadataActions.set(fieldKey, {
|
|
60
|
+
lastValidated: Date.now(),
|
|
61
|
+
validationCount: currentMeta.validationCount + 1
|
|
62
|
+
});
|
|
54
63
|
try {
|
|
55
64
|
const value = state$.values[field].get();
|
|
56
65
|
const allValues = state$.values.get();
|
|
@@ -73,7 +82,7 @@ function useForm(options) {
|
|
|
73
82
|
return errorMessage;
|
|
74
83
|
}
|
|
75
84
|
},
|
|
76
|
-
[validationSchema, state
|
|
85
|
+
[validationSchema, state$, fieldMetadataActions]
|
|
77
86
|
);
|
|
78
87
|
const validateForm = React2.useCallback(async () => {
|
|
79
88
|
if (!validationSchema) return {};
|
|
@@ -123,10 +132,11 @@ function useForm(options) {
|
|
|
123
132
|
state$.isSubmitting.set(false);
|
|
124
133
|
state$.status.set("idle");
|
|
125
134
|
state$.hasValidated.set({});
|
|
135
|
+
fieldMetadataActions.clear();
|
|
126
136
|
if (debug) {
|
|
127
137
|
console.log("[useForm] Form reset");
|
|
128
138
|
}
|
|
129
|
-
}, [state$, debug]);
|
|
139
|
+
}, [state$, fieldMetadataActions, debug]);
|
|
130
140
|
const handleSubmit = React2.useCallback(
|
|
131
141
|
async (e) => {
|
|
132
142
|
e?.preventDefault();
|
|
@@ -202,14 +212,18 @@ function useForm(options) {
|
|
|
202
212
|
const getFieldMeta = React2.useCallback(
|
|
203
213
|
(field) => {
|
|
204
214
|
const fieldKey = String(field);
|
|
215
|
+
const metadata = fieldMetadataActions.get(fieldKey);
|
|
205
216
|
return {
|
|
206
217
|
error: state$.errors[field].get(),
|
|
207
218
|
touched: state$.touched[field].get() ?? false,
|
|
208
219
|
isDirty: state$.values[field].get() !== state$.initialValues[field].get(),
|
|
209
|
-
isValidating: validationInProgress.current.has(fieldKey)
|
|
220
|
+
isValidating: validationInProgress.current.has(fieldKey),
|
|
221
|
+
// Additional metadata from @opensite/hooks
|
|
222
|
+
validationCount: metadata?.validationCount,
|
|
223
|
+
lastValidated: metadata?.lastValidated
|
|
210
224
|
};
|
|
211
225
|
},
|
|
212
|
-
[state
|
|
226
|
+
[state$, fieldMetadataActions]
|
|
213
227
|
);
|
|
214
228
|
const values = react.useSelector(() => state$.values.get());
|
|
215
229
|
const errors = react.useSelector(() => state$.errors.get());
|