use-form-draft 0.1.0 → 0.2.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 +50 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,20 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
> Auto-save React form drafts to `localStorage` and restore them on mount — with an optional recovery banner. Works with plain `useState`, React Hook Form, Formik, or anything that has a value and a setter.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/use-form-draft)
|
|
6
|
+
[](https://www.npmjs.com/package/use-form-draft)
|
|
5
7
|

|
|
6
|
-

|
|
8
|
+

|
|
9
|
+

|
|
8
10
|

|
|
9
11
|
|
|
10
12
|
Long forms get abandoned mid-fill — a switched tab, a browser crash, an accidental refresh. `use-form-draft` is the small, tested helper that quietly persists what the user has typed and offers it back when they return.
|
|
11
13
|
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
> [!IMPORTANT]
|
|
15
|
-
> **Status — pre-release (`v0.1.0`).** The library is feature-complete and covered by a substantial test suite, but it is **not yet published to npm**. Until the first release is cut, install it [from source](#local-development). The `npm install` line below is how you'll add it once it's published. There are no other distribution channels yet — if you find it on a registry under this name before an official release, it isn't from this project.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
14
|
## Contents
|
|
20
15
|
|
|
21
16
|
- [Why it exists](#why-it-exists)
|
|
@@ -30,6 +25,8 @@ Long forms get abandoned mid-fill — a switched tab, a browser crash, an accide
|
|
|
30
25
|
- [Excluding sensitive fields](#excluding-sensitive-fields)
|
|
31
26
|
- [Schema versioning](#schema-versioning)
|
|
32
27
|
- [Expiry (TTL)](#expiry-ttl)
|
|
28
|
+
- [Cross-tab sync](#cross-tab-sync)
|
|
29
|
+
- [Custom storage backends](#custom-storage-backends)
|
|
33
30
|
- [Theming the banner](#theming-the-banner)
|
|
34
31
|
- [Internationalisation](#internationalisation)
|
|
35
32
|
- [API reference](#api-reference)
|
|
@@ -66,12 +63,12 @@ Every team eventually writes its own "debounce the form state into `localStorage
|
|
|
66
63
|
- **Sensitive-field exclusion** — strip passwords / CVVs before anything touches storage.
|
|
67
64
|
- **Recovery UI, your choice** — a themeable `<DraftBanner>`, a headless `useDraftBanner` hook, or nothing at all.
|
|
68
65
|
- **SSR- and StrictMode-safe** — verified by `react-dom/server` and double-mount tests, not just guards.
|
|
66
|
+
- **Pluggable storage** — `localStorage` (default), `sessionStorage`, or your own synchronous adapter.
|
|
67
|
+
- **Cross-tab aware** (opt-in) — a draft saved in one tab can restore into another.
|
|
69
68
|
- **Fully typed**, ESM + CJS, with bundled `.d.ts`.
|
|
70
69
|
|
|
71
70
|
## Install
|
|
72
71
|
|
|
73
|
-
> Not on npm yet — see [Status](#use-form-draft) above. Once published:
|
|
74
|
-
|
|
75
72
|
```bash
|
|
76
73
|
npm install use-form-draft
|
|
77
74
|
# or
|
|
@@ -82,8 +79,6 @@ yarn add use-form-draft
|
|
|
82
79
|
|
|
83
80
|
`react >= 17` is a peer dependency. `react-hook-form >= 7` is an **optional** peer — only needed if you import the `use-form-draft/rhf` entry.
|
|
84
81
|
|
|
85
|
-
Until the first release, install [from source](#local-development).
|
|
86
|
-
|
|
87
82
|
## Quick start (plain `useState`)
|
|
88
83
|
|
|
89
84
|
```tsx
|
|
@@ -257,6 +252,41 @@ Drafts older than `ttlDays` (default **30**) are treated as absent on read and c
|
|
|
257
252
|
useFormDraft('draft:tender', state, hydrate, { ttlDays: 7 });
|
|
258
253
|
```
|
|
259
254
|
|
|
255
|
+
### Cross-tab sync
|
|
256
|
+
|
|
257
|
+
By default each tab keeps its own copy. Pass `crossTab: true` and a draft saved in one tab is restored into any other tab editing the same key — handy when a user duplicates a long form into a second tab:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
const draft = useFormDraft('draft:tender:create', form, setForm, { crossTab: true });
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
It listens for the browser's `storage` event, so it only applies to `localStorage` (the default backend). Syncing is **last-write-wins** — a save in another tab can overwrite what's being typed here — so opt in deliberately. When another tab *clears* the draft, this instance drops its "restored" badge but doesn't wipe what you're currently editing.
|
|
264
|
+
|
|
265
|
+
### Custom storage backends
|
|
266
|
+
|
|
267
|
+
The hook persists through a tiny synchronous interface, so you can point it anywhere. `window.sessionStorage` already satisfies it (tab-scoped drafts that vanish when the tab closes):
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
useFormDraft('draft:tender', state, hydrate, { storage: window.sessionStorage });
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Or supply your own adapter — namespaced, encrypted, in-memory for tests, etc:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
import type { DraftStorage } from 'use-form-draft';
|
|
277
|
+
|
|
278
|
+
const memory = new Map<string, string>();
|
|
279
|
+
const inMemory: DraftStorage = {
|
|
280
|
+
getItem: (k) => memory.get(k) ?? null,
|
|
281
|
+
setItem: (k, v) => void memory.set(k, v),
|
|
282
|
+
removeItem: (k) => void memory.delete(k),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
useFormDraft('draft:tender', state, hydrate, { storage: inMemory });
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The interface is intentionally synchronous; async stores like IndexedDB aren't supported yet (see [Roadmap](#roadmap)).
|
|
289
|
+
|
|
260
290
|
### Theming the banner
|
|
261
291
|
|
|
262
292
|
`<DraftBanner>` is styled with inline defaults that read from CSS custom properties. Set them anywhere in your CSS to retheme every banner at once:
|
|
@@ -301,6 +331,8 @@ Pass `locale` (drives `Intl.RelativeTimeFormat`) and `messages` to fully localis
|
|
|
301
331
|
| `options.exclude` | `ReadonlyArray<keyof T>` | Keys stripped before persisting (passwords, CVVs). |
|
|
302
332
|
| `options.hasFile` | `boolean` | Stored as a flag so the banner can prompt re-attach. File *content* is never persisted. Default `false`. |
|
|
303
333
|
| `options.debounceMs` | `number` | Write debounce window. Default `400`. |
|
|
334
|
+
| `options.storage` | `DraftStorage` | Where to persist. Default `window.localStorage`. Pass `window.sessionStorage` or a custom synchronous adapter. |
|
|
335
|
+
| `options.crossTab` | `boolean` | Restore a draft saved in another tab into this one (localStorage only, last-write-wins). Default `false`. |
|
|
304
336
|
|
|
305
337
|
**Returns** `UseFormDraftReturn`:
|
|
306
338
|
|
|
@@ -361,17 +393,16 @@ Known and deliberate for this version — call them out so you don't get surpris
|
|
|
361
393
|
|
|
362
394
|
- **The `key` must be stable for the component's lifetime.** Changing it while mounted has two failure modes: (1) the new key's existing draft is **not** restored (the restore effect runs once, on mount); (2) a pending debounced write for the old key still lands on the old key. If your key depends on a route param or entity id, unmount + remount the component instead — e.g. `<Form key={id} />` so React swaps the instance. Native key-change handling is planned for v0.2.
|
|
363
395
|
- **Same-key concurrency is last-write-wins.** Two components mounted with the same `key` at once will race. The typical pattern — one form open per key — is safe.
|
|
364
|
-
- **
|
|
365
|
-
-
|
|
396
|
+
- **Cross-tab sync is opt-in and last-write-wins.** With `crossTab: true`, a save in another tab can overwrite what's being edited here — there's no automatic merge or conflict resolution.
|
|
397
|
+
- **Synchronous storage only.** `localStorage` (default), `sessionStorage`, and custom sync adapters work via `storage`; async stores like IndexedDB aren't supported by the interface yet.
|
|
366
398
|
- **The `exclude` type caveat** described [above](#excluding-sensitive-fields).
|
|
367
399
|
|
|
368
400
|
## Roadmap
|
|
369
401
|
|
|
370
|
-
Not
|
|
402
|
+
Not done yet, planned:
|
|
371
403
|
|
|
372
|
-
- Cross-tab / cross-instance coordination via `BroadcastChannel` or the `storage` event.
|
|
373
404
|
- Native `key`-change handling.
|
|
374
|
-
-
|
|
405
|
+
- An **async** storage-adapter interface for IndexedDB (large drafts, file metadata, rich text). Synchronous backends — `localStorage`, `sessionStorage`, custom adapters — already work via `storage`.
|
|
375
406
|
- Precisely-typed `exclude` (`Omit<T, ExcludedKeys>` in the hydrate signature).
|
|
376
407
|
- Banner: focus management and ARIA alert escalation.
|
|
377
408
|
- Optional encryption at rest.
|
|
@@ -409,8 +440,6 @@ An honest sketch — feature columns and last-release dates were checked against
|
|
|
409
440
|
|
|
410
441
|
## Local development
|
|
411
442
|
|
|
412
|
-
This is the supported way to run it until the first npm release.
|
|
413
|
-
|
|
414
443
|
```bash
|
|
415
444
|
git clone https://github.com/Maaz046/use-form-draft.git
|
|
416
445
|
cd use-form-draft
|
|
@@ -421,7 +450,7 @@ npm run typecheck # tsc --noEmit
|
|
|
421
450
|
npm run build # bundle ESM + CJS + .d.ts with tsup
|
|
422
451
|
```
|
|
423
452
|
|
|
424
|
-
To
|
|
453
|
+
To test an unreleased change against another local project, build it and install the folder (or `npm pack` it and install the resulting tarball):
|
|
425
454
|
|
|
426
455
|
```bash
|
|
427
456
|
npm run build
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-form-draft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Library-agnostic React hook for auto-saving form drafts to localStorage, with restore-on-mount and a themable recovery banner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|