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.
Files changed (2) hide show
  1. package/README.md +50 -21
  2. 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
+ [![npm version](https://img.shields.io/npm/v/use-form-draft?color=cb3837)](https://www.npmjs.com/package/use-form-draft)
6
+ [![npm downloads](https://img.shields.io/npm/dm/use-form-draft?color=cb3837)](https://www.npmjs.com/package/use-form-draft)
5
7
  ![license](https://img.shields.io/badge/license-MIT-3b82f6)
6
- ![size](https://img.shields.io/badge/min%2Bgzip-~2%20kB-22c55e)
7
- ![deps](https://img.shields.io/badge/runtime%20deps-0-22c55e)
8
+ ![min+gzip](https://img.shields.io/badge/min%2Bgzip-~2%20kB-22c55e)
9
+ ![runtime deps](https://img.shields.io/badge/runtime%20deps-0-22c55e)
8
10
  ![types](https://img.shields.io/badge/types-included-3b82f6)
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
- - **No cross-tab / cross-instance coordination** yet. Editing the same draft in two tabs is last-write-wins.
365
- - **`localStorage` only.** No `sessionStorage` or IndexedDB adapter yet.
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 in v0.1, planned:
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
- - `sessionStorage` and IndexedDB storage adapters (large drafts, file metadata, rich text).
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 use it in another local project before it's published, build it and install the folder (or `npm pack` it and install the resulting tarball):
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.1.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",