@samline/forms 2.7.2 → 2.9.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 (58) hide show
  1. package/CHANGELOG.md +316 -0
  2. package/README.md +119 -119
  3. package/dist/browser/global.d.ts +50 -16
  4. package/dist/browser/global.global.js +12 -6188
  5. package/dist/browser/global.global.js.map +1 -1
  6. package/dist/browser/index.cjs +340 -84
  7. package/dist/browser/index.cjs.map +1 -1
  8. package/dist/browser/index.d.cts +1 -1
  9. package/dist/browser/index.d.ts +1 -1
  10. package/dist/browser/index.js +1 -1
  11. package/dist/{chunk-MMHX34WB.js → chunk-6MA5TF6R.js} +341 -85
  12. package/dist/chunk-6MA5TF6R.js.map +1 -0
  13. package/dist/index.cjs +340 -84
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +2 -2
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +1 -1
  18. package/dist/{registry-GzopYPCR.d.ts → registry-CPe_lf8-.d.cts} +50 -4
  19. package/dist/{registry-GzopYPCR.d.cts → registry-CPe_lf8-.d.ts} +50 -4
  20. package/package.json +39 -8
  21. package/dist/chunk-MMHX34WB.js.map +0 -1
  22. package/docs/api/add-cleanup.md +0 -52
  23. package/docs/api/append.md +0 -96
  24. package/docs/api/auto-submit.md +0 -91
  25. package/docs/api/clear-errors.md +0 -69
  26. package/docs/api/destroy.md +0 -80
  27. package/docs/api/disable-auto-submit.md +0 -65
  28. package/docs/api/element.md +0 -78
  29. package/docs/api/form.md +0 -156
  30. package/docs/api/format.md +0 -270
  31. package/docs/api/get-data.md +0 -90
  32. package/docs/api/get-field.md +0 -87
  33. package/docs/api/get-state.md +0 -111
  34. package/docs/api/get-value.md +0 -114
  35. package/docs/api/index.md +0 -55
  36. package/docs/api/observe.md +0 -126
  37. package/docs/api/on-submit.md +0 -94
  38. package/docs/api/parse-form-data.md +0 -94
  39. package/docs/api/prefill.md +0 -89
  40. package/docs/api/regex.md +0 -155
  41. package/docs/api/reset.md +0 -61
  42. package/docs/api/revalidate.md +0 -52
  43. package/docs/api/set-errors.md +0 -97
  44. package/docs/api/set-value.md +0 -120
  45. package/docs/api/subscribe.md +0 -107
  46. package/docs/api/unwatch.md +0 -74
  47. package/docs/api/validate-field-value.md +0 -144
  48. package/docs/api/validate-values.md +0 -103
  49. package/docs/api/validate.md +0 -102
  50. package/docs/api/watch.md +0 -117
  51. package/docs/browser.md +0 -156
  52. package/docs/css-styling.md +0 -191
  53. package/docs/getting-started.md +0 -222
  54. package/docs/options.md +0 -326
  55. package/docs/recipes.md +0 -531
  56. package/docs/typescript.md +0 -615
  57. package/docs/vanilla.md +0 -12
  58. /package/{LICENSE → LICENSE.md} +0 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,316 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@samline/forms` are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [2.9.0] - 2026-11-12
8
+
9
+ No breaking API changes.
10
+
11
+ ### Added
12
+
13
+ - `setOptions(name, options, config?)` replaces the options of a `<select>` with strings, numbers, `{ value, label, disabled, selected }` objects or `{ label, options }` groups. Labels are written as text. The current value is kept when it is still listed (`keepValue: true` by default), and the change pipeline runs once, only when the selected value changes ([#13](https://github.com/samline/forms/issues/13)). New exported types: `SelectOption`, `SelectOptionGroup`, `SelectOptionInput` and `SetOptionsConfig`.
14
+ - `watch()` and `observe()` accept an array of field names. The callback receives `(values, changed, form, state)`: one value per listed field and the name of the field that changed (`null` on registration and after `reset()`). It fires once per change of any listed field. `unwatch(fields, callback?)` removes these watchers. New exported type: `FormFieldsWatcher` ([#10](https://github.com/samline/forms/issues/10)).
15
+
16
+ ### Fixed
17
+
18
+ - Adding or removing `<option>` elements outside the controller is no longer ignored. When it changes a select's value, the controller revalidates, updates `css-filled` / `css-error`, fires watchers and notifies subscribers.
19
+
20
+ ## [2.8.0] - 2026-11-06
21
+
22
+ No breaking API changes. The behavior changes below fix incorrect or surprising results; review them if your code relied on the old behavior.
23
+
24
+ ### Fixed
25
+
26
+ - `prefill()` writes a repeated query key (`?tags[]=a&tags[]=b`) once as an array instead of keeping only the last value, and auto-submit runs once after the whole prefill instead of once per field.
27
+ - A native `<button type="reset">` or a direct `form.reset()` now clears errors, `css-filled`, `css-error` and `aria-invalid` and restores formatted mirrors, like `reset()`. A canceled `reset` event is respected.
28
+ - The form's `MutationObserver` ignores markup that contains no form controls, so a subscriber that renders messages or a summary into the form no longer re-enters validation in an endless loop.
29
+ - An invalid submit started by auto-submit no longer moves focus away from the field being edited.
30
+ - After a failed submit, focus goes to the first invalid control in document order instead of the first field declared in `validators`.
31
+ - Formatting no longer moves the selection of an input that does not have focus (initial values, `setValue()`, `reset()`), which could also focus it in Safari.
32
+ - After `destroy()`, mutating methods (`setValue`, `setErrors`, `clearErrors`, `validate`, `revalidate`, `reset`, `prefill`, `append`, `autoSubmit`) and new registrations (`subscribe`, `observe`, `watch`, `onSubmit`) are no-ops, so an async handler that settles late cannot repaint a form now owned by another controller. `validate()` returns `{ isValid: true, errors: {} }`.
33
+ - `window.Forms` from the standalone script has exactly `form`, `newForm`, `destroyForm` and `available`; it no longer includes a `default` key.
34
+
35
+ ### Changed
36
+
37
+ - `reset()` and native resets call `watch()`/`observe()` callbacks with the restored values.
38
+ - A rejected promise returned by an `onSubmit()` handler is reported with `console.error('[forms] submit handler failed', reason)` instead of being swallowed. `isSubmitting` still settles.
39
+ - The standalone `dist/browser/global.global.js` is minified (about 188 KB instead of 343 KB before compression); module builds are unchanged.
40
+ - State reads index the form's controls once per snapshot instead of once per field name.
41
+ - Raised the optional `@samline/formatter` peer and development dependency to `^2.1.0` (released 2026-11-06). The standalone script now bundles it, including its fixes for `regex.numeral`, `regex.password`, case-insensitive phone countries and decorated `general` masks.
42
+ - Documentation moved to the website at <https://samline.github.io/forms/docs/>. The npm tarball now ships `dist`, `README.md`, `LICENSE.md` and `CHANGELOG.md`; the Markdown `docs/` folder is no longer published. Old Starlight URLs redirect to their new pages.
43
+
44
+ ### Added
45
+
46
+ - A landing page, twelve live examples with copyable HTML / CDN and Package / ESM files, framework guides and an AI agent skill at `skills/forms/SKILL.md`.
47
+
48
+ ## [2.7.2] - 2026-09-23
49
+
50
+ ### Changed
51
+
52
+ - Raised the optional `@samline/formatter` peer and development dependency minimum to `^2.0.3`, ensuring consumers receive the latest formatter fixes for empty date/time delimiters, suffix stripping, and parametric password validation.
53
+ - Updated the npm-bundled and Starlight documentation examples to reference `@samline/forms@2.7.2`.
54
+
55
+ ## [2.7.1] - 2026-09-23
56
+
57
+ ### Fixed
58
+
59
+ - Interpreted user input and rebind values as display-order data while retaining automatic detection for canonical mirrors and initial server values.
60
+
61
+ ### Changed
62
+
63
+ - Synchronized the internal and Starlight formatter-integration documentation.
64
+ - Removed duplicated implementation narratives and obsolete comments without changing the public API.
65
+
66
+ ## [2.7.0] - 2026-09-22
67
+
68
+ ### Added
69
+
70
+ - Added explicit `dependsOn` metadata for reactive custom cross-field validation with transitive, cycle-safe dependency traversal.
71
+ - Added per-member `each` validation with element-specific `css-error` and `aria-invalid` state for repeated controls while preserving the public `FormErrors` string-array shape.
72
+ - Added strict decimal `numeric`, inclusive `min`, and inclusive `max` validation rules.
73
+ - Added promise-aware submit tracking through `FormStateSnapshot.isSubmitting`, including concurrent and rejected handler promises.
74
+ - Added `addCleanup()` for controller-owned integration teardown.
75
+
76
+ ### Fixed
77
+
78
+ - Synchronized initial and reset `css-filled` state independently from `autoValidate`, without activating validation.
79
+ - Resolved per-input visual validation state for repeated `name="foo[]"` controls ([#1](https://github.com/samline/forms/issues/1)).
80
+
81
+ ### Changed
82
+
83
+ - Updated GitHub Actions, Astro, Starlight, and documentation tooling to their reviewed supported releases, including the Node and Astro compatibility floors required by those updates.
84
+ - Synchronized the npm-bundled and Starlight documentation with every new public type, rule, method, and state transition.
85
+
86
+ ## [2.6.0] - 2026-09-21
87
+
88
+ ### Added
89
+
90
+ - Added the declarative `sameAs` validation rule for password confirmations and other fields that must contain equal non-empty values, with custom messages through `RuleConfig<string>`.
91
+ - Added automatic dependent-field revalidation for `sameAs`, including transitive dependencies, cycle-safe deduplication, `setValue()` updates, and activation after manual validation.
92
+ - Added regression coverage for matching strings, ordered arrays, empty values, source-field changes, reciprocal cycles, and transitive dependencies.
93
+
94
+ ### Changed
95
+
96
+ - Expanded the npm-bundled and Starlight documentation with `sameAs` recipes, exact equality semantics, empty-value behavior, file identity rules, cycle guarantees, and guidance to place mismatch errors on confirmation fields only.
97
+
98
+ ## [2.5.1] - 2026-09-20
99
+
100
+ ### Fixed
101
+
102
+ - Corrected documentation for exact validator names, radio and collection values, subscriber notification points, destroyed-controller behavior, shared browser registries, formatter mirrors, and optional-peer behavior.
103
+
104
+ ### Added
105
+
106
+ - Added focused Starlight guides for accessible validation and input formatting, plus references for native control behavior and package entrypoints.
107
+
108
+ ### Changed
109
+
110
+ - Reorganized the documentation sidebar, expanded the API lifecycle contracts, improved accessible examples, and synchronized the npm-bundled and hosted documentation surfaces.
111
+
112
+ ## [2.5.0] - 2026-09-19
113
+
114
+ ### Fixed
115
+
116
+ - Corrected the browser IIFE so `Forms.form`, `Forms.newForm`, `Forms.destroyForm`, and `Forms.available` are exposed directly instead of under `Forms.default`.
117
+ - Fixed duplicate field processing, formatted-field reset state, submitter serialization, duplicate browser controllers, stateful regular expressions, external associated controls, multiple selects, reserved field names, escaped class names, pending auto-submit cancellation, and async formatter teardown.
118
+ - Added `aria-invalid` synchronization and focus management for invalid submissions.
119
+
120
+ ### Added
121
+
122
+ - Added ESM/CJS browser entrypoints, package artifact smoke tests, CJS/ESM type-consumer tests, coverage thresholds, pull-request CI, dependency auditing, and Dependabot configuration.
123
+ - Added regression coverage for the corrected DOM, validation, serialization, formatting, and registry behavior.
124
+
125
+ ### Changed
126
+
127
+ - Added `docs/` to the npm package, corrected conditional type exports, exported `FormDataPrimitive` and `FormFieldWatcher`, and made multi-field formatter configuration reject a shared `displayField` without mutating the form.
128
+ - Updated root and documentation-site dependencies to audited versions and synchronized both documentation surfaces.
129
+
130
+ ## [2.4.0] - 2026-07-19
131
+
132
+ ### Fixed
133
+
134
+ - `applyFormat` phase 2 and the capture-phase input handler now override `interpretInputAs` to `'auto'` for the initial value pass and for input events whose source is the hidden raw mirror, so a server pre-fill of the canonical raw (e.g. a Blade re-render with `value="{{ old('birthday') }}"` carrying `"19901212"` for a `d/m/Y` display + `Ymd` raw combo) is correctly converted to the display form on mount. Without the override, `@samline/formatter` v1.2.0+ — which changed `interpretInputAs` to default to `'display'` — mis-segmented the raw digits and the visible ended up showing the scrambled value (e.g. `"19/09/1212"`) instead of `"12/12/1990"`. As of `@samline/formatter@2.0.0`, the default is `'auto'` (which handles the same case via the formatter's own heuristic), so the override is now a defensive explicit declaration of intent rather than strictly required. It still serves two purposes: (1) it documents the controller's context awareness in code, and (2) it protects against a future formatter change that flips the default again. Live keystrokes on the visible input keep the formatter's default because what the user types is in display order. An explicit `interpretInputAs` in `config.options` is always respected, never overridden — pass `'display'` if you pre-render the visible in display order from the server, or `'raw'` if you want the legacy pre-1.2.0 round-trip semantics for the initial pass.
135
+
136
+ ### Added
137
+
138
+ - Five regression tests under `test/api/format.test.ts`: the easytrip / Blade `old()` case (raw initial value → display form on mount), the live keystroke path (no `interpretInputAs` override applied to user typing), `setValue('field', raw)` writes the canonical raw with the `'auto'` interpretation forced, the explicit `interpretInputAs: 'display'` opt-in is preserved, and the re-bind path forwards the caller's options verbatim.
139
+
140
+ ### Changed
141
+
142
+ - `docs/api/format.md` adds an "Initial value interpretation (server pre-fill)" section that documents the override and the four contexts (initial pass, mirror-source events, live keystrokes, re-bind) with the explicit opt-in caveat. With the formatter v2.0.0 default flip, the prose now notes that the override is defensive rather than strictly required.
143
+ - `example/src/content/docs/reference/api.md` (Starlight mirror) gains a "Server pre-fill" note in the `format()` summary so the npm-bundled docs and the live site stay in sync.
144
+ - `example/src/content/docs/reference/typescript.md` notes the `interpretInputAs` override in the `FieldFormatConfig` prose so consumers reading the type signature know the initial pass is the special case.
145
+ - The peer dependency `@samline/formatter` is now `^2.0.0` (was `^1.0.1`). The `^1.x` range no longer receives the v2.0.0 fix, so consumers on the previous major must pin explicitly or upgrade.
146
+
147
+ ## [2.3.4] - 2026-07-16
148
+
149
+ ### Fixed
150
+
151
+ - `readFieldValue` and `writeFieldValue` now respect the `name="foo[]"` convention for multi-value inputs. Previously, `getValue` returned only the first matching input's value and `setValue` wrote the same string to every matching input, breaking custom validators that check `Array.isArray(value)` on fields like `docusign_email[]`. The `[]` suffix is the de facto HTML pattern for array inputs (PHP, Rails, Express, Spring); the controller now treats it as the explicit signal for "collect all values as an array".
152
+
153
+ ### Added
154
+
155
+ - Eleven regression tests under `test/api/array-syntax.test.ts` covering `getValue` / `setValue` with `name="foo[]"` for text, email, checkbox, and file inputs (the last via a hand-built `FileList`), plus guards that `name="foo"` (no brackets) keeps the original first-value / broadcast semantics and that custom validators keyed on the bracketed name now receive the full array.
156
+
157
+ ### Changed
158
+
159
+ - `docs/api/get-value.md` and `docs/api/set-value.md` document the `name="foo[]"` behaviour in the per-field-type tables, with a new example for each direction and a dedicated edge-case bullet describing the explicit opt-in. `example/src/content/docs/reference/api.md` mirrors the note in the Starlight reference so the npm-bundled docs and the live site stay in sync.
160
+
161
+ ## [2.3.3] - 2026-07-16
162
+
163
+ ### Fixed
164
+
165
+ - `syncVisualState` was routing `css-filled` and `css-error` to the **visible** input only on the user-typing path (`handleDelegatedEvent`). Every other path — `validate()`, `revalidate()`, `setErrors()`, `clearErrors()` with a name list, the initial mount, and the `MutationObserver`-driven re-sync — still wrote the attribute to the hidden raw mirror. For formatted fields the project label never matched `:has([css-error])` and the error indicator was invisible. The fix centralises the canonical→display name resolution inside `syncVisualState` itself, so all five call sites are fixed at once and the visual attribute contract documented in `api/format.ts` is honoured by every public entrypoint.
166
+
167
+ ### Added
168
+
169
+ - Seven regression tests under `test/api/format.test.ts`: `validate()` (no-arg and with an explicit name list), `revalidate()`, `setErrors()` (array and map form), `clearErrors()` with a name list, and the initial-mount path with a pre-populated hidden mirror.
170
+
171
+ ### Changed
172
+
173
+ - `docs/css-styling.md` adds a callout after the `:has()` recipe explaining that the hidden mirror created by `format()` lives outside any label and that the controller always targets the visible.
174
+ - `docs/recipes.md` adds a one-liner to the mirror-convention section cross-referencing the styling page.
175
+
176
+ ## [2.3.2] - 2026-07-16
177
+
178
+ ### Fixed
179
+
180
+ - `css-filled` and `css-error` were being written to the hidden raw mirror for fields wired through `format()`, so the `:has([css-filled])` selector on the project label parent never matched and the label text stayed invisible. `handleDelegatedEvent` now resolves the event target to the display name and calls `syncVisualState([displayName])` so visual attributes land on the element the user sees. The data path (validation, errors, raw mirror) is unchanged.
181
+
182
+ ### Added
183
+
184
+ - Three regression tests for the user-typing path: `css-filled` on the visible after a keystroke, `css-filled` on the visible when `setValue` writes the canonical, and a guard for non-formatted fields.
185
+
186
+ ## [2.3.1] - 2026-07-15
187
+
188
+ ### Fixed
189
+
190
+ - When `format()` was called multiple times in the same tick (e.g. `format(phone)` + `format(date)` + `format(general)` at form init), the first `applyFormat` to resume from `await loadFormatter()` would bind every entry in the bucket with the first call's config; the subsequent calls short-circuited on the "already bound" check. The easytrip registration form hit this: phone formatter ran for all three fields, so date and tag_number output was phone-style instead of date / general. Phase 2 of `applyFormat` is now scoped to the entries the current call touched (via a local `phase1Entries` array) instead of iterating the whole bucket.
191
+
192
+ ### Added
193
+
194
+ - Regression test covers the three-sequential-`format()` case end-to-end.
195
+
196
+ ## [2.3.0] - 2026-07-15
197
+
198
+ ### Changed
199
+
200
+ - **`format()` mirror convention inverted.** The canonical name (`<field>`) now carries the raw value on a hidden input appended to the form; the visible input is renamed to `<field>_displayed` (or whatever `displayField` resolves to). Both names are first-class in the controller's API: `getValue('phone')` returns the raw, `getValue('phone_displayed')` returns the formatted, `watch('phone', cb)` fires with the raw, etc. `rawField` config renamed to `displayField`.
201
+ - `format()` runs in two phases: phase 1 (sync) renames the visible and creates/reuses the hidden mirror; phase 2 (async) loads the formatter peer and binds the input listener. The sync phase rolls back on a missing peer so the DOM is left exactly as the developer authored it.
202
+ - `state.listeners` tracks `capture?` to match `addEventListener` (`exactOptionalPropertyTypes: true`).
203
+ - `destroy()` restores the visible's name to the canonical form and detaches every capture-phase listener added by `format()`.
204
+
205
+ ### Notes
206
+
207
+ - No peer changes — options are still forwarded to `@samline/formatter` as before, so `datePattern`, `dateRawPattern`, `rawPrefix`, `rawSuffix`, `prefixMode`, `suffixMode`, etc. all keep working identically.
208
+
209
+ ## [2.2.3] - 2026-07-14
210
+
211
+ ### Fixed
212
+
213
+ - Cursor jumped past digits when backspacing adjacent to a delimiter. `applyFormattedValue` ignored `InputEvent.inputType` and `computeCursorPosition` counted non-format characters to the left of the caret without ever pulling the cursor back past a re-inserted delimiter. After a `deleteContentBackward` / `deleteWordBackward` the caret could land with a format character immediately on its left, so the next backspace removed the delimiter instead of the digit the user expected to delete.
214
+ - `applyFormattedValue` now threads `InputEvent.inputType` from `buildHandler` and applies cleave.js' `postDelimiterBackspace` scan in **both** branches of the function (early-return and full `computeCursorPosition`), so the contract holds whether or not the formatter rewrites the visible value.
215
+
216
+ ### Added
217
+
218
+ - Four vitest cases under `test/api/format.test.ts` covering the `postDelimiterBackspace` contract for `deleteContentBackward` adjacent to a delimiter, deletion of a delimiter itself, `deleteWordBackward` in the middle of a formatted value, and a DD/MM/YYYY date field with the caret parked after the first slash.
219
+
220
+ ## [2.2.2] - 2026-07-10
221
+
222
+ ### Fixed
223
+
224
+ - `@samline/formatter` peer was being silently left as a bare specifier in the IIFE bundle, so `format()` / `formatAll()` did not run in Vite / Rollup production builds. The dynamic import carried a `/* @vite-ignore */` hint, which told Rollup to leave the literal `'@samline/formatter'` in the bundle.
225
+ - The IIFE build now bundles the peer; ESM and CJS still externalize it. Consumers that need to ship the formatter separately can do so via a bundler-level externals configuration.
226
+
227
+ ## [2.2.1] - 2026-06-30
228
+
229
+ ### Changed
230
+
231
+ - Documentation references aligned to 2.2.1 (CDN URLs in `README.md` and `docs/browser.md`).
232
+
233
+ ## [2.2.0] - 2026-06-30
234
+
235
+ ### Added
236
+
237
+ - `browser` singleton export for bundler usage — the same `{ form, newForm, destroyForm, available }` surface as the IIFE, but as a module-level singleton, so bundler consumers can use the registry helpers without opting into the IIFE `globalThis` side-effect.
238
+
239
+ ### Fixed
240
+
241
+ - `format()` integration tests: raw mirror handling on `setValue` writing the display name (the visible's `.value` was being clobbered by the canonical write path).
242
+
243
+ ## [2.1.0] - 2026-06-30
244
+
245
+ > Not tagged at the time. The version was bumped and published; this entry is reconstructed from the commit log between v1.0.3 and v2.2.0.
246
+
247
+ ### Added
248
+
249
+ - `@samline/formatter` peer dependency. The controller reads the peer at runtime via a dynamic import; a missing peer is non-fatal — a single `console.error` is logged and `format()` becomes a no-op so the rest of the form keeps working.
250
+ - `format()` / `formatAll()` / `format` options API: `type`, `field` (string or `string[]`), `options` forwarded to the peer, `displayField` for the visible's name.
251
+ - `onSubmit`, `watch`, `observe`, `unwatch`, `subscribe`, `prefill`, `append`, `setErrors`, `clearErrors`, `setValue`, `getValue`, `getField`, `getData`, `getState`, `reset`, `autoSubmit`, `disableAutoSubmit`, `validate`, `revalidate`, `destroy` — the per-method api/ factories.
252
+
253
+ ### Changed
254
+
255
+ - **Package focus narrowed to vanilla JS and direct browser usage.** Framework-specific variants and docs were removed (`refactor: remove framework-specific variants and docs, simplify package structure`). The README was rewritten to reflect the new focus.
256
+ - Browser global renamed to `Forms` and a registry helper added: `Forms.newForm({ id, options })` stores the controller under `Forms.available[id]`, `Forms.destroyForm(id)` calls `destroy()` and removes the entry.
257
+ - `watch()` and `observe()` callbacks now receive the field element(s) as an additional argument, so consumers can read attributes (`css-filled`, `css-error`, `aria-*`, …) without doing their own DOM lookup.
258
+ - "API at a glance" section added to the README and browser usage clarified.
259
+ - Installation instructions updated to use `bun`.
260
+ - Deployment branch for the GitHub Pages docs site changed to `v2`.
261
+ - Accessibility skill and patterns documented.
262
+
263
+ ## [1.0.3] - 2026-05-31
264
+
265
+ ### Fixed
266
+
267
+ - Manual errors were not cleared when the affected field changed. The controller now deletes the entry from `state.manualErrors[canonical]` on the next `input` / `change` event of the affected field, unless `clearManualErrorsOnChange: false` is passed via `FormControllerOptions`.
268
+
269
+ ## [1.0.2] - 2026-03-24
270
+
271
+ ### Fixed
272
+
273
+ - `npm publish` workflow was failing in CI; auth check fixed.
274
+
275
+ ## [1.0.1] - 2026-03-24
276
+
277
+ ### Fixed
278
+
279
+ - First stable patch on the 1.x line (release plumbing).
280
+
281
+ ## [1.0.0] - 2026-03-24
282
+
283
+ ### Fixed
284
+
285
+ - `npm publish` workflow: npm auth check on the publish job.
286
+
287
+ ## [0.1.0] - 2026-03-24
288
+
289
+ ### Added
290
+
291
+ - Initial release of `@samline/forms`. Vanilla JS form controller with framework-specific variants (React, Vue, Svelte) initially included; bundled `.local/` agent docs at the repo root.
292
+
293
+ [Unreleased]: https://github.com/samline/forms/compare/v2.9.0...HEAD
294
+ [2.9.0]: https://github.com/samline/forms/compare/v2.8.0...v2.9.0
295
+ [2.8.0]: https://github.com/samline/forms/compare/v2.7.2...v2.8.0
296
+ [2.7.2]: https://github.com/samline/forms/compare/v2.7.1...v2.7.2
297
+ [2.7.1]: https://github.com/samline/forms/compare/v2.7.0...v2.7.1
298
+ [2.7.0]: https://github.com/samline/forms/compare/v2.6.0...v2.7.0
299
+ [2.6.0]: https://github.com/samline/forms/compare/v2.5.1...v2.6.0
300
+ [2.5.1]: https://github.com/samline/forms/compare/v2.5.0...v2.5.1
301
+ [2.5.0]: https://github.com/samline/forms/compare/v2.4.0...v2.5.0
302
+ [2.4.0]: https://github.com/samline/forms/compare/v2.3.4...v2.4.0
303
+ [2.3.4]: https://github.com/samline/forms/compare/v2.3.3...v2.3.4
304
+ [2.3.3]: https://github.com/samline/forms/compare/v2.3.2...v2.3.3
305
+ [2.3.2]: https://github.com/samline/forms/compare/v2.3.1...v2.3.2
306
+ [2.3.1]: https://github.com/samline/forms/compare/v2.3.0...v2.3.1
307
+ [2.3.0]: https://github.com/samline/forms/compare/v2.2.3...v2.3.0
308
+ [2.2.3]: https://github.com/samline/forms/compare/v2.2.2...v2.2.3
309
+ [2.2.2]: https://github.com/samline/forms/compare/v2.2.1...v2.2.2
310
+ [2.2.1]: https://github.com/samline/forms/compare/v2.2.0...v2.2.1
311
+ [2.2.0]: https://github.com/samline/forms/compare/v1.0.3...v2.2.0
312
+ [1.0.3]: https://github.com/samline/forms/compare/v1.0.2...v1.0.3
313
+ [1.0.2]: https://github.com/samline/forms/compare/v1.0.1...v1.0.2
314
+ [1.0.1]: https://github.com/samline/forms/compare/v1.0.0...v1.0.1
315
+ [1.0.0]: https://github.com/samline/forms/compare/v0.1.0...v1.0.0
316
+ [0.1.0]: https://github.com/samline/forms/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,183 +1,183 @@
1
- # Forms
1
+ # @samline/forms
2
2
 
3
- > A TypeScript-first, framework-free controller for native HTML forms.
3
+ Validation, reactive state, serialization and auto-submit for native HTML forms, with a typed, framework-free API for JavaScript and TypeScript.
4
4
 
5
- > Add validation, reactive field state, accessible error handling, serialization, auto-submit, and optional input formatting without adopting a UI framework.
5
+ [Demo](https://samline.github.io/forms/) · [Examples](https://samline.github.io/forms/examples/) · [Documentation](https://samline.github.io/forms/docs/) · [Changelog](CHANGELOG.md)
6
6
 
7
- ---
8
-
9
- ## Table of Contents
7
+ ## Table of contents
10
8
 
11
9
  - [Installation](#installation)
12
- - [CDN / Browser](#cdn--browser)
13
- - [Entrypoints](#entrypoints)
14
- - [Quick Start](#quick-start)
15
- - [What You Can Build](#what-you-can-build)
16
- - [API at a Glance](#api-at-a-glance)
10
+ - [Quick start](#quick-start)
11
+ - [HTML / CDN](#html--cdn)
12
+ - [Capabilities](#capabilities)
13
+ - [API at a glance](#api-at-a-glance)
17
14
  - [Documentation](#documentation)
15
+ - [Changelog](#changelog)
16
+ - [AI agent skill](#ai-agent-skill)
18
17
  - [License](#license)
19
18
 
20
- ---
21
-
22
19
  ## Installation
23
20
 
24
- ```bash
21
+ ```sh
25
22
  npm install @samline/forms
26
23
  ```
