@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.
- package/CHANGELOG.md +316 -0
- package/README.md +119 -119
- package/dist/browser/global.d.ts +50 -16
- package/dist/browser/global.global.js +12 -6188
- package/dist/browser/global.global.js.map +1 -1
- package/dist/browser/index.cjs +340 -84
- package/dist/browser/index.cjs.map +1 -1
- package/dist/browser/index.d.cts +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/{chunk-MMHX34WB.js → chunk-6MA5TF6R.js} +341 -85
- package/dist/chunk-6MA5TF6R.js.map +1 -0
- package/dist/index.cjs +340 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{registry-GzopYPCR.d.ts → registry-CPe_lf8-.d.cts} +50 -4
- package/dist/{registry-GzopYPCR.d.cts → registry-CPe_lf8-.d.ts} +50 -4
- package/package.json +39 -8
- package/dist/chunk-MMHX34WB.js.map +0 -1
- package/docs/api/add-cleanup.md +0 -52
- package/docs/api/append.md +0 -96
- package/docs/api/auto-submit.md +0 -91
- package/docs/api/clear-errors.md +0 -69
- package/docs/api/destroy.md +0 -80
- package/docs/api/disable-auto-submit.md +0 -65
- package/docs/api/element.md +0 -78
- package/docs/api/form.md +0 -156
- package/docs/api/format.md +0 -270
- package/docs/api/get-data.md +0 -90
- package/docs/api/get-field.md +0 -87
- package/docs/api/get-state.md +0 -111
- package/docs/api/get-value.md +0 -114
- package/docs/api/index.md +0 -55
- package/docs/api/observe.md +0 -126
- package/docs/api/on-submit.md +0 -94
- package/docs/api/parse-form-data.md +0 -94
- package/docs/api/prefill.md +0 -89
- package/docs/api/regex.md +0 -155
- package/docs/api/reset.md +0 -61
- package/docs/api/revalidate.md +0 -52
- package/docs/api/set-errors.md +0 -97
- package/docs/api/set-value.md +0 -120
- package/docs/api/subscribe.md +0 -107
- package/docs/api/unwatch.md +0 -74
- package/docs/api/validate-field-value.md +0 -144
- package/docs/api/validate-values.md +0 -103
- package/docs/api/validate.md +0 -102
- package/docs/api/watch.md +0 -117
- package/docs/browser.md +0 -156
- package/docs/css-styling.md +0 -191
- package/docs/getting-started.md +0 -222
- package/docs/options.md +0 -326
- package/docs/recipes.md +0 -531
- package/docs/typescript.md +0 -615
- package/docs/vanilla.md +0 -12
- /package/{LICENSE → LICENSE.md} +0 -0
package/docs/getting-started.md
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
# Getting Started
|
|
2
|
-
|
|
3
|
-
This page explains what `@samline/forms` is, how the controller is wired, and which side effects to expect from each method. Use it as a mental model before diving into the per-method reference under [`docs/api/`](api/index.md).
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## When to use this variant
|
|
8
|
-
|
|
9
|
-
Use the main `@samline/forms` entrypoint for native HTML forms. The package also provides `@samline/forms/browser` as an ESM/CJS browser module and `@samline/forms/browser/global` as the global IIFE distribution.
|
|
10
|
-
|
|
11
|
-
> Note: the latest version is `2.7.2` — see [Releases](https://github.com/samline/forms/releases) for the changelog.
|
|
12
|
-
|
|
13
|
-
If you want a `<script>`-only setup without a bundler, see [docs/browser.md](browser.md).
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Anatomy of a controller
|
|
18
|
-
|
|
19
|
-
A controller is created with [`form()`](api/form.md) and is bound to exactly one `HTMLFormElement`. Once bound, it:
|
|
20
|
-
|
|
21
|
-
- Listens to delegated `input` events, including controls associated through `form="id"`.
|
|
22
|
-
- Listens to the native `submit` event.
|
|
23
|
-
- Maintains a small internal state (values, errors, watched fields, subscribers, submit handlers).
|
|
24
|
-
- Applies DOM attributes (`css-filled`, `css-error`) to fields when needed.
|
|
25
|
-
- Watches the form subtree with a `MutationObserver` so dynamic fields work.
|
|
26
|
-
|
|
27
|
-
The controller is returned with a small, focused method surface. Most methods are chainable and return the same controller instance so you can compose setup fluently.
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
import { form } from '@samline/forms'
|
|
31
|
-
|
|
32
|
-
const contact = form('contact-form')
|
|
33
|
-
.watch('email', value => console.log(value))
|
|
34
|
-
.setErrors({ email: ['Already in use'] })
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Methods that return data instead of the controller: [`getValue`](api/get-value.md), [`getField`](api/get-field.md), [`getData`](api/get-data.md), [`getState`](api/get-state.md), [`validate`](api/validate.md), [`revalidate`](api/revalidate.md). Methods that return an unsubscribe or unregister function: [`observe`](api/observe.md), [`subscribe`](api/subscribe.md), [`addCleanup`](api/add-cleanup.md).
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Observable contract
|
|
42
|
-
|
|
43
|
-
Once a controller is created, you can rely on the following behaviour:
|
|
44
|
-
|
|
45
|
-
- **`css-filled` attribute** is added to a field when it has a non-empty value, and removed when it becomes empty. Override the attribute name with `options.attributes.filled`.
|
|
46
|
-
- **Initial filled state is always synchronized.** Prefilled controls receive `css-filled` on mount even with `autoValidate: false`; this does not run validation or set `isValidated`.
|
|
47
|
-
- **`css-error` attribute** is added to a field when it has at least one error (validation or manual), and removed when it has none. Override with `options.attributes.error`.
|
|
48
|
-
- **Validation runs on every change** for any field that has rules configured under `options.validators` (gated by `autoValidate`, default `true`).
|
|
49
|
-
- **Manual errors from [`setErrors`](api/set-errors.md) are cleared by default when the affected field changes.** Set `clearManualErrorsOnChange: false` to keep them.
|
|
50
|
-
- **Submit handlers receive a real `FormData` instance** built from the live form, plus a plain-object mirror. Invalid submissions never reach your handler.
|
|
51
|
-
- **Async submit handlers are tracked.** `getState().isSubmitting` stays true while any promise-returning `onSubmit` work is pending, including overlapping submissions, and returns to false after fulfillment or rejection.
|
|
52
|
-
- **Dynamic fields are auto-discovered.** A `MutationObserver` watches the form subtree for `name` / `type` attribute changes or new fields, re-caches the field registry, and re-runs visual state and validation.
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## Lifecycle
|
|
57
|
-
|
|
58
|
-
The recommended flow:
|
|
59
|
-
|
|
60
|
-
1. **Mount** — call [`form()`](api/form.md) with a binding target and options. The controller wires events, applies initial visual state, and (when `autoValidate`) runs an initial validation pass.
|
|
61
|
-
2. **React** — register [`watch`](api/watch.md) / [`observe`](api/observe.md) callbacks for field-level reactions, and [`subscribe`](api/subscribe.md) for whole-form reactions.
|
|
62
|
-
3. **Validate** — built-in validation runs automatically. Use [`validate`](api/validate.md) (or the alias [`revalidate`](api/revalidate.md)) to run it on demand.
|
|
63
|
-
4. **Submit** — register [`onSubmit`](api/on-submit.md) handlers. Valid submissions are intercepted (default) or allowed to continue natively (`preventDefault: false`).
|
|
64
|
-
5. **Reset** — call [`reset`](api/reset.md) to restore native defaults, clear errors, and re-synchronize visual attributes.
|
|
65
|
-
6. **Destroy** — register integration teardown with [`addCleanup`](api/add-cleanup.md), then call [`destroy`](api/destroy.md) to remove listeners, disconnect the `MutationObserver`, run cleanups in reverse registration order, clear caches, and drop all subscribers.
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## Side effects per method
|
|
70
|
-
|
|
71
|
-
Use this as a quick lookup when you need to know what a method will touch.
|
|
72
|
-
|
|
73
|
-
| Method | DOM mutation | Events fired | Subscribers notified | AutoSubmit trigger | Visual state |
|
|
74
|
-
| --- | --- | --- | --- | --- | --- |
|
|
75
|
-
| [`setValue`](api/set-value.md) | yes (writes value, then `input`) | `input` | yes | yes | re-synced for that field |
|
|
76
|
-
| [`getValue`](api/get-value.md) | no | no | no | no | unchanged |
|
|
77
|
-
| [`getField`](api/get-field.md) | no | no | no | no | unchanged |
|
|
78
|
-
| [`prefill`](api/prefill.md) | yes (writes via [`setValue`](api/set-value.md)) | per-field | yes | yes | re-synced per touched field |
|
|
79
|
-
| [`append`](api/append.md) | yes (inserts a node; removes prior node with same class) | no | no | no | unchanged |
|
|
80
|
-
| [`validate`](api/validate.md) | re-syncs visual attributes for the targeted fields | no | no | no | re-synced |
|
|
81
|
-
| [`revalidate`](api/revalidate.md) | same as [`validate`](api/validate.md) | no | no | no | re-synced |
|
|
82
|
-
| [`setErrors`](api/set-errors.md) | re-syncs visual attributes for the targeted fields | no | yes | no | re-synced |
|
|
83
|
-
| [`clearErrors`](api/clear-errors.md) | re-syncs visual attributes (all or for the listed fields) | no | yes | no | re-synced |
|
|
84
|
-
| [`reset`](api/reset.md) | calls native `form.reset()`; clears errors and re-syncs attributes from default values | no | yes | no | re-synced |
|
|
85
|
-
| [`onSubmit`](api/on-submit.md) | none directly; submit handler is invoked on submit | n/a | n/a | n/a | n/a |
|
|
86
|
-
| [`autoSubmit`](api/auto-submit.md) | schedules `form.requestSubmit()` (with optional debounce) | native submit | yes (config change) | enables the behaviour | unchanged |
|
|
87
|
-
| [`disableAutoSubmit`](api/disable-auto-submit.md) | cancels any pending debounce timer | no | yes | disables | unchanged |
|
|
88
|
-
| [`watch`](api/watch.md) / [`observe`](api/observe.md) | none directly; callback fires on changes | n/a | n/a | n/a | unchanged |
|
|
89
|
-
| [`unwatch`](api/unwatch.md) | none | no | no | no | unchanged |
|
|
90
|
-
| [`subscribe`](api/subscribe.md) | none; fires immediately with the current snapshot | no | n/a | n/a | unchanged |
|
|
91
|
-
| [`addCleanup`](api/add-cleanup.md) | none until its callback runs during destroy | no | no | no | unchanged |
|
|
92
|
-
| [`getData`](api/get-data.md) | no | no | no | no | unchanged |
|
|
93
|
-
| [`getState`](api/get-state.md) | no | no | no | no | unchanged |
|
|
94
|
-
| [`destroy`](api/destroy.md) | removes listeners and runs registered cleanups; does not remove visual attributes itself | no | no longer fires | disabled | unchanged |
|
|
95
|
-
|
|
96
|
-
> Native field `input` events are delegated. Controls outside the form that use `form="id"` are handled through a document-level delegated listener.
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## Recommended usage patterns
|
|
101
|
-
|
|
102
|
-
- Use [`watch`](api/watch.md) when you want a chainable, fire-and-forget reaction to a field. Use [`observe`](api/observe.md) when you need to clean up later — it returns an unsubscribe function.
|
|
103
|
-
- Use [`subscribe`](api/subscribe.md) when a higher-level component (router, store, view layer) needs to react to the whole form state.
|
|
104
|
-
- Use [`addCleanup`](api/add-cleanup.md) when third-party listeners, observers, subscriptions, or abort controllers should be released with the form controller.
|
|
105
|
-
- Use [`getData()`](api/get-data.md) when you need both a plain object and a `FormData` instance.
|
|
106
|
-
- Use [`setErrors`](api/set-errors.md) and [`clearErrors`](api/clear-errors.md) to drive visual feedback from server responses.
|
|
107
|
-
- Pass `clearManualErrorsOnChange: false` only when manual errors should persist after the user edits a field.
|
|
108
|
-
- Use [`autoSubmit({ debounce })`](api/auto-submit.md) for autosave / autosubmit flows. Pair with [`disableAutoSubmit()`](api/disable-auto-submit.md) when you want to pause it.
|
|
109
|
-
- Call [`destroy()`](api/destroy.md) whenever a form is mounted and unmounted dynamically (SPA route changes, conditional rendering, modals).
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
## Direct utilities
|
|
114
|
-
|
|
115
|
-
The package also exposes a few pure helpers. They do not instantiate a controller — they accept plain values or a raw `HTMLFormElement` and return results.
|
|
116
|
-
|
|
117
|
-
```ts
|
|
118
|
-
import {
|
|
119
|
-
parseFormData,
|
|
120
|
-
validateFieldValue,
|
|
121
|
-
validateValues
|
|
122
|
-
} from '@samline/forms'
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
- [`parseFormData(formElement, submitter?)`](api/parse-form-data.md) — same serializer the controller uses internally. Returns `{ data, formData }`.
|
|
126
|
-
- [`validateFieldValue(field, value, rules, values)`](api/validate-field-value.md) — runs the rule set against a single value.
|
|
127
|
-
- [`validateValues(values, schema)`](api/validate-values.md) — runs the rule set against a full values map.
|
|
128
|
-
|
|
129
|
-
These are safe to tree-shake into any bundle and do not require a live form.
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## Browser registry helpers
|
|
134
|
-
|
|
135
|
-
The browser IIFE bundle ships a small registry (`window.Forms`) that wraps `form()` with a `newForm` / `destroyForm` pair keyed by the form id. The same shape is available from the vanilla entrypoint as a module-level singleton called `browser`:
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
import { browser } from '@samline/forms'
|
|
139
|
-
import { regex } from '@samline/formatter' // optional, from your project
|
|
140
|
-
|
|
141
|
-
window.Form = { ...browser, regex }
|
|
142
|
-
|
|
143
|
-
window.Form.newForm({
|
|
144
|
-
id: 'contact-form',
|
|
145
|
-
options: {
|
|
146
|
-
validators: {
|
|
147
|
-
email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
window.Form.destroyForm('contact-form')
|
|
153
|
-
console.log(window.Form.available) // { 'contact-form': FormController }
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
`browser` is an object you can spread into your own globals or use directly. Because `newForm` and `destroyForm` close over the singleton's `available` map, every spread shares the same registry — `window.Form.available`, `browser.available`, and any other spread all point to the same object, and `destroyForm` on one updates the others.
|
|
157
|
-
|
|
158
|
-
If you need multiple independent registries, call the `form()` factory directly and keep your own map of controllers — `browser` is designed for the common case of one registry per page.
|
|
159
|
-
|
|
160
|
-
```ts
|
|
161
|
-
import { form } from '@samline/forms'
|
|
162
|
-
|
|
163
|
-
const checkout = form('cart-form')
|
|
164
|
-
const auth = form('login-form')
|
|
165
|
-
|
|
166
|
-
// Track them yourself when you have multiple registries.
|
|
167
|
-
const registries = {
|
|
168
|
-
checkout,
|
|
169
|
-
auth
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
Use `form()` directly when you do not need the registry. The main entrypoint never touches `globalThis`. For a module that also registers `window.Forms`, import `@samline/forms/browser`; for a direct `<script>` tag, load the `@samline/forms/browser/global` IIFE described in [the browser guide](browser.md).
|
|
174
|
-
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
## Submission examples
|
|
178
|
-
|
|
179
|
-
### Intercept and send with `fetch` (default)
|
|
180
|
-
|
|
181
|
-
```ts
|
|
182
|
-
import { form } from '@samline/forms'
|
|
183
|
-
|
|
184
|
-
const profile = form('profile-form')
|
|
185
|
-
|
|
186
|
-
profile.onSubmit(async (_element, _data, formData) => {
|
|
187
|
-
const response = await fetch('/profile', {
|
|
188
|
-
method: 'POST',
|
|
189
|
-
body: formData
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
if (!response.ok) throw new Error('Profile update failed')
|
|
193
|
-
})
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
When `preventDefault` is omitted, valid submissions are intercepted — the browser will not navigate. Invalid submissions are always intercepted, regardless of the flag. Promise-returning handlers run concurrently and are reflected by `isSubmitting`; rejection settles that state but is not converted into a form error.
|
|
197
|
-
|
|
198
|
-
### Let the browser submit natively
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
import { form } from '@samline/forms'
|
|
202
|
-
|
|
203
|
-
const bladeForm = form('blade-form', {
|
|
204
|
-
validators: { email: { required: true } }
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
bladeForm.onSubmit(() => {
|
|
208
|
-
// Runs only when validation passes.
|
|
209
|
-
console.log('client-side hooks ran')
|
|
210
|
-
}, false)
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
Pass `false` as the second argument when you want the browser’s native submit behaviour to continue after validation (Blade, ERB, plain PHP, classic Rails forms).
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
## Next steps
|
|
218
|
-
|
|
219
|
-
- Need a full options reference? See [docs/options.md](options.md).
|
|
220
|
-
- Looking up the exact signature of a method? See [docs/api/index.md](api/index.md).
|
|
221
|
-
- Working with the type system? See [docs/typescript.md](typescript.md).
|
|
222
|
-
- Want end-to-end patterns? See [docs/recipes.md](recipes.md).
|
package/docs/options.md
DELETED
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
# Options
|
|
2
|
-
|
|
3
|
-
`form(target, options?)` accepts a `FormControllerOptions` object. Every option has a default, so you can pass only what you need.
|
|
4
|
-
|
|
5
|
-
```ts
|
|
6
|
-
import { form } from '@samline/forms'
|
|
7
|
-
|
|
8
|
-
const contactForm = form('contact-form', {
|
|
9
|
-
attributes: { filled: 'is-filled', error: 'is-invalid' },
|
|
10
|
-
autoValidate: true,
|
|
11
|
-
autoSubmit: { debounce: 500 },
|
|
12
|
-
clearErrorsOnSubmit: true,
|
|
13
|
-
clearManualErrorsOnChange: true,
|
|
14
|
-
validators: {
|
|
15
|
-
email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
|
|
16
|
-
},
|
|
17
|
-
formats: {
|
|
18
|
-
phone: { type: 'phone', field: 'phone', options: { country: 'MX' } }
|
|
19
|
-
}
|
|
20
|
-
})
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## Signature
|
|
26
|
-
|
|
27
|
-
```ts
|
|
28
|
-
interface FormControllerOptions {
|
|
29
|
-
attributes?: Partial<VisualAttributes>
|
|
30
|
-
autoValidate?: boolean
|
|
31
|
-
autoSubmit?: boolean | AutoSubmitOptions
|
|
32
|
-
clearManualErrorsOnChange?: boolean
|
|
33
|
-
clearErrorsOnSubmit?: boolean
|
|
34
|
-
validators?: ValidationSchema
|
|
35
|
-
formats?: FieldFormatConfigMap
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## Options reference
|
|
42
|
-
|
|
43
|
-
| Option | Type | Default | Behaviour |
|
|
44
|
-
| --- | --- | --- | --- |
|
|
45
|
-
| `attributes` | `Partial<VisualAttributes>` | `{ filled: 'css-filled', error: 'css-error' }` | Override the names of the visual attributes applied to fields. |
|
|
46
|
-
| `autoValidate` | `boolean` | `true` | Run validation on construction and on handled `input` events for fields that have rules. Initial `css-filled` synchronization still runs when this is `false`. |
|
|
47
|
-
| `autoSubmit` | `boolean \| AutoSubmitOptions` | `false` | Submit automatically after handled `input` events. Pass `{ debounce: ms }` to delay. |
|
|
48
|
-
| `clearErrorsOnSubmit` | `boolean` | `true` | Clear all manual errors before submit validation runs. |
|
|
49
|
-
| `clearManualErrorsOnChange` | `boolean` | `true` | Clear the manual error of a field when it changes. Set `false` to keep manual errors until you call [`clearErrors`](api/clear-errors.md). |
|
|
50
|
-
| `validators` | `ValidationSchema` | `{}` | Field → rules map. See [docs/typescript.md](typescript.md#validationschema) and the rule reference below. |
|
|
51
|
-
| `formats` | [`FieldFormatConfigMap`](typescript.md#fieldformatconfig-and-fieldformatconfigmap) | `{}` | Declarative `@samline/formatter` configuration. Each entry is applied during `form()` initialization using the same logic as [`format()`](api/format.md). Requires the optional peer dependency. |
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## `attributes`
|
|
56
|
-
|
|
57
|
-
By default the controller adds `css-filled` to a field when it has a value, and `css-error` when it has at least one error. Override the names with `attributes`:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
form('profile-form', {
|
|
61
|
-
attributes: { filled: 'is-filled', error: 'is-invalid' }
|
|
62
|
-
})
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
This affects which DOM attributes the controller toggles. You provide the CSS. See [docs/css-styling.md](css-styling.md) for recipes.
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## `autoValidate`
|
|
70
|
-
|
|
71
|
-
When `true` (default), the controller:
|
|
72
|
-
|
|
73
|
-
- Runs an initial validation pass on mount for every field that has rules.
|
|
74
|
-
- Validates each field on every delegated `input` event when the field has rules.
|
|
75
|
-
|
|
76
|
-
Set this to `false` if you want to validate manually (e.g. only on submit, or only when the user moves past a step):
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
form('wizard-form', { autoValidate: false }).validate(['step-1'])
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
The initial visual-state pass is independent of validation. Prefilled controls receive `css-filled` on mount even when `autoValidate` is `false`, but no rules run and `isValidated` remains `false` until validation is requested.
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
## `autoSubmit`
|
|
87
|
-
|
|
88
|
-
When `true`, the controller schedules a native submit (`form.requestSubmit()`) on every change. Pass an `AutoSubmitOptions` object to add a debounce:
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
form('search-form', {
|
|
92
|
-
autoSubmit: { debounce: 300 }
|
|
93
|
-
})
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
- `autoSubmit: true` — submit immediately on every change.
|
|
97
|
-
- `autoSubmit: { debounce: 300 }` — submit 300ms after the last change.
|
|
98
|
-
- `autoSubmit: false` — disabled at mount; enable later with [`autoSubmit()`](api/auto-submit.md).
|
|
99
|
-
|
|
100
|
-
Disable at any time with [`disableAutoSubmit()`](api/disable-auto-submit.md).
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
## `clearErrorsOnSubmit`
|
|
105
|
-
|
|
106
|
-
When `true` (default), all manual errors created with [`setErrors`](api/set-errors.md) are cleared before submit validation runs. Validation errors from the current rules are computed fresh. Set this to `false` if you want manual errors to survive submit attempts:
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
form('profile-form', { clearErrorsOnSubmit: false })
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
This only affects manual errors. Validation errors always recompute on submit.
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## `clearManualErrorsOnChange`
|
|
117
|
-
|
|
118
|
-
When `true` (default), the manual error of a field is cleared the moment the field changes (including changes triggered through [`setValue`](api/set-value.md)). Validation then runs and decides the new error state.
|
|
119
|
-
|
|
120
|
-
Set this to `false` to keep manual errors visible until you explicitly clear them with [`clearErrors`](api/clear-errors.md):
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
form('profile-form', { clearManualErrorsOnChange: false })
|
|
124
|
-
|
|
125
|
-
profileForm.setErrors({ email: ['This email is already in use.'] })
|
|
126
|
-
// The error stays visible even after the user edits the field.
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Use this when manual errors come from server validation and you want them to persist across edits.
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## `validators`
|
|
134
|
-
|
|
135
|
-
A map of field names to rule sets. Each rule set is a `FieldValidationRules` object.
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
form('signup-form', {
|
|
139
|
-
validators: {
|
|
140
|
-
email: {
|
|
141
|
-
required: true,
|
|
142
|
-
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
143
|
-
},
|
|
144
|
-
password: {
|
|
145
|
-
required: true,
|
|
146
|
-
minLength: { value: 8, message: 'Use at least 8 characters.' },
|
|
147
|
-
maxLength: 64
|
|
148
|
-
},
|
|
149
|
-
password_confirmation: {
|
|
150
|
-
required: true,
|
|
151
|
-
sameAs: {
|
|
152
|
-
value: 'password',
|
|
153
|
-
message: 'Passwords do not match.'
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
amount: {
|
|
157
|
-
required: true,
|
|
158
|
-
numeric: true,
|
|
159
|
-
min: 0,
|
|
160
|
-
max: 10000
|
|
161
|
-
},
|
|
162
|
-
'attendees[].email': {
|
|
163
|
-
each: {
|
|
164
|
-
required: true,
|
|
165
|
-
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
})
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### Supported rules
|
|
173
|
-
|
|
174
|
-
| Rule | Type | Description |
|
|
175
|
-
| --- | --- | --- |
|
|
176
|
-
| `required` | `boolean \| { value: boolean; message?: string }` | Field must have a non-empty value. |
|
|
177
|
-
| `minLength` | `number \| { value: number; message?: string }` | Minimum string length (for checkbox groups, the minimum number of selected items). |
|
|
178
|
-
| `maxLength` | `number \| { value: number; message?: string }` | Maximum string length (for checkbox groups, the maximum number of selected items). |
|
|
179
|
-
| `pattern` | `RegExp \| { value: RegExp; message?: string }` | String must match the regular expression. Skipped when the field is empty. |
|
|
180
|
-
| `numeric` | `boolean \| { value: boolean; message?: string }` | Non-empty value must be an ordinary signed decimal. |
|
|
181
|
-
| `min` | `number \| { value: number; message?: string }` | Non-empty numeric value must be greater than or equal to the inclusive bound. |
|
|
182
|
-
| `max` | `number \| { value: number; message?: string }` | Non-empty numeric value must be less than or equal to the inclusive bound. |
|
|
183
|
-
| `sameAs` | `string \| { value: string; message?: string }` | Non-empty value must equal the named field. Changing the named field automatically revalidates this field. |
|
|
184
|
-
| `dependsOn` | `string \| string[]` | Names fields whose changes should revalidate this field. Useful for custom cross-field validators; it does not itself add a validation error. |
|
|
185
|
-
| `each` | `ValueValidationRules` | Applies value-level rules independently to every member/control in a collection. |
|
|
186
|
-
| `validate` | `FieldValidator \| FieldValidator[]` | Custom validators. Return a string to push an error, or `null` / `undefined` / `true` to pass. Return `false` to push a generic `"Validation failed."` message. |
|
|
187
|
-
|
|
188
|
-
Built-in rules accept either a plain value or a `{ value, message }` object. Use the object form when you want a custom error message per rule.
|
|
189
|
-
|
|
190
|
-
`numeric`, `min`, and `max` trim surrounding whitespace and accept only ordinary signed decimal syntax, such as `12`, `-3.5`, `+4`, `.75`, or `10.`. They reject exponent notation, hexadecimal, `Infinity`, `NaN`, and formatted values containing separators. Empty values skip all three rules; add `required` when emptiness should fail. `min` and `max` are inclusive and also reject a non-numeric value even when `numeric` is omitted.
|
|
191
|
-
|
|
192
|
-
Their default messages are `"Value must be a number."`, `"Minimum value is N."`, and `"Maximum value is N."` respectively.
|
|
193
|
-
|
|
194
|
-
### Matching fields with `sameAs`
|
|
195
|
-
|
|
196
|
-
Declare `sameAs` on the field that owns the error, normally the confirmation field:
|
|
197
|
-
|
|
198
|
-
```ts
|
|
199
|
-
form('signup-form', {
|
|
200
|
-
validators: {
|
|
201
|
-
password: { required: true, minLength: 8 },
|
|
202
|
-
password_confirmation: {
|
|
203
|
-
required: true,
|
|
204
|
-
sameAs: {
|
|
205
|
-
value: 'password',
|
|
206
|
-
message: 'Passwords do not match.'
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
The controller records that `password_confirmation` depends on `password`. Once validation is active, input in either field validates the confirmation, including updates made through `setValue()`. Validation is active immediately with the default `autoValidate: true`; with `autoValidate: false`, it begins after the first `validate()` or `revalidate()` call. You do not need `watch()` or a manual `revalidate()` call for this dependency.
|
|
214
|
-
|
|
215
|
-
Important behavior:
|
|
216
|
-
|
|
217
|
-
- `sameAs` is skipped until both fields have non-empty values. Use `required` separately when either field is mandatory.
|
|
218
|
-
- Strings use exact, case-sensitive equality. Arrays use ordered item equality, so `['a', 'b']` does not equal `['b', 'a']`. File entries compare by `File` object identity.
|
|
219
|
-
- The referenced name must be the exact HTML field name. Wildcards and inferred `_confirmation` names are not supported.
|
|
220
|
-
- Put the rule on the confirmation field only in most forms. Putting reciprocal rules on both fields is cycle-safe, but both fields will own and display the same mismatch error.
|
|
221
|
-
- Dependency cycles do not recurse. The controller resolves the affected fields with a visited set and validates each field at most once per input event.
|
|
222
|
-
|
|
223
|
-
### Custom cross-field dependencies with `dependsOn`
|
|
224
|
-
|
|
225
|
-
Custom validators can read any value, but the controller cannot infer those reads. Declare each source with `dependsOn` so changing it revalidates the field that owns the custom validator:
|
|
226
|
-
|
|
227
|
-
```ts
|
|
228
|
-
form('shipping-form', {
|
|
229
|
-
validators: {
|
|
230
|
-
method: { required: true },
|
|
231
|
-
country: {
|
|
232
|
-
dependsOn: 'method',
|
|
233
|
-
validate: ({ value, values }) =>
|
|
234
|
-
values.method === 'domestic' && value !== 'US'
|
|
235
|
-
? 'Domestic shipping requires a US address.'
|
|
236
|
-
: null
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
})
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
Names are exact HTML field names. A string array declares multiple sources. Like `sameAs`, dependencies become reactive once validation is active, and transitive chains and cycles are deduplicated so each affected field runs at most once per input event. `dependsOn` is controller metadata only: the pure validation helpers evaluate the supplied snapshot without tracking changes.
|
|
243
|
-
|
|
244
|
-
### Validate every collection member with `each`
|
|
245
|
-
|
|
246
|
-
Use `each` for repeated controls, checkbox groups, or other fields whose value is a collection:
|
|
247
|
-
|
|
248
|
-
```ts
|
|
249
|
-
form('attendees-form', {
|
|
250
|
-
validators: {
|
|
251
|
-
'attendees[].email': {
|
|
252
|
-
minLength: { value: 1, message: 'Add at least one attendee.' },
|
|
253
|
-
each: {
|
|
254
|
-
required: true,
|
|
255
|
-
pattern: {
|
|
256
|
-
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
257
|
-
message: 'Enter a valid attendee email.'
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
})
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
Rules outside `each` validate the aggregate value. Rules inside `each` validate members independently. In a bound controller, `css-error` and `aria-invalid="true"` are applied only to the controls whose `each` rules fail; an aggregate or manual field error still marks every control in the field. Public errors remain `Record<string, string[]>`: item messages are flattened under the field name in member order rather than exposing a nested error shape.
|
|
266
|
-
|
|
267
|
-
### Custom validators
|
|
268
|
-
|
|
269
|
-
```ts
|
|
270
|
-
form('checkout-form', {
|
|
271
|
-
validators: {
|
|
272
|
-
card: {
|
|
273
|
-
validate: ({ value, field }) => {
|
|
274
|
-
if (typeof value !== 'string') return `${field} is required.`
|
|
275
|
-
if (!/^\d{16}$/.test(value.replace(/\s+/g, ''))) {
|
|
276
|
-
return 'Card number must be 16 digits.'
|
|
277
|
-
}
|
|
278
|
-
return null
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
})
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
The custom validator receives `{ field, value, values }` and runs after the built-in rules. During `each`, it also receives the optional concrete `element` and zero-based `index`. See [`FieldValidationContext`](typescript.md#fieldvalidationcontext).
|
|
286
|
-
|
|
287
|
-
---
|
|
288
|
-
|
|
289
|
-
## Defaults at a glance
|
|
290
|
-
|
|
291
|
-
```ts
|
|
292
|
-
const DEFAULT_ATTRIBUTES = {
|
|
293
|
-
filled: 'css-filled',
|
|
294
|
-
error: 'css-error'
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const DEFAULT_OPTIONS = {
|
|
298
|
-
autoValidate: true,
|
|
299
|
-
clearErrorsOnSubmit: true,
|
|
300
|
-
clearManualErrorsOnChange: true
|
|
301
|
-
}
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
Everything else (`autoSubmit`, `validators`, `formats`, partial `attributes`) defaults to neutral / empty values.
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## `formats`
|
|
309
|
-
|
|
310
|
-
Declarative way to bind one or more `@samline/formatter` pipelines at construction time. Each entry is applied during `form()` initialization using the same logic as [`format()`](api/format.md).
|
|
311
|
-
|
|
312
|
-
```ts
|
|
313
|
-
import { form } from '@samline/forms'
|
|
314
|
-
|
|
315
|
-
const checkout = form('checkout', {
|
|
316
|
-
formats: {
|
|
317
|
-
phone: { type: 'phone', field: 'phone', options: { country: 'MX' } },
|
|
318
|
-
card: { type: 'creditCard', field: 'card' },
|
|
319
|
-
amount: { type: 'numeral', field: 'amount', options: { prefix: '$' } }
|
|
320
|
-
}
|
|
321
|
-
})
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
The map key is just an identifier — the **canonical** field name lives in `FieldFormatConfig.field`. `format()` renames the visible to `<field>_displayed` (or `config.displayField`) on first run and creates a hidden `<input type="hidden" name="<field>">` that carries the raw value. Both names are first-class in the controller's API. See [`FieldFormatConfigMap`](typescript.md#fieldformatconfig-and-fieldformatconfigmap) for the full type, and the [formatting recipe](recipes.md#13-format-inputs-with-samlineformatter) for an end-to-end example.
|
|
325
|
-
|
|
326
|
-
Module builds require `@samline/formatter` for `formats`. When it is missing, the module instance logs one cached `console.error`, asynchronously rolls back affected visible/mirror pairs, and keeps the controller usable. The standalone global IIFE bundles the formatter.
|