@page-speed/forms 0.7.9 → 0.8.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 +198 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
# ⌨️ `@page-speed/forms`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
4
|
|
|
5
|
-
Type-safe, high-performance React form state and input components for OpenSite/DashTrack workloads.
|
|
5
|
+
> Type-safe, high-performance React form state and input components for OpenSite/DashTrack workloads.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@page-speed/forms)
|
|
8
8
|
[](https://www.npmjs.com/package/@page-speed/forms)
|
|
@@ -10,8 +10,8 @@ Type-safe, high-performance React form state and input components for OpenSite/D
|
|
|
10
10
|
|
|
11
11
|
## Highlights
|
|
12
12
|
|
|
13
|
+
- **`FormEngine`** — declarative form component with built-in API integration
|
|
13
14
|
- Field-level reactivity via `@legendapp/state/react`
|
|
14
|
-
- Typed `useForm` and `useField` APIs
|
|
15
15
|
- Built-in input library (text, select, date, time, upload, rich text)
|
|
16
16
|
- Tree-shakable subpath exports (`/core`, `/inputs`, `/validation`, `/upload`, `/integration`)
|
|
17
17
|
- Validation rules and utilities (sync + async)
|
|
@@ -30,112 +30,151 @@ Peer dependencies:
|
|
|
30
30
|
- `react >= 16.8.0`
|
|
31
31
|
- `react-dom >= 16.8.0`
|
|
32
32
|
|
|
33
|
-
## Quick Start
|
|
33
|
+
## Quick Start with FormEngine
|
|
34
|
+
|
|
35
|
+
`FormEngine` is the recommended entry point for most use cases. It provides a declarative API for rendering forms with built-in API integration, validation, file uploads, and styling.
|
|
34
36
|
|
|
35
37
|
```tsx
|
|
36
38
|
import * as React from "react";
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
import {
|
|
40
|
+
FormEngine,
|
|
41
|
+
type FormFieldConfig,
|
|
42
|
+
} from "@page-speed/forms/integration";
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
44
|
+
const fields: FormFieldConfig[] = [
|
|
45
|
+
{
|
|
46
|
+
name: "fullName",
|
|
47
|
+
type: "text",
|
|
48
|
+
label: "Full Name",
|
|
49
|
+
required: true,
|
|
50
|
+
placeholder: "Your name",
|
|
51
|
+
columnSpan: 12,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "email",
|
|
55
|
+
type: "email",
|
|
56
|
+
label: "Email",
|
|
57
|
+
required: true,
|
|
58
|
+
placeholder: "you@example.com",
|
|
59
|
+
columnSpan: 6,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "phone",
|
|
63
|
+
type: "tel",
|
|
64
|
+
label: "Phone",
|
|
65
|
+
columnSpan: 6,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "message",
|
|
69
|
+
type: "textarea",
|
|
70
|
+
label: "Message",
|
|
71
|
+
required: true,
|
|
72
|
+
columnSpan: 12,
|
|
73
|
+
},
|
|
74
|
+
];
|
|
57
75
|
|
|
76
|
+
export function ContactForm() {
|
|
58
77
|
return (
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
<FormEngine
|
|
79
|
+
api={{
|
|
80
|
+
endpoint: "/api/contact",
|
|
81
|
+
method: "post",
|
|
82
|
+
submissionConfig: { behavior: "showConfirmation" },
|
|
83
|
+
}}
|
|
84
|
+
fields={fields}
|
|
85
|
+
successMessage="Thanks for reaching out!"
|
|
86
|
+
formLayoutSettings={{
|
|
87
|
+
submitButtonSetup: {
|
|
88
|
+
submitLabel: "Send Message",
|
|
89
|
+
},
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
69
95
|
|
|
70
|
-
|
|
71
|
-
{({ field, meta }) => (
|
|
72
|
-
<TextInput
|
|
73
|
-
{...field}
|
|
74
|
-
type="email"
|
|
75
|
-
placeholder="you@example.com"
|
|
76
|
-
error={Boolean(meta.touched && meta.error)}
|
|
77
|
-
/>
|
|
78
|
-
)}
|
|
79
|
-
</Field>
|
|
96
|
+
### FormEngine Props
|
|
80
97
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
/>
|
|
92
|
-
)}
|
|
93
|
-
</Field>
|
|
98
|
+
| Prop | Type | Description |
|
|
99
|
+
|------|------|-------------|
|
|
100
|
+
| `api` | `PageSpeedFormConfig` | API endpoint and submission configuration |
|
|
101
|
+
| `fields` | `FormFieldConfig[]` | Array of field definitions |
|
|
102
|
+
| `formLayoutSettings` | `FormEngineLayoutSettings` | Layout, style, and submit button settings |
|
|
103
|
+
| `successMessage` | `ReactNode` | Message shown after successful submission |
|
|
104
|
+
| `onSubmit` | `(values) => void \| Promise<void>` | Custom submit handler |
|
|
105
|
+
| `onSuccess` | `(data) => void` | Called after successful submission |
|
|
106
|
+
| `onError` | `(error) => void` | Called when submission fails |
|
|
107
|
+
| `resetOnSuccess` | `boolean` | Reset form after success (default: `true`) |
|
|
94
108
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
### Field Configuration
|
|
110
|
+
|
|
111
|
+
Each field in the `fields` array supports:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
interface FormFieldConfig {
|
|
115
|
+
name: string;
|
|
116
|
+
type: "text" | "email" | "tel" | "textarea" | "select" | "multiselect" |
|
|
117
|
+
"date" | "daterange" | "time" | "file" | "checkbox" | "radio";
|
|
118
|
+
label?: string;
|
|
119
|
+
placeholder?: string;
|
|
120
|
+
required?: boolean;
|
|
121
|
+
columnSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|
122
|
+
className?: string;
|
|
123
|
+
options?: { label: string; value: string }[]; // For select/multiselect/radio
|
|
124
|
+
// File-specific props
|
|
125
|
+
accept?: string;
|
|
126
|
+
maxFiles?: number;
|
|
127
|
+
maxFileSize?: number;
|
|
100
128
|
}
|
|
101
129
|
```
|
|
102
130
|
|
|
103
|
-
###
|
|
131
|
+
### Layout Options
|
|
104
132
|
|
|
105
|
-
|
|
133
|
+
#### Standard Layout (default)
|
|
106
134
|
|
|
107
|
-
|
|
108
|
-
import * as React from "react";
|
|
109
|
-
import { Form } from "@page-speed/forms";
|
|
135
|
+
Multi-column grid with a submit button below the fields:
|
|
110
136
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
```tsx
|
|
138
|
+
<FormEngine
|
|
139
|
+
fields={fields}
|
|
140
|
+
formLayoutSettings={{
|
|
141
|
+
formLayout: "standard",
|
|
142
|
+
submitButtonSetup: {
|
|
143
|
+
submitLabel: "Submit",
|
|
144
|
+
submitVariant: "default", // | "destructive" | "outline" | "secondary" | "ghost" | "link"
|
|
145
|
+
},
|
|
146
|
+
styleRules: {
|
|
147
|
+
formContainer: "max-w-2xl mx-auto",
|
|
148
|
+
fieldsContainer: "gap-6",
|
|
149
|
+
formClassName: "space-y-4",
|
|
150
|
+
},
|
|
121
151
|
}}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Button-Group Layout
|
|
156
|
+
|
|
157
|
+
Inline input with submit button (e.g., newsletter signup):
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
<FormEngine
|
|
161
|
+
fields={[{ name: "email", type: "email", label: "Email", required: true }]}
|
|
162
|
+
formLayoutSettings={{
|
|
163
|
+
formLayout: "button-group",
|
|
164
|
+
buttonGroupSetup: {
|
|
165
|
+
size: "lg", // | "xs" | "sm" | "default"
|
|
166
|
+
submitLabel: "Subscribe",
|
|
167
|
+
submitVariant: "default",
|
|
127
168
|
},
|
|
128
169
|
}}
|
|
129
|
-
|
|
170
|
+
/>
|
|
130
171
|
```
|
|
131
172
|
|
|
132
|
-
###
|
|
173
|
+
### Using formEngineSetup Wrapper
|
|
133
174
|
|
|
134
|
-
|
|
135
|
-
for block/component libraries that want to provide local default fields and styles.
|
|
175
|
+
For block/component libraries that provide default configurations:
|
|
136
176
|
|
|
137
177
|
```tsx
|
|
138
|
-
import * as React from "react";
|
|
139
178
|
import {
|
|
140
179
|
FormEngine,
|
|
141
180
|
type FormEngineSetup,
|
|
@@ -151,16 +190,16 @@ const defaultStyleRules: FormEngineStyleRules = {
|
|
|
151
190
|
formClassName: "space-y-6",
|
|
152
191
|
};
|
|
153
192
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
193
|
+
// Consumer passes setup, component provides defaults
|
|
194
|
+
function ContactBlock({ formEngineSetup }: { formEngineSetup?: FormEngineSetup }) {
|
|
195
|
+
return (
|
|
196
|
+
<FormEngine
|
|
197
|
+
formEngineSetup={formEngineSetup}
|
|
198
|
+
defaultFields={defaultFields}
|
|
199
|
+
defaultStyleRules={defaultStyleRules}
|
|
200
|
+
/>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
164
203
|
```
|
|
165
204
|
|
|
166
205
|
## Package Entry Points
|
|
@@ -170,22 +209,22 @@ const setup: FormEngineSetup = {
|
|
|
170
209
|
|
|
171
210
|
Exports:
|
|
172
211
|
- `useForm`, `useField`, `Form`, `Field`, `FormContext`
|
|
173
|
-
-
|
|
212
|
+
- Core form/types interfaces
|
|
213
|
+
|
|
214
|
+
### Integration (Recommended)
|
|
215
|
+
- `@page-speed/forms/integration`
|
|
216
|
+
|
|
217
|
+
Exports:
|
|
218
|
+
- `FormEngine`, `FormEngineSetup`, `FormEngineProps`
|
|
219
|
+
- `FormFieldConfig`, `FormEngineStyleRules`, `FormEngineLayoutSettings`
|
|
220
|
+
- `DynamicFormField`, `useContactForm`, `useFileUpload`
|
|
174
221
|
|
|
175
222
|
### Inputs
|
|
176
223
|
- `@page-speed/forms/inputs`
|
|
177
224
|
|
|
178
225
|
Exports:
|
|
179
|
-
- `TextInput`
|
|
180
|
-
- `
|
|
181
|
-
- `Checkbox`
|
|
182
|
-
- `CheckboxGroup`
|
|
183
|
-
- `Radio`
|
|
184
|
-
- `Select`
|
|
185
|
-
- `MultiSelect`
|
|
186
|
-
- `DatePicker`
|
|
187
|
-
- `DateRangePicker`
|
|
188
|
-
- `TimePicker`
|
|
226
|
+
- `TextInput`, `TextArea`, `Checkbox`, `CheckboxGroup`, `Radio`
|
|
227
|
+
- `Select`, `MultiSelect`, `DatePicker`, `DateRangePicker`, `TimePicker`
|
|
189
228
|
- `FileInput`
|
|
190
229
|
|
|
191
230
|
### Validation
|
|
@@ -194,14 +233,13 @@ Exports:
|
|
|
194
233
|
- `@page-speed/forms/validation/utils`
|
|
195
234
|
- `@page-speed/forms/validation/valibot`
|
|
196
235
|
|
|
197
|
-
### Upload
|
|
236
|
+
### Upload
|
|
198
237
|
- `@page-speed/forms/upload`
|
|
199
|
-
- `@page-speed/forms/integration`
|
|
200
238
|
|
|
201
239
|
## Input Notes
|
|
202
240
|
|
|
203
241
|
### `TimePicker`
|
|
204
|
-
`TimePicker`
|
|
242
|
+
`TimePicker` uses a native `input[type="time"]` UX internally.
|
|
205
243
|
|
|
206
244
|
- Accepts controlled values in `HH:mm` (24-hour) or `h:mm AM/PM` (12-hour)
|
|
207
245
|
- Emits `HH:mm` when `use24Hour` is `true`
|
|
@@ -222,8 +260,6 @@ Exports:
|
|
|
222
260
|
|
|
223
261
|
This library ships with Tailwind utility classes and semantic token class names.
|
|
224
262
|
|
|
225
|
-
It is **not** a BEM-only unstyled package anymore.
|
|
226
|
-
|
|
227
263
|
### Base conventions
|
|
228
264
|
|
|
229
265
|
- Inputs/triggers are transparent shells with semantic borders/rings
|
|
@@ -231,6 +267,19 @@ It is **not** a BEM-only unstyled package anymore.
|
|
|
231
267
|
- Error states use destructive border/ring
|
|
232
268
|
- Dropdown selected rows use muted backgrounds
|
|
233
269
|
|
|
270
|
+
### FormEngine Style Rules
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
interface FormEngineStyleRules {
|
|
274
|
+
formContainer?: string; // Wrapper around <form>
|
|
275
|
+
fieldsContainer?: string; // Grid wrapper for fields
|
|
276
|
+
fieldClassName?: string; // Fallback className for fields
|
|
277
|
+
formClassName?: string; // Applied to <form> element
|
|
278
|
+
successMessageClassName?: string;
|
|
279
|
+
errorMessageClassName?: string;
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
234
283
|
### Autofill normalization
|
|
235
284
|
|
|
236
285
|
Text-like controls apply autofill reset classes to avoid browser-injected background/text colors breaking your theme contrast.
|
|
@@ -249,6 +298,41 @@ Ensure your app defines semantic tokens used in classes such as:
|
|
|
249
298
|
|
|
250
299
|
For complete styling guidance, see [`docs/STYLES.md`](./docs/STYLES.md).
|
|
251
300
|
|
|
301
|
+
## Advanced: Low-Level APIs
|
|
302
|
+
|
|
303
|
+
For custom form implementations, the lower-level `useForm`, `Form`, and `Field` APIs are available:
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import { Form, Field, useForm } from "@page-speed/forms";
|
|
307
|
+
import { TextInput } from "@page-speed/forms/inputs";
|
|
308
|
+
|
|
309
|
+
function CustomForm() {
|
|
310
|
+
const form = useForm({
|
|
311
|
+
initialValues: { email: "" },
|
|
312
|
+
validationSchema: {
|
|
313
|
+
email: (value) => (!value ? "Required" : undefined),
|
|
314
|
+
},
|
|
315
|
+
onSubmit: async (values) => {
|
|
316
|
+
console.log(values);
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<Form form={form}>
|
|
322
|
+
<Field name="email" label="Email">
|
|
323
|
+
{({ field, meta }) => (
|
|
324
|
+
<TextInput
|
|
325
|
+
{...field}
|
|
326
|
+
error={Boolean(meta.touched && meta.error)}
|
|
327
|
+
/>
|
|
328
|
+
)}
|
|
329
|
+
</Field>
|
|
330
|
+
<button type="submit">Submit</button>
|
|
331
|
+
</Form>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
252
336
|
## Validation Utilities
|
|
253
337
|
|
|
254
338
|
Use built-in rules:
|
|
@@ -264,7 +348,7 @@ Use utilities from `/validation/utils`:
|
|
|
264
348
|
|
|
265
349
|
## File Uploads
|
|
266
350
|
|
|
267
|
-
`FileInput`
|
|
351
|
+
`FileInput` and `FormEngine` support validation, drag/drop, preview, and crop workflows.
|
|
268
352
|
|
|
269
353
|
For full two-phase upload patterns and serializer usage, see:
|
|
270
354
|
- [`docs/FILE_UPLOADS.md`](./docs/FILE_UPLOADS.md)
|