@samline/forms 2.7.2 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +302 -0
  2. package/README.md +110 -122
  3. package/dist/browser/global.d.ts +1 -13
  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 +125 -56
  7. package/dist/browser/index.cjs.map +1 -1
  8. package/dist/browser/index.js +1 -1
  9. package/dist/{chunk-MMHX34WB.js → chunk-23VZAYA3.js} +126 -57
  10. package/dist/chunk-23VZAYA3.js.map +1 -0
  11. package/dist/index.cjs +125 -56
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.js +1 -1
  14. package/package.json +35 -7
  15. package/dist/chunk-MMHX34WB.js.map +0 -1
  16. package/docs/api/add-cleanup.md +0 -52
  17. package/docs/api/append.md +0 -96
  18. package/docs/api/auto-submit.md +0 -91
  19. package/docs/api/clear-errors.md +0 -69
  20. package/docs/api/destroy.md +0 -80
  21. package/docs/api/disable-auto-submit.md +0 -65
  22. package/docs/api/element.md +0 -78
  23. package/docs/api/form.md +0 -156
  24. package/docs/api/format.md +0 -270
  25. package/docs/api/get-data.md +0 -90
  26. package/docs/api/get-field.md +0 -87
  27. package/docs/api/get-state.md +0 -111
  28. package/docs/api/get-value.md +0 -114
  29. package/docs/api/index.md +0 -55
  30. package/docs/api/observe.md +0 -126
  31. package/docs/api/on-submit.md +0 -94
  32. package/docs/api/parse-form-data.md +0 -94
  33. package/docs/api/prefill.md +0 -89
  34. package/docs/api/regex.md +0 -155
  35. package/docs/api/reset.md +0 -61
  36. package/docs/api/revalidate.md +0 -52
  37. package/docs/api/set-errors.md +0 -97
  38. package/docs/api/set-value.md +0 -120
  39. package/docs/api/subscribe.md +0 -107
  40. package/docs/api/unwatch.md +0 -74
  41. package/docs/api/validate-field-value.md +0 -144
  42. package/docs/api/validate-values.md +0 -103
  43. package/docs/api/validate.md +0 -102
  44. package/docs/api/watch.md +0 -117
  45. package/docs/browser.md +0 -156
  46. package/docs/css-styling.md +0 -191
  47. package/docs/getting-started.md +0 -222
  48. package/docs/options.md +0 -326
  49. package/docs/recipes.md +0 -531
  50. package/docs/typescript.md +0 -615
  51. package/docs/vanilla.md +0 -12
  52. /package/{LICENSE → LICENSE.md} +0 -0
