@page-speed/forms 0.1.0 → 0.1.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/README.md +51 -426
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,469 +1,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# OpenSite Page Speed Forms
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe form state management and validation for React applications.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- =� **Tree-Shakable**: Import only what you need - Core module starts at 13 KB gzipped
|
|
9
|
-
- � **Built on @legendapp/state**: Observable-based state management for optimal performance
|
|
10
|
-
- **Valibot Integration**: Lightweight validation (95% smaller than Zod)
|
|
11
|
-
- <� **TypeScript-First**: Full type safety with comprehensive type definitions
|
|
12
|
-
- **Accessible**: ARIA attributes and semantic HTML out of the box
|
|
13
|
-
- = **Progressive Enhancement**: Forms work without JavaScript
|
|
14
|
-
- <� **Unstyled**: Bring your own styles - no CSS to override
|
|
7
|
+
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.
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
Learn more at [opensite.ai](https://opensite.ai).
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
# Using pnpm (recommended)
|
|
20
|
-
pnpm add @page-speed/forms
|
|
11
|
+
## Key Features
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
- Type-safe form state management with TypeScript.
|
|
14
|
+
- Flexible validation schemas supporting both synchronous and asynchronous validation.
|
|
15
|
+
- Modular useForm and useField hooks for complete form and field control.
|
|
16
|
+
- Built-in support for form submission and error handling.
|
|
17
|
+
- Configurable validation modes: `onChange`, `onBlur`, and `onSubmit`.
|
|
24
18
|
|
|
25
|
-
|
|
26
|
-
pnpm add @legendapp/state
|
|
27
|
-
```
|
|
19
|
+
## Installation
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
To install OpenSite Page Speed Forms, ensure you have Node.js and npm installed, then run:
|
|
30
22
|
|
|
31
|
-
|
|
23
|
+
```
|
|
24
|
+
npm install @page-speed/forms
|
|
25
|
+
```
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
- **Valibot Adapter**: 392 B
|
|
36
|
-
- **Full Bundle**: 13.25 KB
|
|
27
|
+
Dependencies:
|
|
28
|
+
- React
|
|
37
29
|
|
|
38
30
|
## Quick Start
|
|
39
31
|
|
|
40
|
-
|
|
32
|
+
Here is a basic example to get started with OpenSite Page Speed Forms in your React application:
|
|
41
33
|
|
|
42
|
-
```
|
|
43
|
-
import
|
|
44
|
-
import {
|
|
34
|
+
```typescript
|
|
35
|
+
import React from 'react';
|
|
36
|
+
import { useForm, Form } from '@page-speed/forms';
|
|
45
37
|
|
|
46
|
-
function
|
|
38
|
+
function MyForm() {
|
|
47
39
|
const form = useForm({
|
|
48
|
-
initialValues: {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
validationSchema: {
|
|
53
|
-
email: (value) => !value ? 'Required' : undefined,
|
|
54
|
-
password: (value) => value.length < 8 ? 'Too short' : undefined,
|
|
55
|
-
},
|
|
56
|
-
onSubmit: async (values) => {
|
|
57
|
-
await login(values);
|
|
58
|
-
},
|
|
40
|
+
initialValues: { email: '' },
|
|
41
|
+
onSubmit: (values) => {
|
|
42
|
+
console.log('Form Submitted:', values);
|
|
43
|
+
}
|
|
59
44
|
});
|
|
60
45
|
|
|
61
46
|
return (
|
|
62
47
|
<Form form={form}>
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
</Field>
|
|
71
|
-
|
|
72
|
-
<Field name="password" label="Password">
|
|
73
|
-
{({ field, meta }) => (
|
|
74
|
-
<>
|
|
75
|
-
<TextInput {...field} type="password" error={!!meta.error} />
|
|
76
|
-
{meta.error && <span>{meta.error}</span>}
|
|
77
|
-
</>
|
|
78
|
-
)}
|
|
79
|
-
</Field>
|
|
80
|
-
|
|
81
|
-
<button type="submit" disabled={form.isSubmitting}>
|
|
82
|
-
Submit
|
|
83
|
-
</button>
|
|
48
|
+
<input
|
|
49
|
+
name="email"
|
|
50
|
+
value={form.values.email}
|
|
51
|
+
onChange={(e) => form.setFieldValue('email', e.target.value)}
|
|
52
|
+
onBlur={() => form.setFieldTouched('email', true)}
|
|
53
|
+
/>
|
|
54
|
+
<button type="submit">Submit</button>
|
|
84
55
|
</Form>
|
|
85
56
|
);
|
|
86
57
|
}
|
|
87
58
|
```
|
|
88
59
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
```tsx
|
|
92
|
-
import { useForm } from '@page-speed/forms';
|
|
93
|
-
import { createValibotSchema } from '@page-speed/forms/validation/valibot';
|
|
94
|
-
import * as v from 'valibot';
|
|
95
|
-
|
|
96
|
-
const LoginSchema = v.object({
|
|
97
|
-
email: v.pipe(v.string(), v.email('Invalid email')),
|
|
98
|
-
password: v.pipe(v.string(), v.minLength(8, 'Too short')),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
function LoginForm() {
|
|
102
|
-
const form = useForm({
|
|
103
|
-
initialValues: { email: '', password: '' },
|
|
104
|
-
validationSchema: createValibotSchema(LoginSchema),
|
|
105
|
-
onSubmit: async (values) => {
|
|
106
|
-
await login(values);
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// ... rest of form
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## API Reference
|
|
115
|
-
|
|
116
|
-
### `useForm(options)`
|
|
117
|
-
|
|
118
|
-
Main hook for creating form state.
|
|
119
|
-
|
|
120
|
-
**Options:**
|
|
121
|
-
|
|
122
|
-
- `initialValues` (required): Initial form values
|
|
123
|
-
- `validationSchema`: Schema mapping field names to validators
|
|
124
|
-
- `validateOn`: When to validate - `"onBlur"` (default) | `"onChange"` | `"onSubmit"`
|
|
125
|
-
- `revalidateOn`: When to revalidate after first validation - `"onChange"` (default) | `"onBlur"`
|
|
126
|
-
- `onSubmit` (required): Submit handler function
|
|
127
|
-
- `onError`: Error handler for validation failures
|
|
128
|
-
- `debug`: Enable debug logging
|
|
60
|
+
## Configuration or Advanced Usage
|
|
129
61
|
|
|
130
|
-
|
|
62
|
+
OpenSite Page Speed Forms can be customized with various options:
|
|
131
63
|
|
|
132
64
|
```typescript
|
|
133
|
-
{
|
|
134
|
-
// State
|
|
135
|
-
values: T;
|
|
136
|
-
errors: FormErrors<T>;
|
|
137
|
-
touched: TouchedFields<T>;
|
|
138
|
-
isSubmitting: boolean;
|
|
139
|
-
isValid: boolean;
|
|
140
|
-
isDirty: boolean;
|
|
141
|
-
status: 'idle' | 'submitting' | 'success' | 'error';
|
|
142
|
-
|
|
143
|
-
// Actions
|
|
144
|
-
handleSubmit: (e?: React.FormEvent) => Promise<void>;
|
|
145
|
-
setFieldValue: (field: keyof T, value: T[field]) => void;
|
|
146
|
-
setFieldError: (field: keyof T, error: string) => void;
|
|
147
|
-
setFieldTouched: (field: keyof T, touched: boolean) => void;
|
|
148
|
-
validateForm: () => Promise<FormErrors<T>>;
|
|
149
|
-
validateField: (field: keyof T) => Promise<string | undefined>;
|
|
150
|
-
resetForm: () => void;
|
|
151
|
-
getFieldProps: (field: keyof T) => FieldInputProps;
|
|
152
|
-
getFieldMeta: (field: keyof T) => FieldMeta;
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### `<Form>`
|
|
157
|
-
|
|
158
|
-
Progressive enhancement wrapper component.
|
|
159
|
-
|
|
160
|
-
```tsx
|
|
161
|
-
<Form
|
|
162
|
-
form={form}
|
|
163
|
-
action="/api/endpoint" // Fallback for no-JS
|
|
164
|
-
method="post" // Fallback for no-JS
|
|
165
|
-
className="my-form"
|
|
166
|
-
>
|
|
167
|
-
{/* fields */}
|
|
168
|
-
</Form>
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### `<Field>`
|
|
172
|
-
|
|
173
|
-
Field wrapper with label, description, and error display.
|
|
174
|
-
|
|
175
|
-
```tsx
|
|
176
|
-
<Field
|
|
177
|
-
name="email"
|
|
178
|
-
label="Email Address"
|
|
179
|
-
description="We'll never share your email"
|
|
180
|
-
validate={(value) => !value ? 'Required' : undefined}
|
|
181
|
-
>
|
|
182
|
-
{({ field, meta, helpers }) => (
|
|
183
|
-
<TextInput {...field} error={!!meta.error} />
|
|
184
|
-
)}
|
|
185
|
-
</Field>
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### `useField(options)`
|
|
189
|
-
|
|
190
|
-
Field-level hook for accessing field state.
|
|
191
|
-
|
|
192
|
-
```tsx
|
|
193
|
-
const { field, meta, helpers } = useField({
|
|
194
|
-
name: 'email',
|
|
195
|
-
validate: (value) => !value ? 'Required' : undefined,
|
|
196
|
-
transform: (value) => value.toLowerCase(),
|
|
197
|
-
});
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### `<TextInput>`
|
|
201
|
-
|
|
202
|
-
Lightweight, accessible text input component.
|
|
203
|
-
|
|
204
|
-
```tsx
|
|
205
|
-
<TextInput
|
|
206
|
-
name="email"
|
|
207
|
-
value={value}
|
|
208
|
-
onChange={onChange}
|
|
209
|
-
onBlur={onBlur}
|
|
210
|
-
type="email"
|
|
211
|
-
placeholder="you@example.com"
|
|
212
|
-
error={hasError}
|
|
213
|
-
disabled={false}
|
|
214
|
-
required={true}
|
|
215
|
-
/>
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
## Validation
|
|
219
|
-
|
|
220
|
-
### Inline Validators
|
|
221
|
-
|
|
222
|
-
```tsx
|
|
223
65
|
const form = useForm({
|
|
224
66
|
initialValues: { email: '' },
|
|
225
67
|
validationSchema: {
|
|
226
|
-
email: (value) =>
|
|
227
|
-
if (!value) return 'Required';
|
|
228
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
229
|
-
return 'Invalid email';
|
|
230
|
-
}
|
|
231
|
-
return undefined;
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
onSubmit: async (values) => { /* ... */ },
|
|
235
|
-
});
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### Async Validation
|
|
239
|
-
|
|
240
|
-
```tsx
|
|
241
|
-
const form = useForm({
|
|
242
|
-
initialValues: { username: '' },
|
|
243
|
-
validationSchema: {
|
|
244
|
-
username: async (value) => {
|
|
245
|
-
const exists = await checkUsernameExists(value);
|
|
246
|
-
return exists ? 'Username taken' : undefined;
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
onSubmit: async (values) => { /* ... */ },
|
|
250
|
-
});
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
### Multiple Validators per Field
|
|
254
|
-
|
|
255
|
-
```tsx
|
|
256
|
-
const form = useForm({
|
|
257
|
-
initialValues: { password: '' },
|
|
258
|
-
validationSchema: {
|
|
259
|
-
password: [
|
|
260
|
-
(value) => !value ? 'Required' : undefined,
|
|
261
|
-
(value) => value.length < 8 ? 'Too short' : undefined,
|
|
262
|
-
(value) => !/[A-Z]/.test(value) ? 'Needs uppercase' : undefined,
|
|
263
|
-
],
|
|
68
|
+
email: (value) => value.includes('@') ? undefined : 'Invalid email'
|
|
264
69
|
},
|
|
265
|
-
|
|
70
|
+
validateOn: 'onBlur',
|
|
71
|
+
revalidateOn: 'onChange',
|
|
72
|
+
onSubmit: (values) => console.log(values),
|
|
73
|
+
onError: (errors) => console.error(errors),
|
|
74
|
+
debug: true
|
|
266
75
|
});
|
|
267
76
|
```
|
|
268
77
|
|
|
269
|
-
|
|
78
|
+
## Performance Notes
|
|
270
79
|
|
|
271
|
-
|
|
272
|
-
import { createValibotSchema } from '@page-speed/forms/validation/valibot';
|
|
273
|
-
import * as v from 'valibot';
|
|
80
|
+
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.
|
|
274
81
|
|
|
275
|
-
|
|
276
|
-
email: v.pipe(
|
|
277
|
-
v.string(),
|
|
278
|
-
v.email('Invalid email'),
|
|
279
|
-
v.endsWith('@company.com', 'Must be company email')
|
|
280
|
-
),
|
|
281
|
-
age: v.pipe(
|
|
282
|
-
v.number(),
|
|
283
|
-
v.minValue(18, 'Must be 18+')
|
|
284
|
-
),
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
const form = useForm({
|
|
288
|
-
initialValues: { email: '', age: 0 },
|
|
289
|
-
validationSchema: createValibotSchema(schema),
|
|
290
|
-
onSubmit: async (values) => { /* ... */ },
|
|
291
|
-
});
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
## Advanced Usage
|
|
295
|
-
|
|
296
|
-
### Conditional Validation
|
|
297
|
-
|
|
298
|
-
```tsx
|
|
299
|
-
const form = useForm({
|
|
300
|
-
initialValues: {
|
|
301
|
-
contactMethod: 'email',
|
|
302
|
-
email: '',
|
|
303
|
-
phone: '',
|
|
304
|
-
},
|
|
305
|
-
validationSchema: {
|
|
306
|
-
email: (value, allValues) => {
|
|
307
|
-
if (allValues.contactMethod === 'email' && !value) {
|
|
308
|
-
return 'Email required when email is selected';
|
|
309
|
-
}
|
|
310
|
-
return undefined;
|
|
311
|
-
},
|
|
312
|
-
phone: (value, allValues) => {
|
|
313
|
-
if (allValues.contactMethod === 'phone' && !value) {
|
|
314
|
-
return 'Phone required when phone is selected';
|
|
315
|
-
}
|
|
316
|
-
return undefined;
|
|
317
|
-
},
|
|
318
|
-
},
|
|
319
|
-
onSubmit: async (values) => { /* ... */ },
|
|
320
|
-
});
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
### Dynamic Forms
|
|
324
|
-
|
|
325
|
-
```tsx
|
|
326
|
-
function DynamicForm() {
|
|
327
|
-
const form = useForm({
|
|
328
|
-
initialValues: {
|
|
329
|
-
contacts: [{ name: '', email: '' }],
|
|
330
|
-
},
|
|
331
|
-
onSubmit: async (values) => { /* ... */ },
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
return (
|
|
335
|
-
<Form form={form}>
|
|
336
|
-
{form.values.contacts.map((contact, index) => (
|
|
337
|
-
<div key={index}>
|
|
338
|
-
<Field name={`contacts.${index}.name`}>
|
|
339
|
-
{({ field }) => <TextInput {...field} />}
|
|
340
|
-
</Field>
|
|
341
|
-
<Field name={`contacts.${index}.email`}>
|
|
342
|
-
{({ field }) => <TextInput {...field} type="email" />}
|
|
343
|
-
</Field>
|
|
344
|
-
</div>
|
|
345
|
-
))}
|
|
346
|
-
<button
|
|
347
|
-
type="button"
|
|
348
|
-
onClick={() => {
|
|
349
|
-
form.setFieldValue('contacts', [
|
|
350
|
-
...form.values.contacts,
|
|
351
|
-
{ name: '', email: '' },
|
|
352
|
-
]);
|
|
353
|
-
}}
|
|
354
|
-
>
|
|
355
|
-
Add Contact
|
|
356
|
-
</button>
|
|
357
|
-
</Form>
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
### Form Helpers in onSubmit
|
|
363
|
-
|
|
364
|
-
```tsx
|
|
365
|
-
const form = useForm({
|
|
366
|
-
initialValues: { email: '' },
|
|
367
|
-
onSubmit: async (values, helpers) => {
|
|
368
|
-
try {
|
|
369
|
-
await api.submit(values);
|
|
370
|
-
helpers.resetForm();
|
|
371
|
-
} catch (error) {
|
|
372
|
-
if (error.code === 'EMAIL_EXISTS') {
|
|
373
|
-
helpers.setFieldError('email', 'Email already exists');
|
|
374
|
-
} else {
|
|
375
|
-
helpers.setErrors({ email: 'Unexpected error' });
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
},
|
|
379
|
-
});
|
|
380
|
-
```
|
|
381
|
-
|
|
382
|
-
## Tree-Shaking
|
|
383
|
-
|
|
384
|
-
The library is designed for optimal tree-shaking. Import only what you need:
|
|
385
|
-
|
|
386
|
-
```tsx
|
|
387
|
-
// Import core functionality (13.11 KB)
|
|
388
|
-
import { useForm, Form, Field } from '@page-speed/forms/core';
|
|
389
|
-
|
|
390
|
-
// Import input components separately (502 B)
|
|
391
|
-
import { TextInput } from '@page-speed/forms/inputs';
|
|
392
|
-
|
|
393
|
-
// Import validation adapters separately (392 B)
|
|
394
|
-
import { createValibotSchema } from '@page-speed/forms/validation/valibot';
|
|
395
|
-
|
|
396
|
-
// Or import from main entry point
|
|
397
|
-
import { useForm } from '@page-speed/forms';
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
## Performance
|
|
401
|
-
|
|
402
|
-
The library uses @legendapp/state for optimal performance:
|
|
403
|
-
|
|
404
|
-
- **~1 re-render per change** vs ~10 for traditional hooks
|
|
405
|
-
- **Observable-based state**: Fine-grained reactivity at the field level
|
|
406
|
-
- **No unnecessary re-renders**: Parent form doesn't re-render when child field changes
|
|
407
|
-
- **Efficient validation**: Debounced and memoized by default
|
|
408
|
-
|
|
409
|
-
## Progressive Enhancement
|
|
410
|
-
|
|
411
|
-
Forms work without JavaScript by using native HTML form submission:
|
|
412
|
-
|
|
413
|
-
```tsx
|
|
414
|
-
<Form
|
|
415
|
-
form={form}
|
|
416
|
-
action="/api/endpoint" // Used when JS disabled
|
|
417
|
-
method="post"
|
|
418
|
-
>
|
|
419
|
-
{/* fields */}
|
|
420
|
-
</Form>
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
When JavaScript is available, `onSubmit` handles the submission. When JavaScript is disabled, the native HTML form submission takes over.
|
|
424
|
-
|
|
425
|
-
## Browser Support
|
|
426
|
-
|
|
427
|
-
- Modern browsers (Chrome, Firefox, Safari, Edge)
|
|
428
|
-
- Requires ES2020 support
|
|
429
|
-
- No IE11 support
|
|
430
|
-
|
|
431
|
-
## TypeScript
|
|
432
|
-
|
|
433
|
-
Fully typed with comprehensive type definitions:
|
|
434
|
-
|
|
435
|
-
```tsx
|
|
436
|
-
import type {
|
|
437
|
-
FormValues,
|
|
438
|
-
FormErrors,
|
|
439
|
-
TouchedFields,
|
|
440
|
-
ValidationSchema,
|
|
441
|
-
FieldValidator,
|
|
442
|
-
UseFormOptions,
|
|
443
|
-
UseFormReturn,
|
|
444
|
-
} from '@page-speed/forms/core';
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
## Examples
|
|
82
|
+
## Contributing
|
|
448
83
|
|
|
449
|
-
|
|
450
|
-
- Basic forms with inline validation
|
|
451
|
-
- Valibot schema integration
|
|
452
|
-
- Dynamic forms with conditional fields
|
|
453
|
-
- Progressive enhancement
|
|
454
|
-
- Async validation
|
|
84
|
+
We welcome contributions from the community to enhance OpenSite Page Speed Forms. Please refer to our [GitHub repository](https://github.com/opensite-ai) for guidelines and more information on how to get involved.
|
|
455
85
|
|
|
456
86
|
## License
|
|
457
87
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
## Contributing
|
|
461
|
-
|
|
462
|
-
Contributions welcome! Please read our contributing guidelines first.
|
|
88
|
+
Licensed under the BSD 3-Clause License. See the [LICENSE](./LICENSE) file for details.
|
|
463
89
|
|
|
464
|
-
##
|
|
90
|
+
## Related Projects
|
|
465
91
|
|
|
466
|
-
|
|
467
|
-
- [
|
|
468
|
-
- [
|
|
469
|
-
- [tsup](https://tsup.egoist.dev/) - TypeScript bundler
|
|
92
|
+
- [Domain Extractor](https://github.com/opensite-ai/domain_extractor)
|
|
93
|
+
- [Page Speed Hooks](https://github.com/opensite-ai/page-speed-hooks)
|
|
94
|
+
- Visit [opensite.ai](https://opensite.ai) for more tools and information.
|