@samline/forms 2.3.4 → 2.6.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/LICENSE +21 -0
- package/README.md +8 -7
- package/dist/browser/global.d.ts +10 -16
- package/dist/browser/global.global.js +328 -51
- package/dist/browser/global.global.js.map +1 -1
- package/dist/browser/index.cjs +1309 -0
- package/dist/browser/index.cjs.map +1 -0
- package/dist/browser/index.d.cts +14 -0
- package/dist/browser/index.d.ts +14 -0
- package/dist/browser/index.js +23 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/chunk-L4GRKSQ5.js +1270 -0
- package/dist/chunk-L4GRKSQ5.js.map +1 -0
- package/dist/index.cjs +143 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -158
- package/dist/index.d.ts +4 -158
- package/dist/index.js +7 -1141
- package/dist/index.js.map +1 -1
- package/dist/registry-BpqJetAt.d.cts +148 -0
- package/dist/registry-BpqJetAt.d.ts +148 -0
- package/docs/api/append.md +96 -0
- package/docs/api/auto-submit.md +91 -0
- package/docs/api/clear-errors.md +69 -0
- package/docs/api/destroy.md +65 -0
- package/docs/api/disable-auto-submit.md +65 -0
- package/docs/api/element.md +78 -0
- package/docs/api/form.md +154 -0
- package/docs/api/format.md +270 -0
- package/docs/api/get-data.md +90 -0
- package/docs/api/get-field.md +87 -0
- package/docs/api/get-state.md +95 -0
- package/docs/api/get-value.md +114 -0
- package/docs/api/index.md +54 -0
- package/docs/api/observe.md +126 -0
- package/docs/api/on-submit.md +90 -0
- package/docs/api/parse-form-data.md +94 -0
- package/docs/api/prefill.md +89 -0
- package/docs/api/regex.md +155 -0
- package/docs/api/reset.md +60 -0
- package/docs/api/revalidate.md +50 -0
- package/docs/api/set-errors.md +97 -0
- package/docs/api/set-value.md +120 -0
- package/docs/api/subscribe.md +91 -0
- package/docs/api/unwatch.md +74 -0
- package/docs/api/validate-field-value.md +108 -0
- package/docs/api/validate-values.md +100 -0
- package/docs/api/validate.md +100 -0
- package/docs/api/watch.md +117 -0
- package/docs/browser.md +156 -0
- package/docs/css-styling.md +187 -0
- package/docs/getting-started.md +217 -0
- package/docs/options.md +259 -0
- package/docs/recipes.md +477 -0
- package/docs/typescript.md +576 -0
- package/docs/vanilla.md +12 -0
- package/package.json +60 -16
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# `revalidate(fields?)`
|
|
2
|
+
|
|
3
|
+
Alias of [`validate`](validate.md) kept separate for readability at call sites that want to express “re-run validation now”.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
revalidate(): ValidationResult
|
|
9
|
+
revalidate(fields: string[]): ValidationResult
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Parameters
|
|
13
|
+
|
|
14
|
+
Same as [`validate`](validate.md).
|
|
15
|
+
|
|
16
|
+
## Returns
|
|
17
|
+
|
|
18
|
+
Same as [`validate`](validate.md) — a [`ValidationResult`](../typescript.md#validationresult).
|
|
19
|
+
|
|
20
|
+
## Behaviour
|
|
21
|
+
|
|
22
|
+
Identical to [`validate`](validate.md). The two methods share the same internal implementation.
|
|
23
|
+
|
|
24
|
+
## When to use which
|
|
25
|
+
|
|
26
|
+
Use `validate` when you are running validation as part of an initial check. Use `revalidate` when you are explicitly asking the controller to recompute validation after a state change (e.g. after `setErrors`, after async data loads, after resetting partial form state).
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { form } from '@samline/forms'
|
|
30
|
+
|
|
31
|
+
const profile = form('profile-form', {
|
|
32
|
+
validators: { email: { required: true } }
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Initial check.
|
|
36
|
+
profile.validate()
|
|
37
|
+
|
|
38
|
+
// After a manual state change:
|
|
39
|
+
profile.setValue('email', '')
|
|
40
|
+
profile.revalidate(['email'])
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Edge cases
|
|
44
|
+
|
|
45
|
+
- **`revalidate` is not a different operation from `validate`.** Pick whichever name reads better at the call site.
|
|
46
|
+
- **Both methods mark the form as validated** the first time they are called.
|
|
47
|
+
|
|
48
|
+
## Related
|
|
49
|
+
|
|
50
|
+
- [`validate`](validate.md) — same implementation, different name.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# `setErrors(fields)`
|
|
2
|
+
|
|
3
|
+
Pushes manual errors into the form. The typical use case is surfacing server-side validation feedback after a failed `fetch`.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
setErrors(fields: string[]): FormController
|
|
9
|
+
setErrors(fields: FormErrors): FormController
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Parameters
|
|
13
|
+
|
|
14
|
+
| Overload | Type | Description |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Array form | `string[]` | Each entry is treated as a field name. The error message defaults to `'Invalid value.'`. |
|
|
17
|
+
| Map form | [`FormErrors`](../typescript.md#formerrors) | Map from field name to one or more error messages. |
|
|
18
|
+
|
|
19
|
+
## Returns
|
|
20
|
+
|
|
21
|
+
The same [`FormController`](../typescript.md#formcontroller) — chainable.
|
|
22
|
+
|
|
23
|
+
## Behaviour
|
|
24
|
+
|
|
25
|
+
- Manual errors are merged with existing manual errors — existing entries for other fields are preserved.
|
|
26
|
+
- Validation errors from rules are not affected: validation errors and manual errors live in separate maps and are merged only when reading state.
|
|
27
|
+
- Visual attributes are re-synced for the affected fields, so `css-error` is added to each.
|
|
28
|
+
- Subscribers are notified.
|
|
29
|
+
|
|
30
|
+
## Examples
|
|
31
|
+
|
|
32
|
+
### Array form (generic message)
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { form } from '@samline/forms'
|
|
36
|
+
|
|
37
|
+
const profile = form('profile-form')
|
|
38
|
+
|
|
39
|
+
profile.setErrors(['email', 'password'])
|
|
40
|
+
// state.errors.email -> ['Invalid value.']
|
|
41
|
+
// state.errors.password -> ['Invalid value.']
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Map form (per-field messages)
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
profile.setErrors({
|
|
48
|
+
email: ['This email is already in use.'],
|
|
49
|
+
password: ['Too weak. Use at least 8 characters.']
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Map form with multiple errors per field
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
profile.setErrors({
|
|
57
|
+
password: [
|
|
58
|
+
'Too weak.',
|
|
59
|
+
'Must contain a number.',
|
|
60
|
+
'Must contain a symbol.'
|
|
61
|
+
]
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Surface server errors after a failed submit
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { form } from '@samline/forms'
|
|
69
|
+
|
|
70
|
+
const signup = form('signup-form', {
|
|
71
|
+
clearManualErrorsOnChange: false
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
signup.onSubmit(async (_element, _data, formData) => {
|
|
75
|
+
const response = await fetch('/signup', { method: 'POST', body: formData })
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const { fieldErrors } = await response.json()
|
|
79
|
+
signup.setErrors(fieldErrors)
|
|
80
|
+
} else {
|
|
81
|
+
signup.clearErrors()
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Edge cases
|
|
87
|
+
|
|
88
|
+
- **Manual errors are cleared by default on the next change** of the affected field. Pass `clearManualErrorsOnChange: false` to keep them until you call [`clearErrors`](clear-errors.md).
|
|
89
|
+
- **The array form uses the literal string `'Invalid value.'`.** Use the map form when you need custom messages.
|
|
90
|
+
- **`setErrors` does not run validation.** Validation errors are computed independently by [`validate`](validate.md).
|
|
91
|
+
- **Setting an empty object `setErrors({})` clears nothing** — it just doesn’t add new errors. Use [`clearErrors`](clear-errors.md) to remove existing manual errors.
|
|
92
|
+
|
|
93
|
+
## Related
|
|
94
|
+
|
|
95
|
+
- [`clearErrors`](clear-errors.md) — remove manual errors.
|
|
96
|
+
- [`validate`](validate.md) — run validation and read the result.
|
|
97
|
+
- [`options.clearManualErrorsOnChange`](../options.md#clearmanualerrorsonchange).
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# `setValue(name, value)`
|
|
2
|
+
|
|
3
|
+
Writes a value into a field and dispatches the correct DOM event so watchers, validators, and visual state run as if a user typed.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
setValue(name: string, value: unknown): FormController
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Parameters
|
|
12
|
+
|
|
13
|
+
| Name | Type | Required | Description |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| `name` | `string` | yes | The `name` attribute of the target field. |
|
|
16
|
+
| `value` | `unknown` | yes | The value to write. Coerced per field type. |
|
|
17
|
+
|
|
18
|
+
## Returns
|
|
19
|
+
|
|
20
|
+
The same [`FormController`](../typescript.md#formcontroller) — chainable. Returns the controller unchanged when the field does not exist.
|
|
21
|
+
|
|
22
|
+
## Behaviour by field type
|
|
23
|
+
|
|
24
|
+
| Field type | How `value` is applied |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `<input>` (text-like: `text`, `email`, `password`, …) | `String(value)` is assigned to `field.value`. |
|
|
27
|
+
| `<input>` (text-like) with `name="foo[]"` receiving an array | One array element per input, in order: `input[i].value = String(value[i])`. Inputs beyond the array length are cleared to `''`; extra array elements (more elements than inputs) are dropped. |
|
|
28
|
+
| `<input type="checkbox">` (single) | `field.checked = String(value) === field.value`. |
|
|
29
|
+
| `<input type="checkbox">` (group with the same `name`) | Each checkbox is checked if `Array.isArray(value)` includes its `value`, otherwise all unchecked. |
|
|
30
|
+
| `<input type="radio">` (group with the same `name`) | The radio whose `value` matches `String(value)` is checked. |
|
|
31
|
+
| `<input type="file">` | File inputs are read-only — `setValue` cannot assign a file. Passing `[]` clears the input. |
|
|
32
|
+
| `<select>` (single) | `String(value)` must match an option value. |
|
|
33
|
+
| `<select multiple>` | Every option whose value occurs in the supplied array is selected. A scalar selects its matching option. |
|
|
34
|
+
| `<select>` (group with `name="foo[]"`) receiving an array | One array element per select, in order: `select[i].value = String(value[i])`. Inputs beyond the array length are cleared to `''`; extra array elements are dropped. |
|
|
35
|
+
| `<textarea>` | `String(value)` is assigned to `field.value`. |
|
|
36
|
+
| `<textarea>` (group with `name="foo[]"`) receiving an array | One array element per textarea, in order: `textarea[i].value = String(value[i])`. Inputs beyond the array length are cleared to `''`; extra array elements are dropped. |
|
|
37
|
+
|
|
38
|
+
After writing, the controller dispatches one synthetic `input` event on the **first** matching field.
|
|
39
|
+
|
|
40
|
+
The synthetic event flows through the form-level delegated listener, so the rest of the pipeline runs as if the user had typed: manual errors are cleared (default), validation runs (if `autoValidate`), watchers fire, subscribers are notified, and `autoSubmit` is scheduled.
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
### Set a text value
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { form } from '@samline/forms'
|
|
48
|
+
|
|
49
|
+
const contact = form('contact-form')
|
|
50
|
+
contact.setValue('email', 'hello@example.com')
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Toggle checkboxes
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const signup = form('signup-form')
|
|
57
|
+
|
|
58
|
+
// Multiple values — array form.
|
|
59
|
+
signup.setValue('interests', ['design', 'code'])
|
|
60
|
+
|
|
61
|
+
// Single value — string form.
|
|
62
|
+
signup.setValue('terms', 'yes')
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Select a radio
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const shipping = form('shipping-form')
|
|
69
|
+
shipping.setValue('method', 'express')
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Reset a file input
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const upload = form('upload-form')
|
|
76
|
+
upload.setValue('attachments', [])
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Chain with other setup
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
form('contact-form')
|
|
83
|
+
.setValue('subject', 'Question about pricing')
|
|
84
|
+
.setValue('message', 'Hello,')
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Distribute values across a `name="foo[]"` group
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const signup = form('signup-form')
|
|
91
|
+
|
|
92
|
+
// Each input gets one array element, in order. With two inputs and
|
|
93
|
+
// `name="docusign_email[]"`, this writes the first email to the first
|
|
94
|
+
// input and the second email to the second input.
|
|
95
|
+
signup.setValue('docusign_email[]', ['a@example.com', 'b@example.com'])
|
|
96
|
+
|
|
97
|
+
// When the array is shorter than the input count, the surplus inputs
|
|
98
|
+
// are cleared to ''.
|
|
99
|
+
signup.setValue('docusign_email[]', ['a@example.com'])
|
|
100
|
+
|
|
101
|
+
// When the array is longer than the input count, the extra array
|
|
102
|
+
// elements are dropped — there's no input to write to.
|
|
103
|
+
signup.setValue('docusign_email[]', ['a@example.com', 'b@example.com', 'c@example.com'])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Edge cases
|
|
107
|
+
|
|
108
|
+
- **The synthetic event is dispatched only on the first matching field.** For radio/checkbox groups, this is intentional — the event bubbles, and the controller only needs to react once.
|
|
109
|
+
- **Passing `null` or `undefined` writes an empty string.** This is treated as a value, not as “skip”, so the field becomes empty and watchers fire.
|
|
110
|
+
- **`setValue` is a no-op for unknown field names** — the chain still works, but no event fires and no watchers run.
|
|
111
|
+
- **`setValue` cannot assign to `<input type="file">`.** Browsers do not allow programmatic file assignment for security reasons. Use a DataTransfer or a real file picker.
|
|
112
|
+
- **The `name="foo[]"` suffix is the explicit opt-in for multi-value inputs.** A field with the suffix that receives an array distributes one element per input (surplus inputs are cleared, surplus array elements are dropped). Bare names — `name="foo"` with multiple inputs — keep the original broadcast semantics and receive the same value on every matching input.
|
|
113
|
+
- **Manual errors for the field are cleared by default** (because the synthetic event triggers the delegated handler). Pass `clearManualErrorsOnChange: false` to keep them.
|
|
114
|
+
|
|
115
|
+
## Related
|
|
116
|
+
|
|
117
|
+
- [`getValue`](get-value.md) — read the current value.
|
|
118
|
+
- [`getField`](get-field.md) — read the underlying DOM element.
|
|
119
|
+
- [`reset`](reset.md) — restore the form’s initial values.
|
|
120
|
+
- [`format`](format.md) — when a field is formatted, `setValue('<field>', x)` writes the raw to the hidden mirror and reformats the visible; `setValue('<field>_displayed', x)` writes the formatted to the visible and recomputes the raw. See [the mirror convention](format.md#the-mirror-convention).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# `subscribe(listener)`
|
|
2
|
+
|
|
3
|
+
Registers a listener that fires immediately with the current state and at controller notification points. Returns an unsubscribe function.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
subscribe(listener: FormStateListener): () => void
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Parameters
|
|
12
|
+
|
|
13
|
+
| Name | Type | Required | Description |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| `listener` | [`FormStateListener`](../typescript.md#formstatelistener) | yes | Callback invoked with the full state snapshot. |
|
|
16
|
+
|
|
17
|
+
## Returns
|
|
18
|
+
|
|
19
|
+
A function. Calling it removes the listener.
|
|
20
|
+
|
|
21
|
+
## Behaviour
|
|
22
|
+
|
|
23
|
+
- The listener fires **once synchronously** on registration with the current [`FormStateSnapshot`](../typescript.md#formstatesnapshot).
|
|
24
|
+
- It then fires at explicit controller notification points. This is not a proxy around every internal assignment.
|
|
25
|
+
- Multiple subscribers are supported. They run in registration order.
|
|
26
|
+
|
|
27
|
+
State mutations that fire subscribers include:
|
|
28
|
+
|
|
29
|
+
- Any `input` event on a tracked field.
|
|
30
|
+
- [`setValue`](set-value.md).
|
|
31
|
+
- [`setErrors`](set-errors.md) and [`clearErrors`](clear-errors.md).
|
|
32
|
+
- [`autoSubmit`](auto-submit.md) and [`disableAutoSubmit`](disable-auto-submit.md).
|
|
33
|
+
- [`reset`](reset.md).
|
|
34
|
+
- Submit attempts, after their validation pass and `submitCount` update.
|
|
35
|
+
- `MutationObserver`-detected DOM changes.
|
|
36
|
+
|
|
37
|
+
Direct [`validate`](validate.md) and [`revalidate`](revalidate.md) calls update validation state and visual attributes but do not independently notify subscribers. Use their returned result when synchronous rendering is required.
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Mirror state into a view layer
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { form } from '@samline/forms'
|
|
45
|
+
|
|
46
|
+
const profile = form('profile-form')
|
|
47
|
+
|
|
48
|
+
const unsubscribe = profile.subscribe(state => {
|
|
49
|
+
document.querySelector('#status')!.textContent = state.isValid ? 'Ready' : 'Errors'
|
|
50
|
+
document.querySelector('#counter')!.textContent = String(state.submitCount)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
window.addEventListener('beforeunload', unsubscribe)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### React to a specific field
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const signup = form('signup-form')
|
|
60
|
+
|
|
61
|
+
signup.subscribe(state => {
|
|
62
|
+
const password = state.values.password
|
|
63
|
+
const passwordError = state.errors.password?.[0]
|
|
64
|
+
document.querySelector('#password-hint')!.textContent =
|
|
65
|
+
passwordError ?? (typeof password === 'string' && password.length > 0 ? 'OK' : 'Empty')
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Track submit attempts
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const profile = form('profile-form')
|
|
73
|
+
|
|
74
|
+
profile.subscribe(state => {
|
|
75
|
+
if (state.submitCount > 0 && !state.isValid) {
|
|
76
|
+
console.warn('submit attempt failed:', state.submitCount)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Edge cases
|
|
82
|
+
|
|
83
|
+
- **The listener is invoked with a fresh snapshot** on every fire — it is safe to store the reference for the duration of one call.
|
|
84
|
+
- **Subscribers do not receive partial / incremental updates.** Each fire carries the whole snapshot.
|
|
85
|
+
- **Subscribers and watchers are independent.** A field-level change fires both the watchers for that field and every subscriber.
|
|
86
|
+
- **Unsubscribing does not detach the listener immediately if it is currently iterating.** JavaScript’s `Set` semantics guarantee no skips — once the unsubscribe returns, future mutations will not invoke that listener.
|
|
87
|
+
|
|
88
|
+
## Related
|
|
89
|
+
|
|
90
|
+
- [`watch`](watch.md) and [`observe`](observe.md) — per-field reactions.
|
|
91
|
+
- [`getState`](get-state.md) — read the current snapshot synchronously.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# `unwatch(field?, callback?)`
|
|
2
|
+
|
|
3
|
+
Removes watched callbacks. Three overloads, distinguished by the arguments you pass.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
unwatch(): FormController
|
|
9
|
+
unwatch(field: string): FormController
|
|
10
|
+
unwatch(field: string, callback: FormFieldWatcher): FormController
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Parameters
|
|
14
|
+
|
|
15
|
+
| Overload | Description |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `unwatch()` | Removes every watcher from every field. |
|
|
18
|
+
| `unwatch(field)` | Removes every watcher for the given field. |
|
|
19
|
+
| `unwatch(field, callback)` | Removes only the matching callback for the given field. |
|
|
20
|
+
|
|
21
|
+
## Returns
|
|
22
|
+
|
|
23
|
+
The same [`FormController`](../typescript.md#formcontroller) — chainable.
|
|
24
|
+
|
|
25
|
+
## Behaviour
|
|
26
|
+
|
|
27
|
+
- Watchers added via [`watch`](watch.md) and [`observe`](observe.md) share the same internal registry.
|
|
28
|
+
- For exact-match removal (`field` + `callback`), the same function reference must be passed.
|
|
29
|
+
- For bulk removal (`field` only or no args), all matching watchers are removed.
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
### Remove a specific watcher
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { form } from '@samline/forms'
|
|
37
|
+
|
|
38
|
+
const profile = form('profile-form')
|
|
39
|
+
|
|
40
|
+
const callback = (value: unknown) => console.log(value)
|
|
41
|
+
profile.watch('email', callback)
|
|
42
|
+
|
|
43
|
+
profile.unwatch('email', callback)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Remove all watchers for a field
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
profile.unwatch('email')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Remove every watcher
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
profile.unwatch()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Use `observe`’s unsubscribe
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const stop = profile.observe('email', value => console.log(value))
|
|
62
|
+
stop() // Equivalent to profile.unwatch('email', theCallback)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Edge cases
|
|
66
|
+
|
|
67
|
+
- **`unwatch` does not affect subscribers** added via [`subscribe`](subscribe.md). Use the unsubscribe returned by `subscribe`.
|
|
68
|
+
- **Watchers added with the same callback twice are deduplicated** — calling `unwatch` with that callback removes only one entry.
|
|
69
|
+
- **Bulk removal is safe to call at any time** — it does not throw if there are no watchers.
|
|
70
|
+
|
|
71
|
+
## Related
|
|
72
|
+
|
|
73
|
+
- [`watch`](watch.md) — add a watcher.
|
|
74
|
+
- [`observe`](observe.md) — add a watcher with an unsubscribe function.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# `validateFieldValue(field, value, rules, values)`
|
|
2
|
+
|
|
3
|
+
Pure validator. Runs the rule set against a single value. Does not require a controller — useful in tests, custom submission pipelines, or anywhere you need to evaluate validation rules outside of a bound form.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
function validateFieldValue(
|
|
9
|
+
field: string,
|
|
10
|
+
value: FormFieldValue,
|
|
11
|
+
rules: FieldValidationRules,
|
|
12
|
+
values: FormValues
|
|
13
|
+
): string[]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Parameters
|
|
17
|
+
|
|
18
|
+
| Name | Type | Required | Description |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| `field` | `string` | yes | The field name (used for error messages and to seed `FieldValidationContext`). |
|
|
21
|
+
| `value` | [`FormFieldValue`](../typescript.md#formfieldvalue) | yes | The value to validate. |
|
|
22
|
+
| `rules` | [`FieldValidationRules`](../typescript.md#fieldvalidationrules) | yes | The rule set to apply. |
|
|
23
|
+
| `values` | [`FormValues`](../typescript.md#formvalues) | yes | All current values, exposed to custom validators via `context.values`. |
|
|
24
|
+
|
|
25
|
+
## Returns
|
|
26
|
+
|
|
27
|
+
A `string[]` of error messages. Empty when the field is valid.
|
|
28
|
+
|
|
29
|
+
## Behaviour
|
|
30
|
+
|
|
31
|
+
Runs each rule in order:
|
|
32
|
+
|
|
33
|
+
1. `required` — value must be non-empty (non-empty string, non-empty array, non-undefined).
|
|
34
|
+
2. `minLength` — string length (or array length) must be ≥ the threshold.
|
|
35
|
+
3. `maxLength` — string length (or array length) must be ≤ the threshold.
|
|
36
|
+
4. `pattern` — the string form of the value must match the regex (skipped when the value is empty).
|
|
37
|
+
5. `sameAs` — the value must equal the named value (skipped until both values are non-empty).
|
|
38
|
+
6. `validate` — each custom validator runs in registration order. Returning a string pushes an error; returning `false` pushes `"Validation failed."`; returning `null`, `undefined`, or `true` is treated as a pass.
|
|
39
|
+
|
|
40
|
+
Default error messages are produced when the rule was configured without an explicit `message`. Custom messages come from the `{ value, message }` form of `RuleConfig`.
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
### Validate a single value
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { validateFieldValue } from '@samline/forms'
|
|
48
|
+
|
|
49
|
+
const errors = validateFieldValue(
|
|
50
|
+
'email',
|
|
51
|
+
'invalid',
|
|
52
|
+
{
|
|
53
|
+
required: true,
|
|
54
|
+
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
55
|
+
},
|
|
56
|
+
{ email: 'invalid' }
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
console.log(errors) // ['Value does not match the required pattern.']
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Match two fields
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { validateFieldValue } from '@samline/forms'
|
|
66
|
+
|
|
67
|
+
const errors = validateFieldValue(
|
|
68
|
+
'confirm',
|
|
69
|
+
'foo',
|
|
70
|
+
{
|
|
71
|
+
sameAs: { value: 'password', message: 'Passwords do not match.' }
|
|
72
|
+
},
|
|
73
|
+
{ password: 'bar', confirm: 'foo' }
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
console.log(errors) // ['Passwords do not match.']
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Custom error messages
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { validateFieldValue } from '@samline/forms'
|
|
83
|
+
|
|
84
|
+
const errors = validateFieldValue(
|
|
85
|
+
'password',
|
|
86
|
+
'short',
|
|
87
|
+
{
|
|
88
|
+
minLength: { value: 8, message: 'Use at least 8 characters.' }
|
|
89
|
+
},
|
|
90
|
+
{ password: 'short' }
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
console.log(errors) // ['Use at least 8 characters.']
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Edge cases
|
|
97
|
+
|
|
98
|
+
- **The pattern rule is skipped for empty values.** This is the standard “required + pattern” pattern: required runs first, then pattern runs only when there is a value.
|
|
99
|
+
- **The `sameAs` rule is skipped until both values are non-empty.** Add `required` to each mandatory field; `sameAs` does not imply requiredness.
|
|
100
|
+
- **Pure validation does not track dependencies.** `validateFieldValue` compares the supplied values once. Automatic dependent revalidation is controller behavior.
|
|
101
|
+
- **`validateFieldValue` does not read from any DOM.** Pass everything in as arguments.
|
|
102
|
+
- **`validate` runs after the built-in rules.** It receives the full context, including the current value and the other field values.
|
|
103
|
+
- **Multiple custom validators** can be passed as an array — they all run, and any error from any of them is collected.
|
|
104
|
+
|
|
105
|
+
## Related
|
|
106
|
+
|
|
107
|
+
- [`validateValues`](validate-values.md) — run a schema against a whole values map.
|
|
108
|
+
- [`validate`](validate.md) — controller-bound equivalent.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# `validateValues(values, schema)`
|
|
2
|
+
|
|
3
|
+
Pure schema validator. Runs every field in the schema against a values map and returns the aggregated result. Does not require a controller.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
function validateValues(
|
|
9
|
+
values: FormValues,
|
|
10
|
+
schema: ValidationSchema
|
|
11
|
+
): ValidationResult
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
interface ValidationResult {
|
|
16
|
+
isValid: boolean
|
|
17
|
+
errors: FormErrors
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type FormValues = Record<string, FormFieldValue>
|
|
21
|
+
type FormErrors = Record<string, string[]>
|
|
22
|
+
type ValidationSchema = Record<string, FieldValidationRules>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Parameters
|
|
26
|
+
|
|
27
|
+
| Name | Type | Required | Description |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| `values` | [`FormValues`](../typescript.md#formvalues) | yes | The values to validate. |
|
|
30
|
+
| `schema` | [`ValidationSchema`](../typescript.md#validationschema) | yes | Field → rules map. |
|
|
31
|
+
|
|
32
|
+
## Returns
|
|
33
|
+
|
|
34
|
+
A [`ValidationResult`](../typescript.md#validationresult):
|
|
35
|
+
|
|
36
|
+
- `isValid` — `true` when `errors` is empty.
|
|
37
|
+
- `errors` — a fresh error map.
|
|
38
|
+
|
|
39
|
+
## Behaviour
|
|
40
|
+
|
|
41
|
+
For each `[field, rules]` entry in the schema, runs [`validateFieldValue`](validate-field-value.md) against `values[field]`. The other values are available to `sameAs` and custom validators.
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
### Validate a snapshot
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { validateValues } from '@samline/forms'
|
|
49
|
+
|
|
50
|
+
const result = validateValues(
|
|
51
|
+
{
|
|
52
|
+
email: 'invalid',
|
|
53
|
+
terms: ''
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
email: {
|
|
57
|
+
required: true,
|
|
58
|
+
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
59
|
+
},
|
|
60
|
+
terms: {
|
|
61
|
+
required: { value: true, message: 'Accept the terms.' }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
console.log(result.isValid) // false
|
|
67
|
+
console.log(result.errors) // { email: [...], terms: ['Accept the terms.'] }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Use with `parseFormData`
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { parseFormData, validateValues } from '@samline/forms'
|
|
74
|
+
|
|
75
|
+
const formElement = document.querySelector<HTMLFormElement>('#contact-form')
|
|
76
|
+
if (!formElement) throw new Error('form not found')
|
|
77
|
+
|
|
78
|
+
const { data } = parseFormData(formElement)
|
|
79
|
+
|
|
80
|
+
const result = validateValues(data as Record<string, string | string[] | File[] | undefined>, {
|
|
81
|
+
email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
|
|
82
|
+
message: { required: true, minLength: 10 }
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (!result.isValid) {
|
|
86
|
+
console.warn(result.errors)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Edge cases
|
|
91
|
+
|
|
92
|
+
- **`validateValues` does not know which fields are “tracked”.** It validates every entry in the schema, even fields that are not present in `values`. An undefined value fails `required` but passes the rest.
|
|
93
|
+
- **Errors are always a fresh object.** Safe to mutate.
|
|
94
|
+
- **`sameAs` compares the provided snapshot only.** Dependency tracking and automatic revalidation belong to a form controller, not this pure helper.
|
|
95
|
+
- **Custom validators receive the full values map**, so cross-field rules work the same as inside a controller.
|
|
96
|
+
|
|
97
|
+
## Related
|
|
98
|
+
|
|
99
|
+
- [`validateFieldValue`](validate-field-value.md) — single-field validator.
|
|
100
|
+
- [`parseFormData`](parse-form-data.md) — produce a `FormValues` map from a form.
|