react-form-rewind 0.5.0 → 1.1.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 +222 -83
- package/dist/index.d.mts +105 -1
- package/dist/index.d.ts +105 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,17 +10,11 @@
|
|
|
10
10
|
[](https://bundlejs.com/?q=react-form-rewind)
|
|
11
11
|
[](https://github.com/BazilSuhail/npm-react-form-rewind)
|
|
12
12
|
|
|
13
|
-
Zero-dependency, tree-shakable React state engine with
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- **Functional updates** — `setState(prev => prev + 1)` supported
|
|
19
|
-
- **History inspection** — access `past` and `future` arrays for custom UIs
|
|
20
|
-
- **Callbacks** — `onUndo`, `onRedo`, `onSnapshot` hooks
|
|
21
|
-
- Zero-config — no providers, no context, just a hook
|
|
22
|
-
- Tree-shakable — ESM + CJS with `sideEffects: false`
|
|
23
|
-
- TypeScript — full generics, all types exported
|
|
13
|
+
Zero-dependency, tree-shakable React state engine with **field-level undo/redo**, per-field history stacks, validation, draft persistence, and keyboard shortcuts.
|
|
14
|
+
|
|
15
|
+
Two APIs: a standalone hook for full control, or field components for zero-boilerplate forms.
|
|
16
|
+
|
|
17
|
+
---
|
|
24
18
|
|
|
25
19
|
## Install
|
|
26
20
|
|
|
@@ -28,131 +22,287 @@ Zero-dependency, tree-shakable React state engine with auto-saved history stacks
|
|
|
28
22
|
npm install react-form-rewind
|
|
29
23
|
```
|
|
30
24
|
|
|
31
|
-
|
|
25
|
+
---
|
|
32
26
|
|
|
33
|
-
|
|
27
|
+
## Two Ways to Use
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|---------|----------|
|
|
37
|
-
| No native Ctrl+Z / Ctrl+Y in React forms | Built-in keyboard shortcuts with history tracking |
|
|
38
|
-
| User progress lost on tab reload | Auto-save drafts to `localStorage` with schema versioning |
|
|
39
|
-
| Manual debouncers for history snapshots | Automated keystroke coalescing into logical snapshots |
|
|
40
|
-
| Heavy form libraries add validation bloat | Focused solely on history and state persistence |
|
|
29
|
+
### 1. Field Components (recommended for forms)
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
Zero boilerplate. Field components handle registration, onChange, validation, and per-field undo/redo automatically.
|
|
43
32
|
|
|
44
|
-
```
|
|
45
|
-
|
|
33
|
+
```tsx
|
|
34
|
+
import { FormRewind, TextField, NumberField } from "react-form-rewind";
|
|
35
|
+
|
|
36
|
+
function SignupForm() {
|
|
37
|
+
return (
|
|
38
|
+
<FormRewind
|
|
39
|
+
initialState={{ name: "", email: "", age: 0 }}
|
|
40
|
+
keyboard
|
|
41
|
+
persist={{ key: "signup-draft" }}
|
|
42
|
+
onSubmit={(data) => console.log(data)}
|
|
43
|
+
>
|
|
44
|
+
<TextField name="name" label="Name" rules={{ required: true }} />
|
|
45
|
+
<TextField
|
|
46
|
+
name="email"
|
|
47
|
+
label="Email"
|
|
48
|
+
rules={{
|
|
49
|
+
required: true,
|
|
50
|
+
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Invalid email" },
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
<NumberField name="age" label="Age" rules={{ min: 18, max: 120 }} />
|
|
54
|
+
<button type="submit">Submit</button>
|
|
55
|
+
</FormRewind>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
46
58
|
```
|
|
47
59
|
|
|
60
|
+
**What happens:**
|
|
61
|
+
- Click into Name, type "John", press **Ctrl+Z** — only Name undoes, Email stays
|
|
62
|
+
- Type in both fields, submit — validation runs, errors show per field
|
|
63
|
+
- Close tab, reopen — draft restored from localStorage
|
|
64
|
+
|
|
65
|
+
### 2. Standalone Hook (full control)
|
|
66
|
+
|
|
67
|
+
Use `useFormHistory` for any state — not just forms. Form-level undo/redo on the entire state object.
|
|
68
|
+
|
|
48
69
|
```tsx
|
|
49
70
|
import { useFormHistory } from "react-form-rewind";
|
|
50
71
|
|
|
51
|
-
function
|
|
72
|
+
function Counter() {
|
|
52
73
|
const { state, setState, undo, redo, canUndo, canRedo } = useFormHistory(
|
|
53
|
-
{
|
|
54
|
-
{ keyboard: true
|
|
74
|
+
{ count: 0 },
|
|
75
|
+
{ keyboard: true }
|
|
55
76
|
);
|
|
56
77
|
|
|
57
78
|
return (
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<input
|
|
64
|
-
value={state.email}
|
|
65
|
-
onChange={(e) => setState({ ...state, email: e.target.value })}
|
|
66
|
-
/>
|
|
67
|
-
<button type="button" onClick={undo} disabled={!canUndo}>
|
|
68
|
-
Undo
|
|
69
|
-
</button>
|
|
70
|
-
<button type="button" onClick={redo} disabled={!canRedo}>
|
|
71
|
-
Redo
|
|
72
|
-
</button>
|
|
73
|
-
</form>
|
|
79
|
+
<div>
|
|
80
|
+
<button onClick={() => setState({ count: state.count - 1 })} disabled={!canUndo}>-</button>
|
|
81
|
+
<span>{state.count}</span>
|
|
82
|
+
<button onClick={() => setState({ count: state.count + 1 })} disabled={!canRedo}>+</button>
|
|
83
|
+
</div>
|
|
74
84
|
);
|
|
75
85
|
}
|
|
76
86
|
```
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
**Ctrl+Z** reverts the entire state. **Ctrl+Shift+Z** redoes.
|
|
79
89
|
|
|
80
90
|
---
|
|
81
91
|
|
|
82
|
-
##
|
|
92
|
+
## Field-Level Undo/Redo
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
The key feature. When using `<FormRewind>` with `keyboard`, **Ctrl+Z undoes only the field your cursor is in**. Other fields stay untouched.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
Name: [John|] <-- cursor here, Ctrl+Z reverts just Name
|
|
98
|
+
Email: [john@test.com] <-- stays exactly as-is
|
|
99
|
+
Age: [25] <-- untouched
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Each field maintains its own independent history stack:
|
|
103
|
+
- **Per-field debounce** — typing "hello" fast = one undo step, not five
|
|
104
|
+
- **Per-field redo** — Ctrl+Shift+Z redoes only the focused field
|
|
105
|
+
- **Independent stacks** — undoing Name doesn't affect Email's history
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Field Components
|
|
110
|
+
|
|
111
|
+
All field components auto-register with the `<FormRewind>` context, track their own history, validate on blur, and display errors.
|
|
112
|
+
|
|
113
|
+
### TextField
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<TextField name="name" label="Name" placeholder="John" rules={{ required: true }} />
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Props: `name`, `label?`, `rules?`, `placeholder?`, `className?`, `style?`, plus all native `<input>` props.
|
|
120
|
+
|
|
121
|
+
### NumberField
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<NumberField name="age" label="Age" rules={{ min: 0, max: 150 }} />
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Same as TextField but type="number". Value is stored as a number.
|
|
128
|
+
|
|
129
|
+
### CheckboxField
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<CheckboxField name="agree" label="I agree to terms" rules={{ required: true }} />
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Boolean field. `required` means the checkbox must be checked.
|
|
136
|
+
|
|
137
|
+
### SelectField
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<SelectField
|
|
141
|
+
name="country"
|
|
142
|
+
label="Country"
|
|
143
|
+
placeholder="Select..."
|
|
144
|
+
options={[
|
|
145
|
+
{ value: "us", label: "United States" },
|
|
146
|
+
{ value: "uk", label: "United Kingdom" },
|
|
147
|
+
]}
|
|
148
|
+
rules={{ required: true }}
|
|
149
|
+
/>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### TextareaField
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
<TextareaField name="bio" label="Bio" rows={4} rules={{ maxLength: 500 }} />
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Validation
|
|
161
|
+
|
|
162
|
+
Pass a `rules` prop to any field component. Validation runs on blur (when the field is touched) and on form submit.
|
|
163
|
+
|
|
164
|
+
### Built-in Rules
|
|
165
|
+
|
|
166
|
+
| Rule | Type | Description |
|
|
167
|
+
|------|------|-------------|
|
|
168
|
+
| `required` | `boolean \| string` | Field must be non-empty. Pass a string for custom error message. |
|
|
169
|
+
| `pattern` | `RegExp \| { value: RegExp, message: string }` | Must match regex |
|
|
170
|
+
| `minLength` | `number \| { value: number, message: string }` | String min length |
|
|
171
|
+
| `maxLength` | `number \| { value: number, message: string }` | String max length |
|
|
172
|
+
| `min` | `number \| { value: number, message: string }` | Number minimum |
|
|
173
|
+
| `max` | `number \| { value: number, message: string }` | Number maximum |
|
|
174
|
+
| `validate` | `(value) => string \| null` | Custom validator. Return error message or null. |
|
|
175
|
+
|
|
176
|
+
### Examples
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// Required with custom message
|
|
180
|
+
<TextField name="name" rules={{ required: "Name is required" }} />
|
|
181
|
+
|
|
182
|
+
// Email pattern with custom message
|
|
183
|
+
<TextField name="email" rules={{ pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Bad email" } }} />
|
|
184
|
+
|
|
185
|
+
// Number range
|
|
186
|
+
<NumberField name="score" rules={{ min: 0, max: 100 }} />
|
|
187
|
+
|
|
188
|
+
// Custom validator
|
|
189
|
+
<TextField
|
|
190
|
+
name="username"
|
|
191
|
+
rules={{
|
|
192
|
+
validate: (val) => (val as string).length < 3 ? "Too short" : null,
|
|
193
|
+
}}
|
|
194
|
+
/>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## FormRewind Provider
|
|
200
|
+
|
|
201
|
+
The `<FormRewind>` component wraps your form and provides context to all field components.
|
|
202
|
+
|
|
203
|
+
| Prop | Type | Default | Description |
|
|
204
|
+
|------|------|---------|-------------|
|
|
205
|
+
| `initialState` | `Record<string, unknown>` | (required) | Initial form values |
|
|
206
|
+
| `keyboard` | `boolean` | `false` | Enable field-level Ctrl+Z / Ctrl+Shift+Z |
|
|
207
|
+
| `debounceMs` | `number` | `300` | Per-field debounce window |
|
|
208
|
+
| `maxHistory` | `number` | `100` | Max history entries per field |
|
|
209
|
+
| `persist` | `{ key, debounceMs?, version? }` or `false` | `false` | localStorage draft persistence |
|
|
210
|
+
| `onSubmit` | `(state) => void` | — | Called after validation passes |
|
|
211
|
+
| `children` | `ReactNode` | (required) | Form fields |
|
|
212
|
+
|
|
213
|
+
Renders a `<form>` element with `noValidate`. Handles submit, runs validation, calls `onSubmit` only if all fields pass.
|
|
214
|
+
|
|
215
|
+
---
|
|
85
216
|
|
|
86
|
-
|
|
217
|
+
## Standalone Hook API
|
|
87
218
|
|
|
88
|
-
|
|
219
|
+
### `useFormHistory<T>(initialState, options?)`
|
|
89
220
|
|
|
90
221
|
| Property | Type | Description |
|
|
91
222
|
|----------|------|-------------|
|
|
92
|
-
| `state` | `T` | Current
|
|
93
|
-
| `setState` | `(value
|
|
223
|
+
| `state` | `T` | Current state |
|
|
224
|
+
| `setState` | `(value \| updater, label?) => void` | Update state |
|
|
94
225
|
| `undo` | `() => void` | Revert to previous state |
|
|
95
226
|
| `redo` | `() => void` | Re-apply undone state |
|
|
96
227
|
| `canUndo` | `boolean` | Whether undo is available |
|
|
97
228
|
| `canRedo` | `boolean` | Whether redo is available |
|
|
98
229
|
| `clearHistory` | `() => void` | Reset history, keep current state |
|
|
99
230
|
| `clearDraft` | `() => void` | Clear persisted draft from storage |
|
|
100
|
-
| `snapshot` | `(label
|
|
231
|
+
| `snapshot` | `(label?) => void` | Force-commit current state to history |
|
|
101
232
|
| `past` | `HistoryEntry<T>[]` | Past history entries |
|
|
102
233
|
| `future` | `HistoryEntry<T>[]` | Future (undone) entries |
|
|
103
234
|
|
|
104
|
-
|
|
235
|
+
Options:
|
|
105
236
|
|
|
106
237
|
| Option | Type | Default | Description |
|
|
107
238
|
|--------|------|---------|-------------|
|
|
108
|
-
| `maxHistory` | `number` | `100` |
|
|
109
|
-
| `debounceMs` | `number` | `300` | Debounce window
|
|
110
|
-
| `persist` | `boolean \| PersistOptions` | `false` |
|
|
111
|
-
| `keyboard` | `boolean` | `false` |
|
|
239
|
+
| `maxHistory` | `number` | `100` | Max past entries |
|
|
240
|
+
| `debounceMs` | `number` | `300` | Debounce window (0 = no debounce) |
|
|
241
|
+
| `persist` | `boolean \| PersistOptions` | `false` | Draft persistence |
|
|
242
|
+
| `keyboard` | `boolean` | `false` | Ctrl+Z / Ctrl+Shift+Z (form-level) |
|
|
112
243
|
| `onUndo` | `(state: T) => void` | — | Callback after undo |
|
|
113
244
|
| `onRedo` | `(state: T) => void` | — | Callback after redo |
|
|
114
|
-
| `onSnapshot` | `(entry
|
|
245
|
+
| `onSnapshot` | `(entry) => void` | — | Callback on snapshot |
|
|
115
246
|
|
|
116
|
-
|
|
247
|
+
---
|
|
117
248
|
|
|
118
|
-
|
|
119
|
-
|----------|------|---------|-------------|
|
|
120
|
-
| `key` | `string` | — | `localStorage` key for draft storage |
|
|
121
|
-
| `debounceMs` | `number` | `500` | Debounce for auto-save writes |
|
|
122
|
-
| `version` | `number` | `1` | Schema version (mismatches discard draft) |
|
|
249
|
+
## useFormRewindContext
|
|
123
250
|
|
|
124
|
-
|
|
251
|
+
Access form context from outside field components:
|
|
125
252
|
|
|
126
|
-
|
|
253
|
+
```tsx
|
|
254
|
+
import { useFormRewindContext } from "react-form-rewind";
|
|
127
255
|
|
|
128
|
-
|
|
256
|
+
function UndoButton() {
|
|
257
|
+
const { undoField, fields } = useFormRewindContext();
|
|
258
|
+
// undoField("name") — undo just the name field
|
|
259
|
+
// fields.name.canUndo — check if name has undo history
|
|
260
|
+
}
|
|
261
|
+
```
|
|
129
262
|
|
|
130
|
-
|
|
263
|
+
| Property | Type | Description |
|
|
264
|
+
|----------|------|-------------|
|
|
265
|
+
| `state` | `Record<string, unknown>` | Full form state |
|
|
266
|
+
| `setState` | `(name, value) => void` | Set a single field |
|
|
267
|
+
| `errors` | `Record<string, FieldError>` | Current validation errors |
|
|
268
|
+
| `touched` | `Record<string, boolean>` | Which fields have been blurred |
|
|
269
|
+
| `fields` | `Record<string, FieldMeta>` | Per-field metadata (touched, canUndo, canRedo) |
|
|
270
|
+
| `undoField` | `(name) => void` | Undo a specific field |
|
|
271
|
+
| `redoField` | `(name) => void` | Redo a specific field |
|
|
272
|
+
| `setError` | `(name, error) => void` | Manually set a field error |
|
|
273
|
+
| `clearError` | `(name) => void` | Clear a field error |
|
|
131
274
|
|
|
132
|
-
|
|
275
|
+
---
|
|
133
276
|
|
|
134
|
-
|
|
277
|
+
## Draft Persistence
|
|
135
278
|
|
|
136
|
-
|
|
279
|
+
Enable with `persist: { key: "my-form" }`. Drafts auto-save to `localStorage` (debounced) and restore on mount.
|
|
137
280
|
|
|
138
|
-
|
|
281
|
+
| Property | Type | Default | Description |
|
|
282
|
+
|----------|------|---------|-------------|
|
|
283
|
+
| `key` | `string` | (required) | localStorage key |
|
|
284
|
+
| `debounceMs` | `number` | `500` | Auto-save debounce |
|
|
285
|
+
| `version` | `number` | `1` | Schema version (mismatches discard draft) |
|
|
139
286
|
|
|
140
287
|
---
|
|
141
288
|
|
|
142
289
|
## Tree-Shaking
|
|
143
290
|
|
|
144
|
-
|
|
291
|
+
Pure ES module exports with `sideEffects: false`. Only import what you use:
|
|
145
292
|
|
|
146
293
|
```ts
|
|
147
|
-
//
|
|
294
|
+
// Just the hook — no field components bundled
|
|
148
295
|
import { useFormHistory } from "react-form-rewind";
|
|
296
|
+
|
|
297
|
+
// Just field components — no standalone hook logic
|
|
298
|
+
import { FormRewind, TextField } from "react-form-rewind";
|
|
149
299
|
```
|
|
150
300
|
|
|
151
301
|
---
|
|
152
302
|
|
|
153
303
|
## TypeScript
|
|
154
304
|
|
|
155
|
-
Full
|
|
305
|
+
Full generics, all types exported. State is inferred from `initialState`:
|
|
156
306
|
|
|
157
307
|
```ts
|
|
158
308
|
const { state } = useFormHistory({ count: 0 });
|
|
@@ -161,17 +311,6 @@ const { state } = useFormHistory({ count: 0 });
|
|
|
161
311
|
|
|
162
312
|
---
|
|
163
313
|
|
|
164
|
-
## Browser Support
|
|
165
|
-
|
|
166
|
-
- Chrome 80+
|
|
167
|
-
- Firefox 78+
|
|
168
|
-
- Safari 14+
|
|
169
|
-
- Edge 80+
|
|
170
|
-
|
|
171
|
-
Requires `React 18+` and native `Array`, `localStorage`, and `addEventListener` APIs.
|
|
172
|
-
|
|
173
|
-
---
|
|
174
|
-
|
|
175
314
|
## License
|
|
176
315
|
|
|
177
316
|
[MIT](LICENSE)
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface FieldRules {
|
|
5
|
+
required?: boolean | string;
|
|
6
|
+
pattern?: RegExp | {
|
|
7
|
+
value: RegExp;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
minLength?: number | {
|
|
11
|
+
value: number;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
maxLength?: number | {
|
|
15
|
+
value: number;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
min?: number | {
|
|
19
|
+
value: number;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
max?: number | {
|
|
23
|
+
value: number;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
validate?: (value: unknown) => string | null;
|
|
27
|
+
}
|
|
28
|
+
interface FieldError {
|
|
29
|
+
message: string;
|
|
30
|
+
type: string;
|
|
31
|
+
}
|
|
32
|
+
declare function validateField(value: unknown, rules: FieldRules): FieldError | null;
|
|
33
|
+
declare function validateAll(state: Record<string, unknown>, fieldRules: Record<string, FieldRules>): Record<string, FieldError>;
|
|
34
|
+
|
|
1
35
|
interface HistoryEntry<T> {
|
|
2
36
|
state: T;
|
|
3
37
|
timestamp: number;
|
|
@@ -30,7 +64,77 @@ interface UseFormHistoryReturn<T> {
|
|
|
30
64
|
past: HistoryEntry<T>[];
|
|
31
65
|
future: HistoryEntry<T>[];
|
|
32
66
|
}
|
|
67
|
+
interface FieldMeta {
|
|
68
|
+
name: string;
|
|
69
|
+
rules?: FieldRules;
|
|
70
|
+
touched: boolean;
|
|
71
|
+
canUndo: boolean;
|
|
72
|
+
canRedo: boolean;
|
|
73
|
+
}
|
|
74
|
+
interface FieldHistory {
|
|
75
|
+
past: unknown[];
|
|
76
|
+
future: unknown[];
|
|
77
|
+
}
|
|
78
|
+
interface FormRewindContextValue {
|
|
79
|
+
state: Record<string, unknown>;
|
|
80
|
+
setState: (name: string, value: unknown) => void;
|
|
81
|
+
errors: Record<string, FieldError>;
|
|
82
|
+
setError: (name: string, error: FieldError) => void;
|
|
83
|
+
clearError: (name: string) => void;
|
|
84
|
+
touched: Record<string, boolean>;
|
|
85
|
+
touch: (name: string) => void;
|
|
86
|
+
fields: Record<string, FieldMeta>;
|
|
87
|
+
registerField: (name: string, rules?: FieldRules) => void;
|
|
88
|
+
unregisterField: (name: string) => void;
|
|
89
|
+
undoField: (name: string) => void;
|
|
90
|
+
redoField: (name: string) => void;
|
|
91
|
+
}
|
|
33
92
|
|
|
34
93
|
declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
|
|
35
94
|
|
|
36
|
-
|
|
95
|
+
declare function useFormRewindContext(): FormRewindContextValue;
|
|
96
|
+
interface FormRewindProps {
|
|
97
|
+
initialState: Record<string, unknown>;
|
|
98
|
+
children: ReactNode;
|
|
99
|
+
onSubmit?: (state: Record<string, unknown>) => void;
|
|
100
|
+
maxHistory?: number;
|
|
101
|
+
debounceMs?: number;
|
|
102
|
+
persist?: {
|
|
103
|
+
key: string;
|
|
104
|
+
debounceMs?: number;
|
|
105
|
+
version?: number;
|
|
106
|
+
} | false;
|
|
107
|
+
keyboard?: boolean;
|
|
108
|
+
}
|
|
109
|
+
declare function FormRewind({ initialState, children, onSubmit, maxHistory, debounceMs, persist, keyboard, }: FormRewindProps): react.JSX.Element;
|
|
110
|
+
|
|
111
|
+
interface BaseFieldProps {
|
|
112
|
+
name: string;
|
|
113
|
+
label?: string;
|
|
114
|
+
rules?: FieldRules;
|
|
115
|
+
className?: string;
|
|
116
|
+
style?: React.CSSProperties;
|
|
117
|
+
}
|
|
118
|
+
interface TextFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange"> {
|
|
119
|
+
}
|
|
120
|
+
declare function TextField({ name, label, rules, className, style, ...inputProps }: TextFieldProps): react.JSX.Element;
|
|
121
|
+
interface NumberFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange" | "type"> {
|
|
122
|
+
}
|
|
123
|
+
declare function NumberField({ name, label, rules, className, style, ...inputProps }: NumberFieldProps): react.JSX.Element;
|
|
124
|
+
interface CheckboxFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "checked" | "onChange"> {
|
|
125
|
+
}
|
|
126
|
+
declare function CheckboxField({ name, label, rules, className, style, ...inputProps }: CheckboxFieldProps): react.JSX.Element;
|
|
127
|
+
interface SelectOption {
|
|
128
|
+
value: string;
|
|
129
|
+
label: string;
|
|
130
|
+
}
|
|
131
|
+
interface SelectFieldProps extends BaseFieldProps {
|
|
132
|
+
options: SelectOption[];
|
|
133
|
+
placeholder?: string;
|
|
134
|
+
}
|
|
135
|
+
declare function SelectField({ name, label, rules, options, placeholder, className, style }: SelectFieldProps): react.JSX.Element;
|
|
136
|
+
interface TextareaFieldProps extends BaseFieldProps, Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "name" | "value" | "onChange"> {
|
|
137
|
+
}
|
|
138
|
+
declare function TextareaField({ name, label, rules, className, style, ...textareaProps }: TextareaFieldProps): react.JSX.Element;
|
|
139
|
+
|
|
140
|
+
export { CheckboxField, type FieldError, type FieldHistory, type FieldMeta, type FieldRules, FormRewind, type FormRewindContextValue, type FormRewindProps, type HistoryEntry, NumberField, type PersistOptions, SelectField, TextField, TextareaField, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory, useFormRewindContext, validateAll, validateField };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface FieldRules {
|
|
5
|
+
required?: boolean | string;
|
|
6
|
+
pattern?: RegExp | {
|
|
7
|
+
value: RegExp;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
minLength?: number | {
|
|
11
|
+
value: number;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
maxLength?: number | {
|
|
15
|
+
value: number;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
min?: number | {
|
|
19
|
+
value: number;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
max?: number | {
|
|
23
|
+
value: number;
|
|
24
|
+
message: string;
|
|
25
|
+
};
|
|
26
|
+
validate?: (value: unknown) => string | null;
|
|
27
|
+
}
|
|
28
|
+
interface FieldError {
|
|
29
|
+
message: string;
|
|
30
|
+
type: string;
|
|
31
|
+
}
|
|
32
|
+
declare function validateField(value: unknown, rules: FieldRules): FieldError | null;
|
|
33
|
+
declare function validateAll(state: Record<string, unknown>, fieldRules: Record<string, FieldRules>): Record<string, FieldError>;
|
|
34
|
+
|
|
1
35
|
interface HistoryEntry<T> {
|
|
2
36
|
state: T;
|
|
3
37
|
timestamp: number;
|
|
@@ -30,7 +64,77 @@ interface UseFormHistoryReturn<T> {
|
|
|
30
64
|
past: HistoryEntry<T>[];
|
|
31
65
|
future: HistoryEntry<T>[];
|
|
32
66
|
}
|
|
67
|
+
interface FieldMeta {
|
|
68
|
+
name: string;
|
|
69
|
+
rules?: FieldRules;
|
|
70
|
+
touched: boolean;
|
|
71
|
+
canUndo: boolean;
|
|
72
|
+
canRedo: boolean;
|
|
73
|
+
}
|
|
74
|
+
interface FieldHistory {
|
|
75
|
+
past: unknown[];
|
|
76
|
+
future: unknown[];
|
|
77
|
+
}
|
|
78
|
+
interface FormRewindContextValue {
|
|
79
|
+
state: Record<string, unknown>;
|
|
80
|
+
setState: (name: string, value: unknown) => void;
|
|
81
|
+
errors: Record<string, FieldError>;
|
|
82
|
+
setError: (name: string, error: FieldError) => void;
|
|
83
|
+
clearError: (name: string) => void;
|
|
84
|
+
touched: Record<string, boolean>;
|
|
85
|
+
touch: (name: string) => void;
|
|
86
|
+
fields: Record<string, FieldMeta>;
|
|
87
|
+
registerField: (name: string, rules?: FieldRules) => void;
|
|
88
|
+
unregisterField: (name: string) => void;
|
|
89
|
+
undoField: (name: string) => void;
|
|
90
|
+
redoField: (name: string) => void;
|
|
91
|
+
}
|
|
33
92
|
|
|
34
93
|
declare function useFormHistory<T>(initialState: T, options?: UseFormHistoryOptions<T>): UseFormHistoryReturn<T>;
|
|
35
94
|
|
|
36
|
-
|
|
95
|
+
declare function useFormRewindContext(): FormRewindContextValue;
|
|
96
|
+
interface FormRewindProps {
|
|
97
|
+
initialState: Record<string, unknown>;
|
|
98
|
+
children: ReactNode;
|
|
99
|
+
onSubmit?: (state: Record<string, unknown>) => void;
|
|
100
|
+
maxHistory?: number;
|
|
101
|
+
debounceMs?: number;
|
|
102
|
+
persist?: {
|
|
103
|
+
key: string;
|
|
104
|
+
debounceMs?: number;
|
|
105
|
+
version?: number;
|
|
106
|
+
} | false;
|
|
107
|
+
keyboard?: boolean;
|
|
108
|
+
}
|
|
109
|
+
declare function FormRewind({ initialState, children, onSubmit, maxHistory, debounceMs, persist, keyboard, }: FormRewindProps): react.JSX.Element;
|
|
110
|
+
|
|
111
|
+
interface BaseFieldProps {
|
|
112
|
+
name: string;
|
|
113
|
+
label?: string;
|
|
114
|
+
rules?: FieldRules;
|
|
115
|
+
className?: string;
|
|
116
|
+
style?: React.CSSProperties;
|
|
117
|
+
}
|
|
118
|
+
interface TextFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange"> {
|
|
119
|
+
}
|
|
120
|
+
declare function TextField({ name, label, rules, className, style, ...inputProps }: TextFieldProps): react.JSX.Element;
|
|
121
|
+
interface NumberFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "value" | "onChange" | "type"> {
|
|
122
|
+
}
|
|
123
|
+
declare function NumberField({ name, label, rules, className, style, ...inputProps }: NumberFieldProps): react.JSX.Element;
|
|
124
|
+
interface CheckboxFieldProps extends BaseFieldProps, Omit<React.InputHTMLAttributes<HTMLInputElement>, "name" | "checked" | "onChange"> {
|
|
125
|
+
}
|
|
126
|
+
declare function CheckboxField({ name, label, rules, className, style, ...inputProps }: CheckboxFieldProps): react.JSX.Element;
|
|
127
|
+
interface SelectOption {
|
|
128
|
+
value: string;
|
|
129
|
+
label: string;
|
|
130
|
+
}
|
|
131
|
+
interface SelectFieldProps extends BaseFieldProps {
|
|
132
|
+
options: SelectOption[];
|
|
133
|
+
placeholder?: string;
|
|
134
|
+
}
|
|
135
|
+
declare function SelectField({ name, label, rules, options, placeholder, className, style }: SelectFieldProps): react.JSX.Element;
|
|
136
|
+
interface TextareaFieldProps extends BaseFieldProps, Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "name" | "value" | "onChange"> {
|
|
137
|
+
}
|
|
138
|
+
declare function TextareaField({ name, label, rules, className, style, ...textareaProps }: TextareaFieldProps): react.JSX.Element;
|
|
139
|
+
|
|
140
|
+
export { CheckboxField, type FieldError, type FieldHistory, type FieldMeta, type FieldRules, FormRewind, type FormRewindContextValue, type FormRewindProps, type HistoryEntry, NumberField, type PersistOptions, SelectField, TextField, TextareaField, type UseFormHistoryOptions, type UseFormHistoryReturn, useFormHistory, useFormRewindContext, validateAll, validateField };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
'use strict';var react=require('react');var j=100,V=300,X=500,Y=1;function q(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??X,version:e.version??Y}}function U(){return typeof window<"u"&&!!window.localStorage}function G(e,g){try{if(!U())return null;let f=window.localStorage.getItem(e);if(!f)return null;let E=JSON.parse(f);return E.__v!==g?(window.localStorage.removeItem(e),null):E.__s}catch{return null}}function K(e,g,f){try{if(!U())return;window.localStorage.setItem(e,JSON.stringify({__s:g,__v:f}));}catch{}}function Q(e){try{if(!U())return;window.localStorage.removeItem(e);}catch{}}function W(e,g={}){let{maxHistory:f=j,debounceMs:E=V,persist:C,keyboard:P=false,onUndo:k,onRedo:I,onSnapshot:M}=g,n=q(C),[R,h]=react.useState(()=>{if(n){let t=G(n.key,n.version);if(t!==null)return t}return e}),a=react.useRef(R);a.current=R;let u=react.useRef([]),c=react.useRef([]),[x,H]=react.useState([]),[L,S]=react.useState([]),o=react.useRef(null),m=react.useRef(null),i=react.useRef(null),y=react.useRef(null),T=react.useCallback(()=>{c.current=[],S([]);},[]),p=react.useCallback(t=>{let r=[...u.current,t];r.length>f&&r.splice(0,r.length-f),u.current=r,H(r);},[f]);react.useEffect(()=>()=>{o.current!==null&&clearTimeout(o.current),y.current!==null&&clearTimeout(y.current);},[]);let N=react.useCallback(t=>{n&&(y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{K(n.key,t,n.version),y.current=null;},n.debounceMs));},[n]),v=react.useCallback(t=>{n&&(y.current!==null&&(clearTimeout(y.current),y.current=null),K(n.key,t,n.version));},[n]),z=react.useCallback((t,r)=>{let s=a.current,d=typeof t=="function"?t(s):t;if(!Object.is(s,d)){if(h(d),a.current=d,N(d),E<=0){let F={state:s,timestamp:Date.now(),label:r};p(F),T();return}m.current===null&&(i.current=s),m.current=d,o.current!==null&&clearTimeout(o.current),o.current=setTimeout(()=>{let F={state:i.current,timestamp:Date.now(),label:r};p(F),T(),o.current=null,m.current=null,i.current=null;},E);}},[E,p,N,T]),D=react.useCallback(()=>{if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let d={state:i.current,timestamp:Date.now()};p(d),T(),m.current=null,i.current=null;}if(u.current.length===0)return;let t=u.current[u.current.length-1],r=u.current.slice(0,-1);u.current=r,H(r);let s={state:a.current,timestamp:Date.now()};c.current=[...c.current,s],S(c.current),h(t.state),a.current=t.state,v(t.state),k?.(t.state);},[p,k,v,T]),_=react.useCallback(()=>{if(c.current.length===0)return;let t=c.current[c.current.length-1],r=c.current.slice(0,-1);c.current=r,S(r);let s={state:a.current,timestamp:Date.now()};u.current=[...u.current,s],H(u.current),h(t.state),a.current=t.state,v(t.state),I?.(t.state);},[I,v]);react.useEffect(()=>{if(!P)return;let t=r=>{!(r.metaKey||r.ctrlKey)||r.key!=="z"||(r.preventDefault(),r.shiftKey?_():D());};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[P,D,_]);let b=react.useCallback(()=>{o.current!==null&&(clearTimeout(o.current),o.current=null),m.current=null,i.current=null,u.current=[],c.current=[],H([]),S([]);},[]),B=react.useCallback(t=>{let r=false;if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let s={state:i.current,timestamp:Date.now()};p(s),T(),m.current=null,i.current=null,r=true;}if(r)t&&u.current.length>0&&(u.current[u.current.length-1].label=t,H([...u.current]));else {let s={state:a.current,timestamp:Date.now(),label:t};p(s),T(),M?.(s);}},[p,M,T]),J=react.useCallback(()=>{b(),n&&Q(n.key),h(e),a.current=e;},[b,e,n]);return {state:R,setState:z,undo:D,redo:_,canUndo:x.length>0,canRedo:L.length>0,clearHistory:b,clearDraft:J,snapshot:B,past:x,future:L}}exports.useFormHistory=W;
|
|
1
|
+
'use strict';var react=require('react'),jsxRuntime=require('react/jsx-runtime');var ye=100,Fe=300,he=500,Te=1;function xe(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??he,version:e.version??Te}}function te(){return typeof window<"u"&&!!window.localStorage}function Re(e,n){try{if(!te())return null;let u=window.localStorage.getItem(e);if(!u)return null;let l=JSON.parse(u);return l.__v!==n?(window.localStorage.removeItem(e),null):l.__s}catch{return null}}function ie(e,n,u){try{if(!te())return;window.localStorage.setItem(e,JSON.stringify({__s:n,__v:u}));}catch{}}function ve(e){try{if(!te())return;window.localStorage.removeItem(e);}catch{}}function be(e,n={}){let{maxHistory:u=ye,debounceMs:l=Fe,persist:i,keyboard:a=false,onUndo:o,onRedo:p,onSnapshot:T}=n,r=xe(i),[m,F]=react.useState(()=>{if(r){let c=Re(r.key,r.version);if(c!==null)return c}return e}),g=react.useRef(m);g.current=m;let x=react.useRef([]),h=react.useRef([]),[H,S]=react.useState([]),[L,w]=react.useState([]),y=react.useRef(null),k=react.useRef(null),R=react.useRef(null),P=react.useRef(null),M=react.useCallback(()=>{h.current=[],w([]);},[]),N=react.useCallback(c=>{let d=[...x.current,c];d.length>u&&d.splice(0,d.length-u),x.current=d,S(d);},[u]);react.useEffect(()=>()=>{y.current!==null&&clearTimeout(y.current),P.current!==null&&clearTimeout(P.current);},[]);let Y=react.useCallback(c=>{r&&(P.current!==null&&clearTimeout(P.current),P.current=setTimeout(()=>{ie(r.key,c,r.version),P.current=null;},r.debounceMs));},[r]),$=react.useCallback(c=>{r&&(P.current!==null&&(clearTimeout(P.current),P.current=null),ie(r.key,c,r.version));},[r]),I=react.useCallback((c,d)=>{let v=g.current,O=typeof c=="function"?c(v):c;if(!Object.is(v,O)){if(F(O),g.current=O,Y(O),l<=0){let q={state:v,timestamp:Date.now(),label:d};N(q),M();return}k.current===null&&(R.current=v),k.current=O,y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{let q={state:R.current,timestamp:Date.now(),label:d};N(q),M(),y.current=null,k.current=null,R.current=null;},l);}},[l,N,Y,M]),j=react.useCallback(()=>{if(y.current!==null&&(clearTimeout(y.current),y.current=null),k.current!==null&&R.current!==null){let O={state:R.current,timestamp:Date.now()};N(O),M(),k.current=null,R.current=null;}if(x.current.length===0)return;let c=x.current[x.current.length-1],d=x.current.slice(0,-1);x.current=d,S(d);let v={state:g.current,timestamp:Date.now()};h.current=[...h.current,v],w(h.current),F(c.state),g.current=c.state,$(c.state),o?.(c.state);},[N,o,$,M]),K=react.useCallback(()=>{if(h.current.length===0)return;let c=h.current[h.current.length-1],d=h.current.slice(0,-1);h.current=d,w(d);let v={state:g.current,timestamp:Date.now()};x.current=[...x.current,v],S(x.current),F(c.state),g.current=c.state,$(c.state),p?.(c.state);},[p,$]);react.useEffect(()=>{if(!a)return;let c=d=>{!(d.metaKey||d.ctrlKey)||d.key!=="z"||(d.preventDefault(),d.shiftKey?K():j());};return window.addEventListener("keydown",c),()=>window.removeEventListener("keydown",c)},[a,j,K]);let V=react.useCallback(()=>{y.current!==null&&(clearTimeout(y.current),y.current=null),k.current=null,R.current=null,x.current=[],h.current=[],S([]),w([]);},[]),W=react.useCallback(c=>{let d=false;if(y.current!==null&&(clearTimeout(y.current),y.current=null),k.current!==null&&R.current!==null){let v={state:R.current,timestamp:Date.now()};N(v),M(),k.current=null,R.current=null,d=true;}if(d)c&&x.current.length>0&&(x.current[x.current.length-1].label=c,S([...x.current]));else {let v={state:g.current,timestamp:Date.now(),label:c};N(v),M(),T?.(v);}},[N,T,M]),z=react.useCallback(()=>{V(),r&&ve(r.key),F(e),g.current=e;},[V,e,r]);return {state:m,setState:I,undo:j,redo:K,canUndo:H.length>0,canRedo:L.length>0,clearHistory:V,clearDraft:z,snapshot:W,past:H,future:L}}function J(e,n){return e&&typeof e=="object"&&"value"in e?e:{value:e,message:n}}function G(e,n){let l=typeof e=="number"?e:Number(e);if(n.required&&(typeof e=="string"?e.trim()==="":e==null||e===false))return {message:typeof n.required=="string"?n.required:"Required",type:"required"};if(n.pattern!=null){let{value:i,message:a}=J(n.pattern,"Invalid format");if(typeof e=="string"&&!i.test(e))return {message:a,type:"pattern"}}if(n.minLength!=null&&typeof e=="string"){let{value:i,message:a}=J(n.minLength,`Min ${n.minLength} characters`);if(e.length<i)return {message:a,type:"minLength"}}if(n.maxLength!=null&&typeof e=="string"){let{value:i,message:a}=J(n.maxLength,`Max ${n.maxLength} characters`);if(e.length>i)return {message:a,type:"maxLength"}}if(n.min!=null&&!isNaN(l)){let{value:i,message:a}=J(n.min,`Min ${n.min}`);if(l<i)return {message:a,type:"min"}}if(n.max!=null&&!isNaN(l)){let{value:i,message:a}=J(n.max,`Max ${n.max}`);if(l>i)return {message:a,type:"max"}}if(n.validate){let i=n.validate(e);if(i)return {message:i,type:"validate"}}return null}function re(e,n){let u={};for(let l in n){let i=G(e[l],n[l]);i&&(u[l]=i);}return u}var ce=react.createContext(null);function A(){let e=react.useContext(ce);if(!e)throw new Error("useFormRewindContext must be used within <FormRewind>");return e}var Se=300;function le({initialState:e,children:n,onSubmit:u,maxHistory:l=100,debounceMs:i=Se,persist:a,keyboard:o=false}){let[p,T]=react.useState(e),r=react.useRef(p);r.current=p;let[m,F]=react.useState({}),[g,x]=react.useState({}),h=react.useRef({}),H=react.useRef({}),S=react.useRef({}),L=react.useRef({}),w=react.useRef({}),y=a&&typeof a=="object"?a:null,k=typeof window<"u"&&!!window.localStorage,R=react.useCallback(t=>{if(!(!y||!k))try{window.localStorage.setItem(y.key,JSON.stringify({__s:t,__v:y.version??1}));}catch{}},[y,k]),P=react.useCallback((t,s)=>{w.current[t]=s??{},h.current[t]||(h.current[t]={past:[],future:[]});},[]),M=react.useCallback(t=>{delete w.current[t],delete h.current[t],delete H.current[t],delete S.current[t],delete L.current[t];},[]),N=react.useCallback((t,s)=>{F(f=>({...f,[t]:s}));},[]),Y=react.useCallback(t=>{F(s=>{let f={...s};return delete f[t],f});},[]),$=react.useCallback(t=>{x(s=>({...s,[t]:true}));},[]),I=react.useCallback((t,s)=>{let f=h.current[t]??{past:[],future:[]};f.past.push(s),f.past.length>l&&f.past.shift(),f.future=[],h.current[t]=f;},[l]),j=react.useCallback((t,s)=>{let f=r.current[t];if(Object.is(f,s))return;let E={...r.current,[t]:s};if(T(E),r.current=E,R(E),i<=0){I(t,f);return}if(S.current[t]===null&&(L.current[t]=f),S.current[t]=s,H.current[t]!==null&&clearTimeout(H.current[t]),H.current[t]=setTimeout(()=>{I(t,L.current[t]),H.current[t]=null,S.current[t]=null,L.current[t]=null;},i),g[t]&&w.current[t]){let ne=G(s,w.current[t]);F(ne?Z=>({...Z,[t]:ne}):Z=>{let oe={...Z};return delete oe[t],oe});}},[i,I,R,g]),K=react.useCallback(t=>{H.current[t]!==null&&(clearTimeout(H.current[t]),H.current[t]=null,S.current[t]!==null&&L.current[t]!==null&&(I(t,L.current[t]),S.current[t]=null,L.current[t]=null));let s=h.current[t];if(!s||s.past.length===0)return;let f=s.past.pop();s.future.push(r.current[t]);let E={...r.current,[t]:f};T(E),r.current=E,R(E);},[I,R]),V=react.useCallback(t=>{let s=h.current[t];if(!s||s.future.length===0)return;let f=s.future.pop();s.past.push(r.current[t]);let E={...r.current,[t]:f};T(E),r.current=E,R(E);},[R]),W=react.useCallback(()=>{let t=document.activeElement;if(!t||!(t instanceof HTMLElement))return null;let s=t.getAttribute("name")||t.getAttribute("data-field");return s&&w.current[s]?s:null},[]),z=react.useRef(null);z.current=t=>{if(!(t.metaKey||t.ctrlKey)||t.key!=="z")return;let f=W();f&&(t.preventDefault(),t.shiftKey?V(f):K(f));};let c=react.useRef(o);c.current=o;let[d,v]=react.useState(false);o&&!d&&(window.addEventListener("keydown",t=>z.current?.(t)),v(true)),!o&&d&&(window.removeEventListener("keydown",t=>z.current?.(t)),v(false));let O=react.useCallback(t=>{t&&t.preventDefault();let s={};for(let E in w.current)s[E]=true;x(s);let f=re(r.current,w.current);return F(f),Object.keys(f).length===0&&u&&u(r.current),Object.keys(f).length===0},[u]),q={};for(let t in w.current){let s=h.current[t];q[t]={name:t,rules:w.current[t],touched:!!g[t],canUndo:s?s.past.length>0:false,canRedo:s?s.future.length>0:false};}let me={state:p,setState:j,errors:m,setError:N,clearError:Y,touched:g,touch:$,fields:q,registerField:P,unregisterField:M,undoField:K,redoField:V};return jsxRuntime.jsx(ce.Provider,{value:me,children:jsxRuntime.jsx("form",{onSubmit:O,noValidate:true,children:n})})}function ae({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();react.useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{o.setState(e,F.target.value);},T=()=>{o.touch(e);},r=o.errors[e],m=String(o.state[e]??"");return jsxRuntime.jsxs("div",{className:l,style:i,children:[n&&jsxRuntime.jsx("label",{htmlFor:e,children:n}),jsxRuntime.jsx("input",{id:e,type:"text",value:m,onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}function de({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();react.useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{let g=F.target.value===""?"":Number(F.target.value);o.setState(e,g);},T=()=>{o.touch(e);},r=o.errors[e],m=o.state[e];return jsxRuntime.jsxs("div",{className:l,style:i,children:[n&&jsxRuntime.jsx("label",{htmlFor:e,children:n}),jsxRuntime.jsx("input",{id:e,type:"number",value:m===""||m==null?"":Number(m),onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}function fe({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();react.useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=m=>{o.setState(e,m.target.checked);},T=o.errors[e],r=!!o.state[e];return jsxRuntime.jsxs("div",{className:l,style:i,children:[jsxRuntime.jsxs("label",{children:[jsxRuntime.jsx("input",{type:"checkbox",checked:r,onChange:p,"aria-invalid":!!T,...a}),n]}),T&&jsxRuntime.jsx("span",{role:"alert",children:T.message})]})}function pe({name:e,label:n,rules:u,options:l,placeholder:i,className:a,style:o}){let p=A();react.useEffect(()=>(p.registerField(e,u),()=>p.unregisterField(e)),[e,u,p.registerField,p.unregisterField]);let T=g=>{p.setState(e,g.target.value);},r=()=>{p.touch(e);},m=p.errors[e],F=String(p.state[e]??"");return jsxRuntime.jsxs("div",{className:a,style:o,children:[n&&jsxRuntime.jsx("label",{htmlFor:e,children:n}),jsxRuntime.jsxs("select",{id:e,value:F,onChange:T,onBlur:r,"aria-invalid":!!m,"aria-describedby":m?`${e}-error`:void 0,children:[i&&jsxRuntime.jsx("option",{value:"",disabled:true,children:i}),l.map(g=>jsxRuntime.jsx("option",{value:g.value,children:g.label},g.value))]}),m&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:m.message})]})}function ge({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();react.useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{o.setState(e,F.target.value);},T=()=>{o.touch(e);},r=o.errors[e],m=String(o.state[e]??"");return jsxRuntime.jsxs("div",{className:l,style:i,children:[n&&jsxRuntime.jsx("label",{htmlFor:e,children:n}),jsxRuntime.jsx("textarea",{id:e,value:m,onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsxRuntime.jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}exports.CheckboxField=fe;exports.FormRewind=le;exports.NumberField=de;exports.SelectField=pe;exports.TextField=ae;exports.TextareaField=ge;exports.useFormHistory=be;exports.useFormRewindContext=A;exports.validateAll=re;exports.validateField=G;
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import {useState,useRef,useCallback,useEffect}from'react';var j=100,V=300,X=500,Y=1;function q(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??X,version:e.version??Y}}function U(){return typeof window<"u"&&!!window.localStorage}function G(e,g){try{if(!U())return null;let f=window.localStorage.getItem(e);if(!f)return null;let E=JSON.parse(f);return E.__v!==g?(window.localStorage.removeItem(e),null):E.__s}catch{return null}}function K(e,g,f){try{if(!U())return;window.localStorage.setItem(e,JSON.stringify({__s:g,__v:f}));}catch{}}function Q(e){try{if(!U())return;window.localStorage.removeItem(e);}catch{}}function W(e,g={}){let{maxHistory:f=j,debounceMs:E=V,persist:C,keyboard:P=false,onUndo:k,onRedo:I,onSnapshot:M}=g,n=q(C),[R,h]=useState(()=>{if(n){let t=G(n.key,n.version);if(t!==null)return t}return e}),a=useRef(R);a.current=R;let u=useRef([]),c=useRef([]),[x,H]=useState([]),[L,S]=useState([]),o=useRef(null),m=useRef(null),i=useRef(null),y=useRef(null),T=useCallback(()=>{c.current=[],S([]);},[]),p=useCallback(t=>{let r=[...u.current,t];r.length>f&&r.splice(0,r.length-f),u.current=r,H(r);},[f]);useEffect(()=>()=>{o.current!==null&&clearTimeout(o.current),y.current!==null&&clearTimeout(y.current);},[]);let N=useCallback(t=>{n&&(y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{K(n.key,t,n.version),y.current=null;},n.debounceMs));},[n]),v=useCallback(t=>{n&&(y.current!==null&&(clearTimeout(y.current),y.current=null),K(n.key,t,n.version));},[n]),z=useCallback((t,r)=>{let s=a.current,d=typeof t=="function"?t(s):t;if(!Object.is(s,d)){if(h(d),a.current=d,N(d),E<=0){let F={state:s,timestamp:Date.now(),label:r};p(F),T();return}m.current===null&&(i.current=s),m.current=d,o.current!==null&&clearTimeout(o.current),o.current=setTimeout(()=>{let F={state:i.current,timestamp:Date.now(),label:r};p(F),T(),o.current=null,m.current=null,i.current=null;},E);}},[E,p,N,T]),D=useCallback(()=>{if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let d={state:i.current,timestamp:Date.now()};p(d),T(),m.current=null,i.current=null;}if(u.current.length===0)return;let t=u.current[u.current.length-1],r=u.current.slice(0,-1);u.current=r,H(r);let s={state:a.current,timestamp:Date.now()};c.current=[...c.current,s],S(c.current),h(t.state),a.current=t.state,v(t.state),k?.(t.state);},[p,k,v,T]),_=useCallback(()=>{if(c.current.length===0)return;let t=c.current[c.current.length-1],r=c.current.slice(0,-1);c.current=r,S(r);let s={state:a.current,timestamp:Date.now()};u.current=[...u.current,s],H(u.current),h(t.state),a.current=t.state,v(t.state),I?.(t.state);},[I,v]);useEffect(()=>{if(!P)return;let t=r=>{!(r.metaKey||r.ctrlKey)||r.key!=="z"||(r.preventDefault(),r.shiftKey?_():D());};return window.addEventListener("keydown",t),()=>window.removeEventListener("keydown",t)},[P,D,_]);let b=useCallback(()=>{o.current!==null&&(clearTimeout(o.current),o.current=null),m.current=null,i.current=null,u.current=[],c.current=[],H([]),S([]);},[]),B=useCallback(t=>{let r=false;if(o.current!==null&&(clearTimeout(o.current),o.current=null),m.current!==null&&i.current!==null){let s={state:i.current,timestamp:Date.now()};p(s),T(),m.current=null,i.current=null,r=true;}if(r)t&&u.current.length>0&&(u.current[u.current.length-1].label=t,H([...u.current]));else {let s={state:a.current,timestamp:Date.now(),label:t};p(s),T(),M?.(s);}},[p,M,T]),J=useCallback(()=>{b(),n&&Q(n.key),h(e),a.current=e;},[b,e,n]);return {state:R,setState:z,undo:D,redo:_,canUndo:x.length>0,canRedo:L.length>0,clearHistory:b,clearDraft:J,snapshot:B,past:x,future:L}}export{W as useFormHistory};
|
|
1
|
+
import {createContext,useState,useRef,useCallback,useEffect,useContext}from'react';import {jsx,jsxs}from'react/jsx-runtime';var ye=100,Fe=300,he=500,Te=1;function xe(e){return !e||typeof e=="boolean"?null:{key:e.key,debounceMs:e.debounceMs??he,version:e.version??Te}}function te(){return typeof window<"u"&&!!window.localStorage}function Re(e,n){try{if(!te())return null;let u=window.localStorage.getItem(e);if(!u)return null;let l=JSON.parse(u);return l.__v!==n?(window.localStorage.removeItem(e),null):l.__s}catch{return null}}function ie(e,n,u){try{if(!te())return;window.localStorage.setItem(e,JSON.stringify({__s:n,__v:u}));}catch{}}function ve(e){try{if(!te())return;window.localStorage.removeItem(e);}catch{}}function be(e,n={}){let{maxHistory:u=ye,debounceMs:l=Fe,persist:i,keyboard:a=false,onUndo:o,onRedo:p,onSnapshot:T}=n,r=xe(i),[m,F]=useState(()=>{if(r){let c=Re(r.key,r.version);if(c!==null)return c}return e}),g=useRef(m);g.current=m;let x=useRef([]),h=useRef([]),[H,S]=useState([]),[L,w]=useState([]),y=useRef(null),k=useRef(null),R=useRef(null),P=useRef(null),M=useCallback(()=>{h.current=[],w([]);},[]),N=useCallback(c=>{let d=[...x.current,c];d.length>u&&d.splice(0,d.length-u),x.current=d,S(d);},[u]);useEffect(()=>()=>{y.current!==null&&clearTimeout(y.current),P.current!==null&&clearTimeout(P.current);},[]);let Y=useCallback(c=>{r&&(P.current!==null&&clearTimeout(P.current),P.current=setTimeout(()=>{ie(r.key,c,r.version),P.current=null;},r.debounceMs));},[r]),$=useCallback(c=>{r&&(P.current!==null&&(clearTimeout(P.current),P.current=null),ie(r.key,c,r.version));},[r]),I=useCallback((c,d)=>{let v=g.current,O=typeof c=="function"?c(v):c;if(!Object.is(v,O)){if(F(O),g.current=O,Y(O),l<=0){let q={state:v,timestamp:Date.now(),label:d};N(q),M();return}k.current===null&&(R.current=v),k.current=O,y.current!==null&&clearTimeout(y.current),y.current=setTimeout(()=>{let q={state:R.current,timestamp:Date.now(),label:d};N(q),M(),y.current=null,k.current=null,R.current=null;},l);}},[l,N,Y,M]),j=useCallback(()=>{if(y.current!==null&&(clearTimeout(y.current),y.current=null),k.current!==null&&R.current!==null){let O={state:R.current,timestamp:Date.now()};N(O),M(),k.current=null,R.current=null;}if(x.current.length===0)return;let c=x.current[x.current.length-1],d=x.current.slice(0,-1);x.current=d,S(d);let v={state:g.current,timestamp:Date.now()};h.current=[...h.current,v],w(h.current),F(c.state),g.current=c.state,$(c.state),o?.(c.state);},[N,o,$,M]),K=useCallback(()=>{if(h.current.length===0)return;let c=h.current[h.current.length-1],d=h.current.slice(0,-1);h.current=d,w(d);let v={state:g.current,timestamp:Date.now()};x.current=[...x.current,v],S(x.current),F(c.state),g.current=c.state,$(c.state),p?.(c.state);},[p,$]);useEffect(()=>{if(!a)return;let c=d=>{!(d.metaKey||d.ctrlKey)||d.key!=="z"||(d.preventDefault(),d.shiftKey?K():j());};return window.addEventListener("keydown",c),()=>window.removeEventListener("keydown",c)},[a,j,K]);let V=useCallback(()=>{y.current!==null&&(clearTimeout(y.current),y.current=null),k.current=null,R.current=null,x.current=[],h.current=[],S([]),w([]);},[]),W=useCallback(c=>{let d=false;if(y.current!==null&&(clearTimeout(y.current),y.current=null),k.current!==null&&R.current!==null){let v={state:R.current,timestamp:Date.now()};N(v),M(),k.current=null,R.current=null,d=true;}if(d)c&&x.current.length>0&&(x.current[x.current.length-1].label=c,S([...x.current]));else {let v={state:g.current,timestamp:Date.now(),label:c};N(v),M(),T?.(v);}},[N,T,M]),z=useCallback(()=>{V(),r&&ve(r.key),F(e),g.current=e;},[V,e,r]);return {state:m,setState:I,undo:j,redo:K,canUndo:H.length>0,canRedo:L.length>0,clearHistory:V,clearDraft:z,snapshot:W,past:H,future:L}}function J(e,n){return e&&typeof e=="object"&&"value"in e?e:{value:e,message:n}}function G(e,n){let l=typeof e=="number"?e:Number(e);if(n.required&&(typeof e=="string"?e.trim()==="":e==null||e===false))return {message:typeof n.required=="string"?n.required:"Required",type:"required"};if(n.pattern!=null){let{value:i,message:a}=J(n.pattern,"Invalid format");if(typeof e=="string"&&!i.test(e))return {message:a,type:"pattern"}}if(n.minLength!=null&&typeof e=="string"){let{value:i,message:a}=J(n.minLength,`Min ${n.minLength} characters`);if(e.length<i)return {message:a,type:"minLength"}}if(n.maxLength!=null&&typeof e=="string"){let{value:i,message:a}=J(n.maxLength,`Max ${n.maxLength} characters`);if(e.length>i)return {message:a,type:"maxLength"}}if(n.min!=null&&!isNaN(l)){let{value:i,message:a}=J(n.min,`Min ${n.min}`);if(l<i)return {message:a,type:"min"}}if(n.max!=null&&!isNaN(l)){let{value:i,message:a}=J(n.max,`Max ${n.max}`);if(l>i)return {message:a,type:"max"}}if(n.validate){let i=n.validate(e);if(i)return {message:i,type:"validate"}}return null}function re(e,n){let u={};for(let l in n){let i=G(e[l],n[l]);i&&(u[l]=i);}return u}var ce=createContext(null);function A(){let e=useContext(ce);if(!e)throw new Error("useFormRewindContext must be used within <FormRewind>");return e}var Se=300;function le({initialState:e,children:n,onSubmit:u,maxHistory:l=100,debounceMs:i=Se,persist:a,keyboard:o=false}){let[p,T]=useState(e),r=useRef(p);r.current=p;let[m,F]=useState({}),[g,x]=useState({}),h=useRef({}),H=useRef({}),S=useRef({}),L=useRef({}),w=useRef({}),y=a&&typeof a=="object"?a:null,k=typeof window<"u"&&!!window.localStorage,R=useCallback(t=>{if(!(!y||!k))try{window.localStorage.setItem(y.key,JSON.stringify({__s:t,__v:y.version??1}));}catch{}},[y,k]),P=useCallback((t,s)=>{w.current[t]=s??{},h.current[t]||(h.current[t]={past:[],future:[]});},[]),M=useCallback(t=>{delete w.current[t],delete h.current[t],delete H.current[t],delete S.current[t],delete L.current[t];},[]),N=useCallback((t,s)=>{F(f=>({...f,[t]:s}));},[]),Y=useCallback(t=>{F(s=>{let f={...s};return delete f[t],f});},[]),$=useCallback(t=>{x(s=>({...s,[t]:true}));},[]),I=useCallback((t,s)=>{let f=h.current[t]??{past:[],future:[]};f.past.push(s),f.past.length>l&&f.past.shift(),f.future=[],h.current[t]=f;},[l]),j=useCallback((t,s)=>{let f=r.current[t];if(Object.is(f,s))return;let E={...r.current,[t]:s};if(T(E),r.current=E,R(E),i<=0){I(t,f);return}if(S.current[t]===null&&(L.current[t]=f),S.current[t]=s,H.current[t]!==null&&clearTimeout(H.current[t]),H.current[t]=setTimeout(()=>{I(t,L.current[t]),H.current[t]=null,S.current[t]=null,L.current[t]=null;},i),g[t]&&w.current[t]){let ne=G(s,w.current[t]);F(ne?Z=>({...Z,[t]:ne}):Z=>{let oe={...Z};return delete oe[t],oe});}},[i,I,R,g]),K=useCallback(t=>{H.current[t]!==null&&(clearTimeout(H.current[t]),H.current[t]=null,S.current[t]!==null&&L.current[t]!==null&&(I(t,L.current[t]),S.current[t]=null,L.current[t]=null));let s=h.current[t];if(!s||s.past.length===0)return;let f=s.past.pop();s.future.push(r.current[t]);let E={...r.current,[t]:f};T(E),r.current=E,R(E);},[I,R]),V=useCallback(t=>{let s=h.current[t];if(!s||s.future.length===0)return;let f=s.future.pop();s.past.push(r.current[t]);let E={...r.current,[t]:f};T(E),r.current=E,R(E);},[R]),W=useCallback(()=>{let t=document.activeElement;if(!t||!(t instanceof HTMLElement))return null;let s=t.getAttribute("name")||t.getAttribute("data-field");return s&&w.current[s]?s:null},[]),z=useRef(null);z.current=t=>{if(!(t.metaKey||t.ctrlKey)||t.key!=="z")return;let f=W();f&&(t.preventDefault(),t.shiftKey?V(f):K(f));};let c=useRef(o);c.current=o;let[d,v]=useState(false);o&&!d&&(window.addEventListener("keydown",t=>z.current?.(t)),v(true)),!o&&d&&(window.removeEventListener("keydown",t=>z.current?.(t)),v(false));let O=useCallback(t=>{t&&t.preventDefault();let s={};for(let E in w.current)s[E]=true;x(s);let f=re(r.current,w.current);return F(f),Object.keys(f).length===0&&u&&u(r.current),Object.keys(f).length===0},[u]),q={};for(let t in w.current){let s=h.current[t];q[t]={name:t,rules:w.current[t],touched:!!g[t],canUndo:s?s.past.length>0:false,canRedo:s?s.future.length>0:false};}let me={state:p,setState:j,errors:m,setError:N,clearError:Y,touched:g,touch:$,fields:q,registerField:P,unregisterField:M,undoField:K,redoField:V};return jsx(ce.Provider,{value:me,children:jsx("form",{onSubmit:O,noValidate:true,children:n})})}function ae({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{o.setState(e,F.target.value);},T=()=>{o.touch(e);},r=o.errors[e],m=String(o.state[e]??"");return jsxs("div",{className:l,style:i,children:[n&&jsx("label",{htmlFor:e,children:n}),jsx("input",{id:e,type:"text",value:m,onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}function de({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{let g=F.target.value===""?"":Number(F.target.value);o.setState(e,g);},T=()=>{o.touch(e);},r=o.errors[e],m=o.state[e];return jsxs("div",{className:l,style:i,children:[n&&jsx("label",{htmlFor:e,children:n}),jsx("input",{id:e,type:"number",value:m===""||m==null?"":Number(m),onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}function fe({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=m=>{o.setState(e,m.target.checked);},T=o.errors[e],r=!!o.state[e];return jsxs("div",{className:l,style:i,children:[jsxs("label",{children:[jsx("input",{type:"checkbox",checked:r,onChange:p,"aria-invalid":!!T,...a}),n]}),T&&jsx("span",{role:"alert",children:T.message})]})}function pe({name:e,label:n,rules:u,options:l,placeholder:i,className:a,style:o}){let p=A();useEffect(()=>(p.registerField(e,u),()=>p.unregisterField(e)),[e,u,p.registerField,p.unregisterField]);let T=g=>{p.setState(e,g.target.value);},r=()=>{p.touch(e);},m=p.errors[e],F=String(p.state[e]??"");return jsxs("div",{className:a,style:o,children:[n&&jsx("label",{htmlFor:e,children:n}),jsxs("select",{id:e,value:F,onChange:T,onBlur:r,"aria-invalid":!!m,"aria-describedby":m?`${e}-error`:void 0,children:[i&&jsx("option",{value:"",disabled:true,children:i}),l.map(g=>jsx("option",{value:g.value,children:g.label},g.value))]}),m&&jsx("span",{id:`${e}-error`,role:"alert",children:m.message})]})}function ge({name:e,label:n,rules:u,className:l,style:i,...a}){let o=A();useEffect(()=>(o.registerField(e,u),()=>o.unregisterField(e)),[e,u,o.registerField,o.unregisterField]);let p=F=>{o.setState(e,F.target.value);},T=()=>{o.touch(e);},r=o.errors[e],m=String(o.state[e]??"");return jsxs("div",{className:l,style:i,children:[n&&jsx("label",{htmlFor:e,children:n}),jsx("textarea",{id:e,value:m,onChange:p,onBlur:T,"aria-invalid":!!r,"aria-describedby":r?`${e}-error`:void 0,...a}),r&&jsx("span",{id:`${e}-error`,role:"alert",children:r.message})]})}export{fe as CheckboxField,le as FormRewind,de as NumberField,pe as SelectField,ae as TextField,ge as TextareaField,be as useFormHistory,A as useFormRewindContext,re as validateAll,G as validateField};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-form-rewind",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Zero-dependency React state engine with auto-saved history stacks, time-traveling undo/redo, keyboard shortcuts, and draft persistence for forms.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|