@@ -1,69 +0,0 @@
1
- # `clearErrors(fields?)`
2
-
3
- Removes manual errors. Validation errors from rules are not touched.
4
-
5
- ## Signature
6
-
7
- ```ts
8
- clearErrors(): FormController
9
- clearErrors(fields: string[]): FormController
10
- ```
11
-
12
- ## Parameters
13
-
14
- | Name | Type | Required | Description |
15
- | --- | --- | --- | --- |
16
- | `fields` | `string[]` | no | When provided, only the listed fields are cleared. When omitted, every manual error is cleared. |
17
-
18
- ## Returns
19
-
20
- The same [`FormController`](../typescript.md#formcontroller) — chainable.
21
-
22
- ## Behaviour
23
-
24
- - Only manual errors (set via [`setErrors`](set-errors.md)) are removed. Validation errors from rules are recomputed the next time validation runs.
25
- - Visual attributes are re-synced for the affected fields — `css-error` is removed when no errors remain for a field.
26
- - Subscribers are notified.
27
-
28
- ## Examples
29
-
30
- ### Clear a single field
31
-
32
- ```ts
33
- import { form } from '@samline/forms'
34
-
35
- const profile = form('profile-form')
36
-
37
- profile.setErrors({ email: ['Already in use.'] })
38
-
39
- profile.clearErrors(['email'])
40
- ```
41
-
42
- ### Clear everything
43
-
44
- ```ts
45
- profile.setErrors({
46
- email: ['Already in use.'],
47
- password: ['Too weak.']
48
- })
49
-
50
- profile.clearErrors()
51
- ```
52
-
53
- ### Clear before re-running validation
54
-
55
- ```ts
56
- profile.clearErrors()
57
- profile.validate()
58
- ```
59
-
60
- ## Edge cases
61
-
62
- - **Validation errors are not cleared by `clearErrors`.** They live in a separate map and are only cleared by [`reset`](reset.md) or by a successful re-validation of the field.
63
- - **`clearErrors` on a field with no manual error is a no-op** — the chain still returns the controller.
64
- - **Passing an empty array `clearErrors([])` does nothing.** Use `clearErrors()` to clear everything.
65
-
66
- ## Related
67
-
68
- - [`setErrors`](set-errors.md) — push manual errors.
69
- - [`reset`](reset.md) — clear every error and reset field values.
@@ -1,80 +0,0 @@
1
- # `destroy()`
2
-
3
- Tears the controller down. Removes all DOM listeners, disconnects the `MutationObserver`, runs registered cleanups, clears caches, drops subscribers and submit handlers, and resets internal state.
4
-
5
- ## Signature
6
-
7
- ```ts
8
- destroy(): void
9
- ```
10
-
11
- ## Returns
12
-
13
- `void`. Idempotent — calling `destroy()` more than once is a no-op.
14
-
15
- ## Behaviour
16
-
17
- When invoked, the controller:
18
-
19
- - Removes every listener it added (`input`, `submit`, formatter listeners, and document delegation for external controls).
20
- - Cancels any pending auto-submit debounce timer.
21
- - Disconnects the form `MutationObserver`.
22
- - Runs active callbacks registered with [`addCleanup()`](add-cleanup.md) once in reverse registration order. A thrown cleanup is logged and does not stop the others.
23
- - Clears `watchedFields`, `subscribers`, `submitHandlers`, and `fieldCache`.
24
- - Resets manual and validation error maps.
25
- - Clears tracked async-submit state. Pending handler promises continue independently, but no completion notification is emitted after destruction.
26
-
27
- After `destroy()` the controller object still exists and its methods remain callable. Direct reads and writes can still inspect or mutate the bound DOM, but the removed controller listeners no longer react to resulting events.
28
-
29
- ## Examples
30
-
31
- ### SPA route unmount
32
-
33
- ```ts
34
- import { form } from '@samline/forms'
35
-
36
- let profile: ReturnType<typeof form> | null = null
37
-
38
- function mountProfileForm(element: HTMLFormElement) {
39
- profile = form(element, { validators: { email: { required: true } } })
40
- }
41
-
42
- function unmountProfileForm() {
43
- profile?.destroy()
44
- profile = null
45
- }
46
- ```
47
-
48
- ### Cleanup on script teardown
49
-
50
- ```ts
51
- const profile = form('profile-form')
52
-
53
- window.addEventListener('beforeunload', () => {
54
- profile.destroy()
55
- })
56
- ```
57
-
58
- ### Own integration resources
59
-
60
- ```ts
61
- const profile = form('profile-form')
62
- const observer = new ResizeObserver(() => updateLayout())
63
-
64
- if (profile.element) observer.observe(profile.element)
65
- profile.addCleanup(() => observer.disconnect())
66
- ```
67
-
68
- ## Edge cases
69
-
70
- - **`destroy()` does not remove `css-filled` / `css-error` attributes** from fields. If you need that, call [`reset()`](reset.md) before destroy, or strip the attributes yourself.
71
- - **Subscribers and watchers stop receiving notifications immediately**, but their closures remain until you release them.
72
- - **Cleanup registrations are independent.** Registering the same callback twice runs it twice unless one registration's unregister function is called.
73
- - **Late cleanup registration runs immediately.** Calling `addCleanup()` after destruction executes the callback at once and returns a no-op unregister function.
74
- - **Reusing the controller reference after destroy is possible but usually misleading.** Reads still inspect the bound form and methods such as `setValue()` can still write fields, but watchers, subscribers, validation-on-input, and auto-submit no longer run.
75
-
76
- ## Related
77
-
78
- - [`reset`](reset.md) — restore form values without removing listeners.
79
- - [`form`](form.md) — bind a new controller.
80
- - [`addCleanup`](add-cleanup.md) — register controller-owned teardown work.
@@ -1,65 +0,0 @@
1
- # `disableAutoSubmit()`
2
-
3
- Turns auto-submit off and cancels any pending debounce timer.
4
-
5
- ## Signature
6
-
7
- ```ts
8
- disableAutoSubmit(): FormController
9
- ```
10
-
11
- ## Returns
12
-
13
- The same [`FormController`](../typescript.md#formcontroller) — chainable.
14
-
15
- ## Behaviour
16
-
17
- - Sets `state.autoSubmitEnabled` to `false`.
18
- - Cancels `state.autoSubmitTimer` if one is pending.
19
- - Notifies subscribers (because the state’s `autoSubmit` field changes).
20
-
21
- Equivalent to calling [`autoSubmit(false)`](auto-submit.md).
22
-
23
- ## Examples
24
-
25
- ### Pause autosave while typing resumes
26
-
27
- ```ts
28
- import { form } from '@samline/forms'
29
-
30
- const draft = form('draft-form', { autoSubmit: { debounce: 800 } })
31
-
32
- document.querySelector('#pause')?.addEventListener('click', () => {
33
- draft.disableAutoSubmit()
34
- })
35
-
36
- document.querySelector('#resume')?.addEventListener('click', () => {
37
- draft.autoSubmit({ debounce: 800 })
38
- })
39
- ```
40
-
41
- ### Disable on blur
42
-
43
- ```ts
44
- import { form } from '@samline/forms'
45
-
46
- const search = form('search-form', { autoSubmit: { debounce: 250 } })
47
-
48
- search.element?.addEventListener('focusin', () => {
49
- search.autoSubmit({ debounce: 250 })
50
- })
51
-
52
- search.element?.addEventListener('focusout', () => {
53
- search.disableAutoSubmit()
54
- })
55
- ```
56
-
57
- ## Edge cases
58
-
59
- - **Re-enabling later requires calling [`autoSubmit()`](auto-submit.md) again.** The previous debounce configuration is not preserved.
60
- - **A pending debounce timer is removed**, but the timer was already scheduled; nothing fires for it.
61
-
62
- ## Related
63
-
64
- - [`autoSubmit`](auto-submit.md) — enable / configure.
65
- - [`destroy`](destroy.md) — also cancels any pending debounce.
@@ -1,78 +0,0 @@
1
- # `element` and `f`
2
-
3
- Read-only getters that return the bound `HTMLFormElement`, or `null` if the binding target was unresolved at construction time.
4
-
5
- ## Signatures
6
-
7
- ```ts
8
- interface FormController {
9
- readonly element: HTMLFormElement | null
10
- readonly f: HTMLFormElement | null
11
- readonly options: FormControllerOptions
12
- }
13
- ```
14
-
15
- - `element` — the bound form.
16
- - `f` — alias of `element`, kept short for fluent setup.
17
- - `options` — the resolved options object (defaults merged with user-provided values).
18
-
19
- ## Returns
20
-
21
- `HTMLFormElement | null`. `null` when the target passed to [`form()`](form.md) did not resolve to a form.
22
-
23
- ## Examples
24
-
25
- ### Read the bound form
26
-
27
- ```ts
28
- import { form } from '@samline/forms'
29
-
30
- const profile = form('profile-form')
31
-
32
- if (profile.element) {
33
- profile.element.classList.add('mounted')
34
- }
35
- ```
36
-
37
- ### Use the short alias
38
-
39
- ```ts
40
- import { form } from '@samline/forms'
41
-
42
- const profile = form('profile-form')
43
-
44
- profile.f?.querySelector('[data-status]')?.setAttribute('data-state', 'ready')
45
- ```
46
-
47
- ### Read the resolved options
48
-
49
- ```ts
50
- import { form } from '@samline/forms'
51
-
52
- const profile = form('profile-form', {
53
- autoValidate: false,
54
- attributes: { error: 'is-invalid' },
55
- formats: {
56
- phone: { type: 'phone', field: 'phone', options: { country: 'MX' } }
57
- }
58
- })
59
-
60
- profile.options.autoValidate // false (overridden)
61
- profile.options.autoSubmit // false (default)
62
- profile.options.clearErrorsOnSubmit // true (default)
63
- profile.options.attributes.error // 'is-invalid'
64
- profile.options.attributes.filled // 'css-filled' (default)
65
- profile.options.formats.phone // FieldFormatConfig (overridden)
66
- ```
67
-
68
- The `formats` key is the declarative counterpart to [`format()`](format.md) — see [the option reference](../options.md#formats) for the full shape.
69
-
70
- ## Edge cases
71
-
72
- - The getter returns `null` when the target was `null`, `undefined`, a non-existent id, or an element that is not an `HTMLFormElement`.
73
- - The reference is the same one the controller uses internally — do not replace the form node, or the controller will keep listening to the original.
74
-
75
- ## Related
76
-
77
- - [`form(target, options?)`](form.md) — bind a controller.
78
- - [`destroy`](destroy.md) — tear the controller down.
package/docs/api/form.md DELETED
@@ -1,156 +0,0 @@
1
- # `form(target, options?)`
2
-
3
- Creates a new controller bound to a form. This is the main entry point of `@samline/forms`.
4
-
5
- ## Signature
6
-
7
- ```ts
8
- function form(
9
- target: FormTarget,
10
- options?: FormControllerOptions
11
- ): FormController
12
- ```
13
-
14
- ## Parameters
15
-
16
- | Name | Type | Required | Description |
17
- | --- | --- | --- | --- |
18
- | `target` | [`FormTarget`](../typescript.md#formtarget) | yes | What to bind to: a string id, an `HTMLFormElement`, a ref-like `{ current }` object, or `null` / `undefined`. |
19
- | `options` | [`FormControllerOptions`](../typescript.md#formcontrolleroptions) | no | Controller configuration. See [docs/options.md](../options.md). |
20
-
21
- ## Returns
22
-
23
- A [`FormController`](../typescript.md#formcontroller) — chainable for every method that does not return data or an unsubscribe function.
24
-
25
- ## Binding modes
26
-
27
- ### By id string
28
-
29
- ```ts
30
- import { form } from '@samline/forms'
31
-
32
- const profile = form('profile-form')
33
- ```
34
-
35
- The string is passed to `document.getElementById`. If no element is found, or the found element is not an `HTMLFormElement`, the controller is created but `controller.element` is `null` and most methods become no-ops.
36
-
37
- ### By `HTMLFormElement`
38
-
39
- ```ts
40
- const element = document.querySelector<HTMLFormElement>('#profile-form')
41
- const profile = form(element)
42
- ```
43
-
44
- Useful when you already have the element from a `querySelector` call.
45
-
46
- ### By ref-like object
47
-
48
- ```ts
49
- const ref = { current: document.querySelector<HTMLFormElement>('#profile-form') }
50
- const profile = form(ref)
51
- ```
52
-
53
- The controller reads `target.current` only at construction time. If you mount the form later, recreate the controller or call `destroy()` and re-bind.
54
-
55
- ### By `null` / `undefined`
56
-
57
- ```ts
58
- const profile = form()
59
- ```
60
-
61
- Allowed for type-system ergonomics. The resulting controller has `element === null` and is effectively inert.
62
-
63
- ## Behaviour
64
-
65
- On creation the controller:
66
-
67
- 1. Resolves the form element.
68
- 2. Merges the provided options with defaults (`autoValidate: true`, `clearErrorsOnSubmit: true`, `clearManualErrorsOnChange: true`, `attributes: { filled: 'css-filled', error: 'css-error' }`).
69
- 3. Wires delegated `input` and `submit` listeners, including controls associated through `form="id"`.
70
- 4. Starts a `MutationObserver` on the form subtree to track dynamic fields.
71
- 5. Optionally enables [`autoSubmit`](../api/auto-submit.md) when `options.autoSubmit` is truthy.
72
- 6. Synchronizes initial `css-filled` state for every tracked control, including when `autoValidate` is disabled.
73
- 7. Runs an initial validation pass only if `autoValidate` is enabled.
74
-
75
- ## Examples
76
-
77
- ### Minimal setup
78
-
79
- ```ts
80
- import { form } from '@samline/forms'
81
-
82
- const contact = form('contact-form')
83
-
84
- contact.onSubmit(async (_element, _data, formData) => {
85
- await fetch('/api/contact', { method: 'POST', body: formData })
86
- })
87
- ```
88
-
89
- ### With validators and options
90
-
91
- ```ts
92
- import { form } from '@samline/forms'
93
-
94
- const signup = form('signup-form', {
95
- attributes: { filled: 'is-filled', error: 'is-invalid' },
96
- autoValidate: true,
97
- autoSubmit: { debounce: 600 },
98
- clearManualErrorsOnChange: false,
99
- validators: {
100
- email: {
101
- required: { value: true, message: 'Email is required.' },
102
- pattern: {
103
- value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
104
- message: 'Enter a valid email address.'
105
- }
106
- },
107
- password: { required: true, minLength: 8 }
108
- },
109
- formats: {
110
- phone: { type: 'phone', field: 'phone', options: { country: 'MX' } }
111
- }
112
- })
113
- ```
114
-
115
- ### Bind later via ref
116
-
117
- ```ts
118
- import { form } from '@samline/forms'
119
-
120
- const profileRef = { current: null as HTMLFormElement | null }
121
-
122
- const profile = form(profileRef)
123
-
124
- document.querySelector('#mount')?.appendChild(document.createElement('form'))
125
- profileRef.current = document.querySelector<HTMLFormElement>('#mount form')
126
-
127
- // Re-bind because the controller captured `current` at construction time.
128
- profile.destroy()
129
- form(profileRef)
130
- ```
131
-
132
- ## Edge cases
133
-
134
- - **The id is resolved once.** If the DOM element with that id is replaced, the controller keeps listening to the original element. Recreate the controller if you re-render the form.
135
- - **`null` / `undefined` targets** are accepted and produce an inert controller. The chainable methods still return the controller, so they are safe to call as no-ops.
136
- - **Reusing a target across multiple controllers** is supported but they each install their own listeners — call [`destroy()`](destroy.md) on the previous one to avoid duplicate work.
137
- - **`autoValidate: false` disables the initial rule pass, not visual initialization.** Prefilled fields still receive `css-filled`; no validation errors are produced and `isValidated` remains `false`.
138
- - **`createFormController` is also exported** as the lower-level factory; prefer `form` for ergonomic imports.
139
-
140
- ## `createFormController`
141
-
142
- ```ts
143
- function createFormController(
144
- target: FormTarget,
145
- options?: FormControllerOptions
146
- ): FormController
147
- ```
148
-
149
- This is the factory used by `form()` and accepts the same arguments. It is exported for integrations that prefer an explicit constructor-style name.
150
-
151
- ## Related
152
-
153
- - [`FormController`](../typescript.md#formcontroller) — the returned controller type.
154
- - [`destroy`](destroy.md) — tear the controller down.
155
- - [`addCleanup`](add-cleanup.md) — attach integration cleanup to the controller lifecycle.
156
- - [docs/options.md](../options.md) — every option in detail.