performa 1.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/LICENSE +21 -0
- package/README.md +850 -0
- package/dist/index.cjs +1623 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +438 -0
- package/dist/index.d.ts +438 -0
- package/dist/index.js +1591 -0
- package/dist/index.js.map +1 -0
- package/dist/nextjs.cjs +1486 -0
- package/dist/nextjs.cjs.map +1 -0
- package/dist/nextjs.d.cts +77 -0
- package/dist/nextjs.d.ts +77 -0
- package/dist/nextjs.js +1483 -0
- package/dist/nextjs.js.map +1 -0
- package/dist/react-router.cjs +1466 -0
- package/dist/react-router.cjs.map +1 -0
- package/dist/react-router.d.cts +50 -0
- package/dist/react-router.d.ts +50 -0
- package/dist/react-router.js +1463 -0
- package/dist/react-router.js.map +1 -0
- package/dist/server-Cjhy29dZ.d.cts +159 -0
- package/dist/server-Cjhy29dZ.d.ts +159 -0
- package/dist/server.cjs +42 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +40 -0
- package/dist/server.js.map +1 -0
- package/dist/tanstack-start.cjs +1499 -0
- package/dist/tanstack-start.cjs.map +1 -0
- package/dist/tanstack-start.d.cts +78 -0
- package/dist/tanstack-start.d.ts +78 -0
- package/dist/tanstack-start.js +1496 -0
- package/dist/tanstack-start.js.map +1 -0
- package/package.json +140 -0
package/README.md
ADDED
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
# performa
|
|
2
|
+
|
|
3
|
+
A lightweight, framework-agnostic React form library with built-in server-side validation, TypeScript support, and adapters for popular React frameworks.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Framework Agnostic**: Works with Next.js, React Router, TanStack Start, and any React framework
|
|
8
|
+
- **Server-Side Validation**: Secure form validation with comprehensive validation rules
|
|
9
|
+
- **TypeScript First**: Full type safety with excellent IDE autocomplete
|
|
10
|
+
- **Accessible**: Built-in ARIA attributes and keyboard navigation
|
|
11
|
+
- **Themeable**: Fully customizable with Tailwind CSS or custom styling
|
|
12
|
+
- **Lightweight**: Core library is only 7.69 KB (brotli compressed)
|
|
13
|
+
- **File Uploads**: Built-in support for file validation and previews
|
|
14
|
+
- **Dark Mode**: Native dark mode support out of the box
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install performa
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Framework-Specific Installation
|
|
23
|
+
|
|
24
|
+
For Next.js (App Router):
|
|
25
|
+
```bash
|
|
26
|
+
npm install performa react-dom
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For React Router:
|
|
30
|
+
```bash
|
|
31
|
+
npm install performa react-router
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For TanStack Start:
|
|
35
|
+
```bash
|
|
36
|
+
npm install performa @tanstack/start
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### 1. Define Your Form Configuration
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { FormConfig } from 'performa';
|
|
45
|
+
|
|
46
|
+
const loginForm = {
|
|
47
|
+
key: 'login',
|
|
48
|
+
method: 'post',
|
|
49
|
+
action: '/api/login',
|
|
50
|
+
fields: {
|
|
51
|
+
email: {
|
|
52
|
+
type: 'email',
|
|
53
|
+
label: 'Email Address',
|
|
54
|
+
placeholder: 'Enter your email',
|
|
55
|
+
rules: {
|
|
56
|
+
required: true,
|
|
57
|
+
isEmail: true,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
password: {
|
|
61
|
+
type: 'password',
|
|
62
|
+
label: 'Password',
|
|
63
|
+
placeholder: 'Enter your password',
|
|
64
|
+
rules: {
|
|
65
|
+
required: true,
|
|
66
|
+
minLength: 8,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
remember: {
|
|
70
|
+
type: 'checkbox',
|
|
71
|
+
label: 'Remember me',
|
|
72
|
+
},
|
|
73
|
+
submit: {
|
|
74
|
+
type: 'submit',
|
|
75
|
+
label: 'Sign In',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
} satisfies FormConfig;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2. Choose Your Framework Adapter
|
|
82
|
+
|
|
83
|
+
#### Next.js (App Router)
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// app/login/page.tsx
|
|
87
|
+
'use client';
|
|
88
|
+
|
|
89
|
+
import { NextForm } from 'performa/nextjs';
|
|
90
|
+
import { loginForm } from './form-config';
|
|
91
|
+
|
|
92
|
+
export default function LoginPage() {
|
|
93
|
+
return (
|
|
94
|
+
<NextForm
|
|
95
|
+
config={loginForm}
|
|
96
|
+
action={submitLogin}
|
|
97
|
+
onSuccess={(data) => {
|
|
98
|
+
console.log('Login successful', data);
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// app/login/actions.ts
|
|
107
|
+
'use server';
|
|
108
|
+
|
|
109
|
+
import { validateForm } from 'performa/server';
|
|
110
|
+
import { loginForm } from './form-config';
|
|
111
|
+
|
|
112
|
+
export async function submitLogin(prevState: any, formData: FormData) {
|
|
113
|
+
const result = await validateForm(
|
|
114
|
+
new Request('', { method: 'POST', body: formData }),
|
|
115
|
+
loginForm
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (result.hasErrors) {
|
|
119
|
+
return { errors: result.errors };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Process login with result.values
|
|
123
|
+
const { email, password } = result.values;
|
|
124
|
+
|
|
125
|
+
// Your authentication logic here
|
|
126
|
+
|
|
127
|
+
return { success: true };
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### React Router
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// routes/login.tsx
|
|
135
|
+
import { ReactRouterForm } from 'performa/react-router';
|
|
136
|
+
import { loginForm } from './form-config';
|
|
137
|
+
|
|
138
|
+
export default function LoginRoute() {
|
|
139
|
+
return <ReactRouterForm config={loginForm} />;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Action handler
|
|
143
|
+
import { validateForm } from 'formbase/server';
|
|
144
|
+
|
|
145
|
+
export async function action({ request }: ActionFunctionArgs) {
|
|
146
|
+
const result = await validateForm(request, loginForm);
|
|
147
|
+
|
|
148
|
+
if (result.hasErrors) {
|
|
149
|
+
return { errors: result.errors };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Process login
|
|
153
|
+
return redirect('/dashboard');
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### TanStack Start
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { TanStackStartForm } from 'performa/tanstack-start';
|
|
161
|
+
import { createServerFn } from '@tanstack/start';
|
|
162
|
+
import { validateForm } from 'formbase/server';
|
|
163
|
+
|
|
164
|
+
const submitLogin = createServerFn({ method: 'POST' }).handler(
|
|
165
|
+
async ({ data }: { data: FormData }) => {
|
|
166
|
+
const result = await validateForm(
|
|
167
|
+
new Request('', { method: 'POST', body: data }),
|
|
168
|
+
loginForm
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (result.hasErrors) {
|
|
172
|
+
return { errors: result.errors };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { success: true };
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
export default function LoginPage() {
|
|
180
|
+
return <TanStackStartForm config={loginForm} action={submitLogin} />;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Form Configuration
|
|
185
|
+
|
|
186
|
+
### Field Types
|
|
187
|
+
|
|
188
|
+
formbase supports the following field types:
|
|
189
|
+
|
|
190
|
+
#### Text Inputs
|
|
191
|
+
- `text` - Standard text input
|
|
192
|
+
- `email` - Email input with validation
|
|
193
|
+
- `password` - Password input with masking
|
|
194
|
+
- `url` - URL input
|
|
195
|
+
- `tel` - Telephone number input
|
|
196
|
+
- `number` - Numeric input
|
|
197
|
+
- `date` - Date picker
|
|
198
|
+
- `time` - Time picker
|
|
199
|
+
|
|
200
|
+
#### Textareas
|
|
201
|
+
- `textarea` - Multi-line text input
|
|
202
|
+
|
|
203
|
+
#### Select Inputs
|
|
204
|
+
- `select` - Dropdown selection with options
|
|
205
|
+
|
|
206
|
+
#### Radio Inputs
|
|
207
|
+
- `radio` - Radio button group
|
|
208
|
+
|
|
209
|
+
#### Checkboxes and Toggles
|
|
210
|
+
- `checkbox` - Single checkbox
|
|
211
|
+
- `toggle` - Toggle switch
|
|
212
|
+
|
|
213
|
+
#### File Inputs
|
|
214
|
+
- `file` - File upload with validation and preview
|
|
215
|
+
|
|
216
|
+
#### Special Types
|
|
217
|
+
- `datetime` - Combined date and time picker
|
|
218
|
+
- `hidden` - Hidden input field
|
|
219
|
+
- `none` - Display-only field (no input)
|
|
220
|
+
- `submit` - Submit button
|
|
221
|
+
|
|
222
|
+
### Validation Rules
|
|
223
|
+
|
|
224
|
+
All validation rules are applied server-side for security:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
{
|
|
228
|
+
rules: {
|
|
229
|
+
required: true, // Field is required
|
|
230
|
+
minLength: 8, // Minimum character length
|
|
231
|
+
maxLength: 100, // Maximum character length
|
|
232
|
+
pattern: /^[A-Z0-9]+$/, // Custom regex pattern
|
|
233
|
+
matches: 'password', // Must match another field
|
|
234
|
+
isEmail: true, // Valid email format
|
|
235
|
+
isUrl: true, // Valid URL format
|
|
236
|
+
isPhone: true, // Valid phone number
|
|
237
|
+
isDate: true, // Valid date
|
|
238
|
+
isTime: true, // Valid time
|
|
239
|
+
isNumber: true, // Valid number
|
|
240
|
+
isInteger: true, // Valid integer
|
|
241
|
+
isAlphanumeric: true, // Only letters and numbers
|
|
242
|
+
isSlug: true, // Valid URL slug
|
|
243
|
+
isUUID: true, // Valid UUID
|
|
244
|
+
denyHtml: true, // Reject HTML tags
|
|
245
|
+
weakPasswordCheck: true, // Check against known breached passwords
|
|
246
|
+
minValue: 0, // Minimum numeric value
|
|
247
|
+
maxValue: 100, // Maximum numeric value
|
|
248
|
+
mustBeEither: ['admin', 'user'], // Value must be one of the options
|
|
249
|
+
mimeTypes: ['JPEG', 'PNG', 'PDF'], // Allowed file types
|
|
250
|
+
maxFileSize: 5 * 1024 * 1024, // Maximum file size in bytes
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Field Configuration Options
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
{
|
|
259
|
+
type: 'text', // Field type (required)
|
|
260
|
+
label: 'Username', // Field label (required)
|
|
261
|
+
placeholder: 'Enter username', // Placeholder text
|
|
262
|
+
defaultValue: '', // Default value
|
|
263
|
+
defaultChecked: false, // Default checked state (checkbox/toggle)
|
|
264
|
+
disabled: false, // Disable the field
|
|
265
|
+
className: 'custom-class', // Additional CSS classes
|
|
266
|
+
before: 'Help text above', // Content before the field
|
|
267
|
+
beforeClassName: 'text-sm', // Classes for before content
|
|
268
|
+
after: 'Help text below', // Content after the field
|
|
269
|
+
afterClassName: 'text-sm', // Classes for after content
|
|
270
|
+
uploadDir: 'avatars', // Upload directory for files
|
|
271
|
+
width: 'full', // Field width (full, half, third, quarter)
|
|
272
|
+
options: [ // Options for select/radio
|
|
273
|
+
{ value: 'opt1', label: 'Option 1' },
|
|
274
|
+
{ value: 'opt2', label: 'Option 2' },
|
|
275
|
+
],
|
|
276
|
+
rules: { /* validation rules */ },
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Server-Side Validation
|
|
281
|
+
|
|
282
|
+
### Validation Function
|
|
283
|
+
|
|
284
|
+
The `validateForm` function performs server-side validation and returns typed results:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { validateForm } from 'formbase/server';
|
|
288
|
+
|
|
289
|
+
const result = await validateForm(request, formConfig);
|
|
290
|
+
|
|
291
|
+
if (result.hasErrors) {
|
|
292
|
+
// result.errors contains field-specific error messages
|
|
293
|
+
return { errors: result.errors };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// result.values contains validated and typed form data
|
|
297
|
+
const { email, password } = result.values;
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Custom Error Messages
|
|
301
|
+
|
|
302
|
+
You can customize validation error messages:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { defaultErrorMessages } from 'performa/server';
|
|
306
|
+
|
|
307
|
+
// Customize individual messages
|
|
308
|
+
defaultErrorMessages.required = (label) => `${label} is mandatory`;
|
|
309
|
+
defaultErrorMessages.minLength = (label, min) =>
|
|
310
|
+
`${label} needs at least ${min} characters`;
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Return Type
|
|
314
|
+
|
|
315
|
+
errors: {
|
|
316
|
+
fieldName?: string; // Error message for each field
|
|
317
|
+
__server?: string; // Server-level error message
|
|
318
|
+
} | undefined;
|
|
319
|
+
values: {
|
|
320
|
+
fieldName: string | boolean | File; // Validated values
|
|
321
|
+
};
|
|
322
|
+
hasErrors: boolean; // Quick check for validation failure
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## File Uploads
|
|
327
|
+
|
|
328
|
+
### Configuration
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
{
|
|
332
|
+
avatar: {
|
|
333
|
+
type: 'file',
|
|
334
|
+
label: 'Profile Picture',
|
|
335
|
+
rules: {
|
|
336
|
+
required: true,
|
|
337
|
+
mimeTypes: ['JPEG', 'PNG', 'WEBP'],
|
|
338
|
+
maxFileSize: 2 * 1024 * 1024, // 2MB
|
|
339
|
+
},
|
|
340
|
+
uploadDir: 'avatars', // Optional: subdirectory for uploads
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Supported MIME Types
|
|
346
|
+
|
|
347
|
+
- Images: `JPEG`, `PNG`, `GIF`, `WEBP`, `SVG`, `BMP`, `TIFF`
|
|
348
|
+
- Documents: `PDF`, `DOC`, `DOCX`, `XLS`, `XLSX`, `PPT`, `PPTX`, `TXT`, `CSV`
|
|
349
|
+
- Archives: `ZIP`, `RAR`, `TAR`, `GZIP`
|
|
350
|
+
- Media: `MP3`, `MP4`, `WAV`, `AVI`, `MOV`
|
|
351
|
+
|
|
352
|
+
### File Preview
|
|
353
|
+
|
|
354
|
+
Files are automatically previewed if:
|
|
355
|
+
- A `baseUrl` prop is provided to the form component
|
|
356
|
+
- The file is an image type
|
|
357
|
+
- A `defaultValue` exists (for editing forms)
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
<NextForm
|
|
361
|
+
config={formConfig}
|
|
362
|
+
action={submitForm}
|
|
363
|
+
fileBaseUrl="https://cdn.example.com"
|
|
364
|
+
/>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Handling File Uploads
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
export async function submitForm(prevState: any, formData: FormData) {
|
|
371
|
+
const result = await validateForm(
|
|
372
|
+
new Request('', { method: 'POST', body: formData }),
|
|
373
|
+
formConfig
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (result.hasErrors) {
|
|
377
|
+
return { errors: result.errors };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const file = result.values.avatar as File;
|
|
381
|
+
|
|
382
|
+
// Upload file to storage
|
|
383
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
384
|
+
const filename = `${Date.now()}-${file.name}`;
|
|
385
|
+
|
|
386
|
+
// Save to file system, S3, etc.
|
|
387
|
+
await saveFile(filename, buffer);
|
|
388
|
+
|
|
389
|
+
return { success: true };
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Theming
|
|
394
|
+
|
|
395
|
+
### Default Theme
|
|
396
|
+
|
|
397
|
+
formbase comes with a default theme optimized for Tailwind CSS with dark mode support:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
import { FormThemeProvider } from 'performa';
|
|
401
|
+
|
|
402
|
+
function App() {
|
|
403
|
+
return (
|
|
404
|
+
<FormThemeProvider>
|
|
405
|
+
{/* Your forms here */}
|
|
406
|
+
</FormThemeProvider>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Custom Theme
|
|
412
|
+
|
|
413
|
+
Override the default theme with your own styles:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
import { FormThemeProvider } from 'formbase';
|
|
417
|
+
|
|
418
|
+
const customTheme = {
|
|
419
|
+
formGroup: 'mb-6',
|
|
420
|
+
label: {
|
|
421
|
+
base: 'block text-sm font-semibold mb-2',
|
|
422
|
+
required: 'text-red-600 ml-1',
|
|
423
|
+
},
|
|
424
|
+
input: {
|
|
425
|
+
base: 'w-full px-4 py-3 border rounded-lg focus:ring-2',
|
|
426
|
+
error: 'border-red-500 focus:ring-red-500',
|
|
427
|
+
},
|
|
428
|
+
error: 'text-red-600 text-sm mt-2',
|
|
429
|
+
button: {
|
|
430
|
+
primary: 'bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700',
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
function App() {
|
|
435
|
+
return (
|
|
436
|
+
<FormThemeProvider theme={customTheme}>
|
|
437
|
+
{/* Your forms here */}
|
|
438
|
+
</FormThemeProvider>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Theme Structure
|
|
444
|
+
|
|
445
|
+
The complete theme object structure:
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
{
|
|
449
|
+
form: string; // Form element styles
|
|
450
|
+
formGroup: string; // Field group wrapper
|
|
451
|
+
fieldset: string; // Fieldset styles
|
|
452
|
+
label: {
|
|
453
|
+
base: string; // Label base styles
|
|
454
|
+
required: string; // Required asterisk styles
|
|
455
|
+
};
|
|
456
|
+
input: {
|
|
457
|
+
base: string; // Input base styles
|
|
458
|
+
error: string; // Error state styles
|
|
459
|
+
};
|
|
460
|
+
textarea: {
|
|
461
|
+
base: string;
|
|
462
|
+
error: string;
|
|
463
|
+
};
|
|
464
|
+
select: {
|
|
465
|
+
base: string;
|
|
466
|
+
error: string;
|
|
467
|
+
};
|
|
468
|
+
checkbox: {
|
|
469
|
+
base: string;
|
|
470
|
+
label: string;
|
|
471
|
+
error: string;
|
|
472
|
+
};
|
|
473
|
+
radio: {
|
|
474
|
+
group: string;
|
|
475
|
+
base: string;
|
|
476
|
+
label: string;
|
|
477
|
+
};
|
|
478
|
+
toggle: {
|
|
479
|
+
wrapper: string;
|
|
480
|
+
base: string;
|
|
481
|
+
slider: string;
|
|
482
|
+
label: string;
|
|
483
|
+
};
|
|
484
|
+
file: {
|
|
485
|
+
dropzone: string;
|
|
486
|
+
dropzoneActive: string;
|
|
487
|
+
dropzoneError: string;
|
|
488
|
+
icon: string;
|
|
489
|
+
text: string;
|
|
490
|
+
hint: string;
|
|
491
|
+
};
|
|
492
|
+
datetime: {
|
|
493
|
+
input: string;
|
|
494
|
+
iconButton: string;
|
|
495
|
+
dropdown: string;
|
|
496
|
+
navButton: string;
|
|
497
|
+
monthYear: string;
|
|
498
|
+
weekday: string;
|
|
499
|
+
day: string;
|
|
500
|
+
daySelected: string;
|
|
501
|
+
timeLabel: string;
|
|
502
|
+
timeInput: string;
|
|
503
|
+
formatButton: string;
|
|
504
|
+
periodButton: string;
|
|
505
|
+
periodButtonActive: string;
|
|
506
|
+
};
|
|
507
|
+
button: {
|
|
508
|
+
primary: string;
|
|
509
|
+
secondary: string;
|
|
510
|
+
};
|
|
511
|
+
alert: {
|
|
512
|
+
base: string;
|
|
513
|
+
error: string;
|
|
514
|
+
success: string;
|
|
515
|
+
warning: string;
|
|
516
|
+
info: string;
|
|
517
|
+
};
|
|
518
|
+
error: string;
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Custom Labels
|
|
523
|
+
|
|
524
|
+
Customize form labels and messages:
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
import { FormThemeProvider } from 'formbase';
|
|
528
|
+
|
|
529
|
+
const customLabels = {
|
|
530
|
+
fileUpload: {
|
|
531
|
+
clickToUpload: 'Choose file',
|
|
532
|
+
dragAndDrop: 'or drag and drop',
|
|
533
|
+
allowedTypes: 'Supported formats:',
|
|
534
|
+
maxSize: 'Maximum size:',
|
|
535
|
+
},
|
|
536
|
+
datetime: {
|
|
537
|
+
weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
538
|
+
clear: 'Clear',
|
|
539
|
+
done: 'Done',
|
|
540
|
+
timeFormat: 'Time Format',
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
function App() {
|
|
545
|
+
return (
|
|
546
|
+
<FormThemeProvider labels={customLabels}>
|
|
547
|
+
{/* Your forms here */}
|
|
548
|
+
</FormThemeProvider>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## Advanced Usage
|
|
554
|
+
|
|
555
|
+
### Using Hooks for Custom Forms
|
|
556
|
+
|
|
557
|
+
#### Next.js
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
import { useNextForm } from 'performa/nextjs';
|
|
561
|
+
|
|
562
|
+
function CustomForm() {
|
|
563
|
+
const { formAction, errors, isPending, isSuccess } = useNextForm(
|
|
564
|
+
submitForm,
|
|
565
|
+
(data) => {
|
|
566
|
+
console.log('Success!', data);
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
return (
|
|
571
|
+
<form action={formAction}>
|
|
572
|
+
<input name="email" type="email" />
|
|
573
|
+
{errors?.email && <p>{errors.email}</p>}
|
|
574
|
+
<button disabled={isPending}>
|
|
575
|
+
{isPending ? 'Submitting...' : 'Submit'}
|
|
576
|
+
</button>
|
|
577
|
+
{isSuccess && <p>Form submitted successfully!</p>}
|
|
578
|
+
</form>
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
#### React Router
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
import { useReactRouterForm } from 'performa/react-router';
|
|
587
|
+
|
|
588
|
+
function CustomForm() {
|
|
589
|
+
const { fetcher, errors, isSubmitting } = useReactRouterForm('my-form');
|
|
590
|
+
|
|
591
|
+
return (
|
|
592
|
+
<fetcher.Form method="post">
|
|
593
|
+
<input name="email" type="email" />
|
|
594
|
+
{errors?.email && <p>{errors.email}</p>}
|
|
595
|
+
<button disabled={isSubmitting}>Submit</button>
|
|
596
|
+
</fetcher.Form>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
#### TanStack Start
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
import { useTanStackStartForm } from 'performa/tanstack-start';
|
|
605
|
+
|
|
606
|
+
function CustomForm() {
|
|
607
|
+
const { handleSubmit, errors, isPending } = useTanStackStartForm(
|
|
608
|
+
submitForm
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
<form onSubmit={handleSubmit}>
|
|
613
|
+
<input name="email" type="email" />
|
|
614
|
+
{errors?.email && <p>{errors.email}</p>}
|
|
615
|
+
<button disabled={isPending}>Submit</button>
|
|
616
|
+
</form>
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### Conditional Fields
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
const formConfig: FormConfig = {
|
|
625
|
+
key: 'signup',
|
|
626
|
+
fields: {
|
|
627
|
+
accountType: {
|
|
628
|
+
type: 'select',
|
|
629
|
+
label: 'Account Type',
|
|
630
|
+
options: [
|
|
631
|
+
{ value: 'personal', label: 'Personal' },
|
|
632
|
+
{ value: 'business', label: 'Business' },
|
|
633
|
+
],
|
|
634
|
+
rules: { required: true },
|
|
635
|
+
},
|
|
636
|
+
// Only show company name for business accounts
|
|
637
|
+
...(accountType === 'business' && {
|
|
638
|
+
companyName: {
|
|
639
|
+
type: 'text',
|
|
640
|
+
label: 'Company Name',
|
|
641
|
+
rules: { required: true },
|
|
642
|
+
},
|
|
643
|
+
}),
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Multi-Step Forms
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
const step1Config: FormConfig = {
|
|
652
|
+
key: 'registration-step1',
|
|
653
|
+
fields: {
|
|
654
|
+
email: { type: 'email', label: 'Email', rules: { required: true } },
|
|
655
|
+
password: { type: 'password', label: 'Password', rules: { required: true } },
|
|
656
|
+
submit: { type: 'submit', label: 'Continue' },
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const step2Config: FormConfig = {
|
|
661
|
+
key: 'registration-step2',
|
|
662
|
+
fields: {
|
|
663
|
+
firstName: { type: 'text', label: 'First Name', rules: { required: true } },
|
|
664
|
+
lastName: { type: 'text', label: 'Last Name', rules: { required: true } },
|
|
665
|
+
submit: { type: 'submit', label: 'Complete Registration' },
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
function MultiStepForm() {
|
|
670
|
+
const [step, setStep] = useState(1);
|
|
671
|
+
|
|
672
|
+
return (
|
|
673
|
+
<>
|
|
674
|
+
{step === 1 && (
|
|
675
|
+
<NextForm
|
|
676
|
+
config={step1Config}
|
|
677
|
+
action={submitStep1}
|
|
678
|
+
onSuccess={() => setStep(2)}
|
|
679
|
+
/>
|
|
680
|
+
)}
|
|
681
|
+
{step === 2 && (
|
|
682
|
+
<NextForm
|
|
683
|
+
config={step2Config}
|
|
684
|
+
action={submitStep2}
|
|
685
|
+
/>
|
|
686
|
+
)}
|
|
687
|
+
</>
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Dynamic Field Options
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
function DynamicForm() {
|
|
696
|
+
const [categories, setCategories] = useState([]);
|
|
697
|
+
|
|
698
|
+
useEffect(() => {
|
|
699
|
+
fetch('/api/categories')
|
|
700
|
+
.then(res => res.json())
|
|
701
|
+
.then(data => setCategories(data));
|
|
702
|
+
}, []);
|
|
703
|
+
|
|
704
|
+
const formConfig: FormConfig = {
|
|
705
|
+
key: 'product',
|
|
706
|
+
fields: {
|
|
707
|
+
category: {
|
|
708
|
+
type: 'select',
|
|
709
|
+
label: 'Category',
|
|
710
|
+
options: categories.map(cat => ({
|
|
711
|
+
value: cat.id,
|
|
712
|
+
label: cat.name,
|
|
713
|
+
})),
|
|
714
|
+
rules: { required: true },
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
return <NextForm config={formConfig} action={submitProduct} />;
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
## TypeScript
|
|
724
|
+
|
|
725
|
+
formbase is built with TypeScript and provides full type safety:
|
|
726
|
+
|
|
727
|
+
```typescript
|
|
728
|
+
import { FormConfig } from 'performa';
|
|
729
|
+
import { validateForm } from 'performa/server';
|
|
730
|
+
|
|
731
|
+
// Type-safe form configuration using 'satisfies'
|
|
732
|
+
// This gives you both type checking AND proper inference
|
|
733
|
+
const formConfig = {
|
|
734
|
+
key: 'contact',
|
|
735
|
+
fields: {
|
|
736
|
+
name: {
|
|
737
|
+
type: 'text',
|
|
738
|
+
label: 'Name',
|
|
739
|
+
rules: { required: true },
|
|
740
|
+
},
|
|
741
|
+
email: {
|
|
742
|
+
type: 'email',
|
|
743
|
+
label: 'Email',
|
|
744
|
+
rules: { required: true, isEmail: true },
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
} satisfies FormConfig;
|
|
748
|
+
|
|
749
|
+
// Type-safe validation result
|
|
750
|
+
// TypeScript automatically infers the correct types from formConfig
|
|
751
|
+
const result = await validateForm(request, formConfig);
|
|
752
|
+
|
|
753
|
+
if (result.hasErrors) {
|
|
754
|
+
// result.errors is properly typed with field names
|
|
755
|
+
console.log(result.errors.name); // string | undefined
|
|
756
|
+
console.log(result.errors.email); // string | undefined
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// result.values is properly typed based on field types
|
|
760
|
+
const name: string = result.values.name; // TypeScript knows this is a string
|
|
761
|
+
const email: string = result.values.email; // TypeScript knows this is a string
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
## Security Considerations
|
|
765
|
+
|
|
766
|
+
### Server-Side Validation
|
|
767
|
+
|
|
768
|
+
All validation is performed server-side. Client-side HTML5 validation attributes are added for better UX, but should not be relied upon for security.
|
|
769
|
+
|
|
770
|
+
### File Upload Security
|
|
771
|
+
|
|
772
|
+
1. **MIME Type Validation**: Files are validated by actual MIME type, not just extension
|
|
773
|
+
2. **Size Limits**: Enforce maximum file sizes to prevent DoS attacks
|
|
774
|
+
3. **File Storage**: Always validate and sanitize filenames before storage
|
|
775
|
+
4. **Virus Scanning**: Implement virus scanning for uploaded files in production
|
|
776
|
+
|
|
777
|
+
### XSS Prevention
|
|
778
|
+
|
|
779
|
+
- All form inputs are properly escaped when rendered
|
|
780
|
+
- The `denyHtml` validation rule prevents HTML injection
|
|
781
|
+
- Use the `pattern` rule to restrict input to safe characters
|
|
782
|
+
|
|
783
|
+
### CSRF Protection
|
|
784
|
+
|
|
785
|
+
Implement CSRF protection at the framework level:
|
|
786
|
+
|
|
787
|
+
```typescript
|
|
788
|
+
// Next.js example with csrf-token
|
|
789
|
+
import { headers } from 'next/headers';
|
|
790
|
+
|
|
791
|
+
export async function submitForm(prevState: any, formData: FormData) {
|
|
792
|
+
const headersList = headers();
|
|
793
|
+
const csrfToken = headersList.get('x-csrf-token');
|
|
794
|
+
|
|
795
|
+
// Validate CSRF token
|
|
796
|
+
|
|
797
|
+
const result = await validateForm(request, formConfig);
|
|
798
|
+
// ...
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
## Performance
|
|
803
|
+
|
|
804
|
+
### Bundle Sizes
|
|
805
|
+
|
|
806
|
+
- Core library: 7.69 KB (brotli)
|
|
807
|
+
- Server validation: 367 B (brotli)
|
|
808
|
+
- React Router adapter: 18.8 KB (brotli)
|
|
809
|
+
- Next.js adapter: 7.3 KB (brotli)
|
|
810
|
+
- TanStack Start adapter: 7.36 KB (brotli)
|
|
811
|
+
|
|
812
|
+
### Optimization
|
|
813
|
+
|
|
814
|
+
All components are memoized with `React.memo` for optimal performance. The library uses:
|
|
815
|
+
|
|
816
|
+
- Tree-shaking for unused code elimination
|
|
817
|
+
- Code splitting between client and server modules
|
|
818
|
+
- Minimal dependencies (only `lucide-react` for icons)
|
|
819
|
+
|
|
820
|
+
## Accessibility
|
|
821
|
+
|
|
822
|
+
formbase is built with accessibility in mind:
|
|
823
|
+
|
|
824
|
+
- Proper ARIA attributes on all form elements
|
|
825
|
+
- Keyboard navigation support
|
|
826
|
+
- Screen reader friendly error messages
|
|
827
|
+
- Focus management
|
|
828
|
+
- Semantic HTML structure
|
|
829
|
+
- High contrast mode support
|
|
830
|
+
|
|
831
|
+
## Browser Support
|
|
832
|
+
|
|
833
|
+
- Chrome/Edge (latest)
|
|
834
|
+
- Firefox (latest)
|
|
835
|
+
- Safari (latest)
|
|
836
|
+
- iOS Safari (latest)
|
|
837
|
+
- Chrome Android (latest)
|
|
838
|
+
|
|
839
|
+
## Contributing
|
|
840
|
+
|
|
841
|
+
Contributions are welcome! Please see our contributing guidelines.
|
|
842
|
+
|
|
843
|
+
## License
|
|
844
|
+
|
|
845
|
+
MIT License - see LICENSE file for details
|
|
846
|
+
|
|
847
|
+
## Support
|
|
848
|
+
|
|
849
|
+
- GitHub Issues: https://github.com/matttehat/performa/issues
|
|
850
|
+
- Documentation: https://github.com/matttehat/performa#readme
|