27
24
 
28
- ```bash
25
+ ```sh
29
26
  pnpm add @samline/forms
30
27
  ```
31
28
 
32
- ```bash
29
+ ```sh
33
30
  bun add @samline/forms
34
31
  ```
35
32
 
36
- Requires Node 20+ when bundling. Runtime target is ES2020.
37
-
38
- ---
33
+ Requires Node 20+ when bundling; the runtime targets ES2020 browsers. Formatting needs the optional peer `@samline/formatter` (`npm install @samline/formatter`); validation, state and serialization do not. No runtime stylesheet is required; the application owns form styling.
39
34
 
40
- ## CDN / Browser
35
+ ## Quick start
41
36
 
42
- Use the browser build when you do not have a bundler and need to run the package directly in HTML, Shopify, WordPress, or any traditional template.
37
+ Keep a native form with named fields. `novalidate` lets the controller's rules replace the browser's bubbles.
43
38
 
44
39
  ```html
45
- <script src="https://unpkg.com/@samline/forms@2.7.2/dist/browser/global.global.js"></script>
40
+ <form id="signup" novalidate>
41
+ <label for="email">Email</label>
42
+ <input id="email" name="email" type="email" autocomplete="email" aria-describedby="email-error" />
43
+ <p id="email-error" data-error-for="email" aria-live="polite"></p>
44
+ <button type="submit">Sign up</button>
45
+ </form>
46
46
  ```
47
47
 
48
- > Pin the version in production. Replace `2.7.2` with the version you ship.
49
-
50
- The browser bundle exposes a single global: `window.Forms`.
48
+ ```js
49
+ import { form } from '@samline/forms'
51
50
 
52
- ```html
53
- <form id="contact-form">
54
- <input name="email" type="email" />
55
- <button type="submit">Send</button>
56
- </form>
51
+ const signup = form('signup', {
52
+ autoValidate: false,
53
+ validators: {
54
+ email: {
55
+ required: { value: true, message: 'Enter your email.' },
56
+ pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Use a valid email.' },
57
+ },
58
+ },
59
+ })
57
60
 
58
- <script src="https://unpkg.com/@samline/forms@2.7.2/dist/browser/global.global.js"></script>
59
- <script>
60
- const contactForm = window.Forms.newForm({ id: 'contact-form' })
61
+ signup.subscribe(state => {
62
+ document.querySelector('#email-error').textContent = state.errors.email?.[0] ?? ''
63
+ })
61
64
 
62
- contactForm.onSubmit(async (form, data, formData) => {
63
- await fetch('/api/contact', { method: 'POST', body: formData })
64
- })
65
+ signup.onSubmit(async (_element, _data, formData) => {
66
+ await fetch('/api/signup', { method: 'POST', body: formData })
67
+ signup.reset()
68
+ })
65
69
 
66
- contactForm.validate()
67
- </script>
70
+ // When the form goes away (route change, component unmount):
71
+ // signup.destroy()
68
72
  ```
69
73
 
70
- The browser surface keeps a small registry under `Forms.available`, keyed by the `id` you pass to `Forms.newForm`. Each successful `newForm` call stores the returned controller there, and `Forms.destroyForm(id)` calls `destroy()` and removes the entry. Use `Forms.form` directly when you need the factory without the registry side-effect.
71
-
72
- See [docs/browser.md](docs/browser.md) for the full browser surface.
74
+ Invalid submits are always prevented and focus moves to the first invalid field. Valid submits reach every `onSubmit()` handler with a plain object, a fresh `FormData` and a state snapshot.
73
75
 
74
- ---
76
+ ## HTML / CDN
75
77
 
76
- ## Entrypoints
78
+ The standalone build defines `window.Forms` and already includes `@samline/formatter`. Pin the version.
77
79
 
78
- | Entrypoint | When to use |
79
- | --- | --- |
80
- | `@samline/forms` | Main vanilla API for bundlers, ESM, or CJS consumers. |
81
- | `@samline/forms/browser` | ESM/CJS browser module with registry exports, bundled types, and `window.Forms` registration. |
82
- | `@samline/forms/browser/global` | Pre-bundled IIFE for direct `<script>` usage; the CDN URL resolves to `dist/browser/global.global.js`. |
80
+ ```html
81
+ <script src="https://unpkg.com/@samline/forms@2.9.0/dist/browser/global.global.js"></script>
82
+ <script>
83
+ const signup = window.Forms.form('signup', {
84
+ validators: { email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ } },
85
+ })
86
+ </script>
87
+ ```
83
88
 
84
- The vanilla entrypoint also exports `browser`, the same `{ form, newForm, destroyForm, available }` surface as the IIFE but as a module-level singleton (no `globalThis` side-effect). Use it from a bundler when you want the registry helpers without the IIFE — see [docs/browser.md → Using the same shape from a bundler](docs/browser.md#using-the-same-shape-from-a-bundler).
89
+ `window.Forms` has `form`, `newForm({ id, options })`, `destroyForm(id)` and the `available` registry. See [Browser global and CDN](https://samline.github.io/forms/docs/reference/browser/).
85
90
 
86
- ---
91
+ ## Capabilities
87
92
 
88
- ## Quick Start
93
+ - **Validation rules:** `required`, `minLength`, `maxLength`, `pattern`, `numeric`, `min`, `max`, `sameAs`, `dependsOn`, per-row `each` and custom validators that read every value.
94
+ - **Accessible state:** invalid controls get `aria-invalid` and `css-error`, filled ones `css-filled`; a failed submit focuses the first invalid control in document order.
95
+ - **Reactive state:** `subscribe()` for the whole form, `observe()`/`watch()` for one field.
96
+ - **Submission:** promise-aware `onSubmit()`, `autoSubmit()` with debounce, `setErrors()` for server responses, `prefill()` from the URL and `reset()`.
97
+ - **Native controls:** checkbox groups, radios, multiple selects, files, repeated `name[]` inputs and controls linked with `form="id"`, including fields added later.
98
+ - **Formatting:** `format()`, `formatAll()` and the `formats` option format phones, cards, dates, times and amounts while a hidden field submits the raw value.
99
+ - **DOM-free helpers:** `validateValues()` and `validateFieldValue()` run the same rules on the server; `parseFormData()` serializes any form.
89
100
 
90
- ```ts
91
- import { form } from '@samline/forms'
101
+ ### Lifecycle
92
102
 
93
- const contactForm = form('contact-form', {
94
- validators: {
95
- email: {
96
- required: true,
97
- pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
98
- }
99
- }
100
- })
103
+ One controller owns one form's listeners, `MutationObserver` and auto-submit timer. Create it once the form exists and call `destroy()` when it goes away. `addCleanup()` ties your own teardown to the same lifetime. After `destroy()`, writes and new registrations are no-ops.
101
104
 
102
- contactForm.watch('email', value => {
103
- console.log('email is now:', value)
104
- })
105
-
106
- contactForm.onSubmit(async (_element, _data, formData) => {
107
- await fetch('/api/contact', { method: 'POST', body: formData })
108
- })
109
- ```
105
+ ## API at a glance
110
106
 
111
- What this does:
107
+ | API | Purpose |
108
+ | ------------------------------------------------- | ------------------------------------------------------------------ |
109
+ | `form(target, options?)` | Create a controller for an id, `HTMLFormElement` or `{ current }`. |
110
+ | `createFormController(target, options?)` | The same factory under its descriptive name. |
111
+ | `browser` | `{ form, newForm, destroyForm, available }` registry singleton. |
112
+ | `parseFormData(form, submitter?)` | Serialize a form into `{ data, formData }`. |
113
+ | `validateValues(values, schema)` | Validate a plain values object against a `ValidationSchema`. |
114
+ | `validateFieldValue(field, value, rules, values)` | Validate one value. |
115
+ | `@samline/forms/browser` | The registry as a module namespace; also sets `globalThis.Forms`. |
116
+ | `@samline/forms/browser/global` | The standalone IIFE that defines `window.Forms`. |
112
117
 
113
- - Binds to the form with id `contact-form`.
114
- - Adds `css-filled` / `css-error` attributes on fields so you can style them with CSS.
115
- - Validates `email` on every change and on submit.
116
- - Intercepts valid submits (the default for `onSubmit`) and hands a real `FormData` instance to your handler.
118
+ ### Entrypoints
117
119
 
118
- ---
120
+ | Entrypoint | When to use |
121
+ | ------------------------------- | ---------------------------------------------------------------------------------------- |
122
+ | `@samline/forms` | Controller factory, pure validation/serialization helpers and types for ESM or CommonJS. |
123
+ | `@samline/forms/browser` | Module registry that also installs `globalThis.Forms`. |
124
+ | `@samline/forms/browser/global` | Standalone script with `window.Forms` and the bundled formatter. |
119
125
 
120
- ## What You Can Build
126
+ ### Controller methods
121
127
 
122
- - Contact, newsletter, login, signup, checkout, and profile forms.
123
- - Forms that submit with `fetch` while keeping native `FormData` payloads.
124
- - Autosave / autosubmit flows with optional debounce.
125
- - Visual feedback driven by `css-filled` and `css-error` attributes.
126
- - Progressive enhancement on top of any existing HTML form.
127
- - Forms rendered server-side (Blade, Twig, ERB) that still want client-side validation.
128
+ | Group | Methods |
129
+ | ---------- | ------------------------------------------------------------------------------- |
130
+ | Submission | `onSubmit`, `autoSubmit`, `disableAutoSubmit` |
131
+ | Validation | `validate`, `revalidate`, `setErrors`, `clearErrors` |
132
+ | State | `subscribe`, `observe`, `watch`, `unwatch`, `getState` |
133
+ | Values | `setValue`, `setOptions`, `getValue`, `getField`, `getData`, `prefill`, `reset` |
134
+ | Formatting | `format`, `formatAll` |
135
+ | Lifecycle | `element`, `f`, `options`, `append`, `addCleanup`, `destroy` |
128
136
 
129
- ---
137
+ ### Options
130
138
 
131
- ## API at a Glance
139
+ `validators`, `autoValidate` (default `true`), `autoSubmit`, `clearErrorsOnSubmit` (default `true`), `clearManualErrorsOnChange` (default `true`), `attributes` (default `{ filled: 'css-filled', error: 'css-error' }`) and `formats`. Every option is described in [Configuration options](https://samline.github.io/forms/docs/reference/configuration/); every type is exported, see the [TypeScript reference](https://samline.github.io/forms/docs/reference/typescript/).
132
140
 
133
- The controller is built around one factory and a small set of focused methods. Most methods are chainable.
141
+ ## Documentation
134
142
 
135
- | Group | Methods |
136
- | --- | --- |
137
- | Lifecycle | [`form`](docs/api/form.md) · [`addCleanup`](docs/api/add-cleanup.md) · [`destroy`](docs/api/destroy.md) · [`reset`](docs/api/reset.md) |
138
- | Registry (vanilla) | [`browser`](docs/getting-started.md#browser-registry-helpers) — bundler-friendly `{ form, newForm, destroyForm, available }` singleton. |
139
- | Properties | [`element`](docs/api/element.md) · [`options`](docs/options.md) |
140
- | Submission | [`onSubmit`](docs/api/on-submit.md) · [`autoSubmit`](docs/api/auto-submit.md) · [`disableAutoSubmit`](docs/api/disable-auto-submit.md) |
141
- | Field observation | [`watch`](docs/api/watch.md) · [`observe`](docs/api/observe.md) · [`unwatch`](docs/api/unwatch.md) · [`subscribe`](docs/api/subscribe.md) |
142
- | Field values | [`setValue`](docs/api/set-value.md) · [`getValue`](docs/api/get-value.md) · [`getField`](docs/api/get-field.md) · [`prefill`](docs/api/prefill.md) · [`format`](docs/api/format.md) · [`formatAll`](docs/api/format.md) |
143
- | Validation | [`validate`](docs/api/validate.md) · [`revalidate`](docs/api/revalidate.md) · [`setErrors`](docs/api/set-errors.md) · [`clearErrors`](docs/api/clear-errors.md) |
144
- | State and data | [`getData`](docs/api/get-data.md) · [`getState`](docs/api/get-state.md) · [`append`](docs/api/append.md) |
145
- | Pure helpers | [`parseFormData`](docs/api/parse-form-data.md) · [`validateValues`](docs/api/validate-values.md) · [`validateFieldValue`](docs/api/validate-field-value.md) |
143
+ Read the [documentation](https://samline.github.io/forms/docs/) for the full guides and API contracts, and explore [working examples](https://samline.github.io/forms/examples/) with complete and separate source files for both loading modes.
146
144
 
147
- Validation supports explicit cross-field dependencies with `dependsOn`, per-member collection rules with `each`, and strict decimal `numeric` / inclusive `min` / `max` rules. Async `onSubmit` handlers are reflected by `getState().isSubmitting`, while `addCleanup()` lets integrations register controller-owned teardown work.
145
+ | Guide | What it covers |
146
+ | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
147
+ | [Get started](https://samline.github.io/forms/docs/getting-started/) | Installation, the first form, CDN use and cleanup. |
148
+ | [Examples](https://samline.github.io/forms/docs/guides/examples/) | Twelve live forms with their code. |
149
+ | [Validation and errors](https://samline.github.io/forms/docs/guides/validation/) | Every rule, cross-field checks and accessible messages. |
150
+ | [State and submission](https://samline.github.io/forms/docs/guides/state-and-submission/) | Subscribers, async submits, auto-submit, prefill and reset. |
151
+ | [Formatting fields](https://samline.github.io/forms/docs/guides/formatting/) | The optional formatter and the hidden raw mirror. |
152
+ | [Framework integrations](https://samline.github.io/forms/docs/guides/frameworks/) | React, Vue, Svelte, Astro, Shopify and WordPress. |
153
+ | [API reference](https://samline.github.io/forms/docs/reference/api/) | Every method, grouped by task, with one page per method. |
148
154
 
149
- See the full per-method reference in [`docs/api/`](docs/api/index.md).
155
+ ## Changelog
150
156
 
151
- ---
157
+ Read [CHANGELOG.md](CHANGELOG.md) or the [published changelog](https://samline.github.io/forms/docs/changelog/) for release history and explicitly marked breaking and behavior changes. Review the notes before upgrading.
152
158
 
153
- ## Optional peer: `@samline/formatter`
159
+ ### Upgrading to 2.8
154
160
 
155
- [`format`](docs/api/format.md) and [`formatAll`](docs/api/format.md) rely on the optional peer dependency [`@samline/formatter`](https://github.com/samline/formatter). Install it when you need input masks (phone, credit-card, date, time, numeral, general):
161
+ 2.8.0 removes no API. Review these behavior fixes before upgrading: `prefill()` writes repeated query keys as arrays and auto-submits once; native reset buttons now clear errors and attributes; `reset()` calls watchers; failed submits focus the first invalid control in document order and automatic submits never move focus; methods called after `destroy()` no longer change the form; rejected async submit handlers are logged. The Markdown `docs/` folder is no longer published to npm; the documentation lives on the website. The optional `@samline/formatter` peer now requires `^2.1.0`, which the standalone script bundles. Read the [2.8.0 notes](CHANGELOG.md).
156
162
 
157
- ```bash
158
- npm install @samline/formatter
159
- ```
163
+ ## AI agent skill
160
164
 
161
- If the peer is not installed, the methods log a single `console.error` explaining how to install it and return the controller unchanged — the rest of the form keeps working. See [docs/recipes.md → 13. Format inputs with `@samline/formatter`](docs/recipes.md#13-format-inputs-with-samlineformatter) for end-to-end examples.
165
+ <a id="agent-integration"></a>
162
166
 
163
- ---
167
+ The [Forms skill](https://samline.github.io/forms/skills/forms/SKILL.md) describes 2.8 integration: loading modes, validation rules and accessible messages, state and submission, formatting and host ownership. Pass it to an agent or install it using a skills-compatible tool. Verify the installed package version before applying its guidance.
164
168
 
165
- ## Documentation
169
+ The maintained [repository source](skills/forms/SKILL.md) and website download are the same file.
166
170
 
167
- Full API reference, guides, and examples are available at **[samline.github.io/forms](https://samline.github.io/forms)**.
171
+ Download [SKILL.md](https://samline.github.io/forms/skills/forms/SKILL.md) into a `forms/` folder in your agent's supported skill directory. For agents that discover project skills from `.agents/skills/`:
168
172
 
169
- | Doc | Purpose |
170
- | --- | --- |
171
- | [docs/getting-started.md](docs/getting-started.md) | Concepts, observable contract, lifecycle, and side-effect overview. |
172
- | [docs/options.md](docs/options.md) | Full `FormControllerOptions` reference. |
173
- | [docs/css-styling.md](docs/css-styling.md) | `css-filled` and `css-error` styling recipes. |
174
- | [docs/typescript.md](docs/typescript.md) | Every exported TypeScript type, with examples. |
175
- | [docs/api/index.md](docs/api/index.md) | One page per public method. |
176
- | [docs/recipes.md](docs/recipes.md) | End-to-end patterns: fetch submit, server errors, autosave, multi-step, etc. |
177
- | [docs/browser.md](docs/browser.md) | Browser global (`window.Forms`) usage. |
173
+ ```sh
174
+ mkdir -p .agents/skills/forms
175
+ curl --fail --location https://samline.github.io/forms/skills/forms/SKILL.md \
176
+ --output .agents/skills/forms/SKILL.md
177
+ ```
178
178
 
179
- ---
179
+ When working from this repository, copy `skills/forms/SKILL.md` instead. Keep the filename `SKILL.md`; discovery paths and invocation syntax depend on the agent.
180
180
 
181
181
  ## License
182
182
 
183
- MIT
183
+ MIT — [LICENSE.md](LICENSE.md).