@samline/forms 2.4.0 → 2.7.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 +11 -8
- package/dist/browser/global.d.ts +28 -18
- package/dist/browser/global.global.js +477 -66
- package/dist/browser/global.global.js.map +1 -1
- package/dist/browser/index.cjs +1450 -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-WGPAVJTK.js +1415 -0
- package/dist/chunk-WGPAVJTK.js.map +1 -0
- package/dist/index.cjs +296 -40
- 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 -1148
- package/dist/index.js.map +1 -1
- package/dist/registry-GzopYPCR.d.cts +164 -0
- package/dist/registry-GzopYPCR.d.ts +164 -0
- package/docs/api/add-cleanup.md +52 -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 +80 -0
- package/docs/api/disable-auto-submit.md +65 -0
- package/docs/api/element.md +78 -0
- package/docs/api/form.md +156 -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 +111 -0
- package/docs/api/get-value.md +114 -0
- package/docs/api/index.md +55 -0
- package/docs/api/observe.md +126 -0
- package/docs/api/on-submit.md +94 -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 +61 -0
- package/docs/api/revalidate.md +52 -0
- package/docs/api/set-errors.md +97 -0
- package/docs/api/set-value.md +120 -0
- package/docs/api/subscribe.md +107 -0
- package/docs/api/unwatch.md +74 -0
- package/docs/api/validate-field-value.md +144 -0
- package/docs/api/validate-values.md +103 -0
- package/docs/api/validate.md +102 -0
- package/docs/api/watch.md +117 -0
- package/docs/browser.md +156 -0
- package/docs/css-styling.md +191 -0
- package/docs/getting-started.md +222 -0
- package/docs/options.md +326 -0
- package/docs/recipes.md +530 -0
- package/docs/typescript.md +615 -0
- package/docs/vanilla.md +12 -0
- package/package.json +58 -14
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
Every public method and helper gets its own page. Use this index to navigate to the right one.
|
|
4
|
+
|
|
5
|
+
## Lifecycle
|
|
6
|
+
|
|
7
|
+
- [`form(target, options?)`](form.md) — bind a controller to an `HTMLFormElement`.
|
|
8
|
+
- [`createFormController(target, options?)`](form.md#createformcontroller) — underlying controller factory.
|
|
9
|
+
- [`browser`](../browser.md) — registry-oriented browser API and global build.
|
|
10
|
+
- [`element`](element.md) — the bound form (`f` is an alias).
|
|
11
|
+
- [`addCleanup(cleanup)`](add-cleanup.md) — register controller-owned teardown work.
|
|
12
|
+
- [`destroy()`](destroy.md) — tear down listeners, observer, and caches.
|
|
13
|
+
- [`reset()`](reset.md) — restore native form values and clear errors.
|
|
14
|
+
|
|
15
|
+
## Submission
|
|
16
|
+
|
|
17
|
+
- [`onSubmit(callback, preventDefault?)`](on-submit.md) — register a submit handler.
|
|
18
|
+
- [`autoSubmit(options?)`](auto-submit.md) — enable submit-on-change with optional debounce.
|
|
19
|
+
- [`disableAutoSubmit()`](disable-auto-submit.md) — turn auto-submit off and cancel pending debounce.
|
|
20
|
+
|
|
21
|
+
## Field observation
|
|
22
|
+
|
|
23
|
+
- [`watch(field, callback)`](watch.md) — chainable fire-and-forget reaction to a field.
|
|
24
|
+
- [`observe(field, callback)`](observe.md) — like `watch`, but returns an unsubscribe function and fires immediately.
|
|
25
|
+
- [`unwatch(field?, callback?)`](unwatch.md) — remove watched callbacks.
|
|
26
|
+
- [`subscribe(listener)`](subscribe.md) — react to the entire form state.
|
|
27
|
+
|
|
28
|
+
## Field values
|
|
29
|
+
|
|
30
|
+
- [`setValue(name, value)`](set-value.md) — write a value into a field.
|
|
31
|
+
- [`getValue(name)`](get-value.md) — read the current value of a field.
|
|
32
|
+
- [`getField(name)`](get-field.md) — read the underlying DOM element(s).
|
|
33
|
+
- [`prefill(fieldName?)`](prefill.md) — populate the form from `window.location.search`.
|
|
34
|
+
- [`format(config)`](format.md) — apply `@samline/formatter` to a field, with auto-managed raw mirror and cursor tracking. The visible is renamed to `<field>_displayed` and a hidden `<field>` raw mirror is created. Both names are first-class in the controller's API.
|
|
35
|
+
- [`formatAll(config)`](format.md) — alias of `format()` for `field: string[]` use cases.
|
|
36
|
+
|
|
37
|
+
## Validation
|
|
38
|
+
|
|
39
|
+
- [`validate(fields?)`](validate.md) — run validation, return the result.
|
|
40
|
+
- [`revalidate(fields?)`](revalidate.md) — alias of `validate` with explicit intent.
|
|
41
|
+
- [`setErrors(fields)`](set-errors.md) — push manual errors.
|
|
42
|
+
- [`clearErrors(fields?)`](clear-errors.md) — clear manual errors.
|
|
43
|
+
|
|
44
|
+
## State and data
|
|
45
|
+
|
|
46
|
+
- [`getData()`](get-data.md) — return plain object + `FormData` for the form.
|
|
47
|
+
- [`getState()`](get-state.md) — return a snapshot of values, errors, and metadata.
|
|
48
|
+
- [`append(options)`](append.md) — inject a DOM node into the form.
|
|
49
|
+
|
|
50
|
+
## Pure helpers
|
|
51
|
+
|
|
52
|
+
- [`parseFormData(formElement, submitter?)`](parse-form-data.md) — same serializer used internally, no controller needed.
|
|
53
|
+
- [`validateValues(values, schema)`](validate-values.md) — run a schema against a values map.
|
|
54
|
+
- [`validateFieldValue(field, value, rules, values)`](validate-field-value.md) — run a rule set against a single value.
|
|
55
|
+
- [`regex`](regex.md) — named regular expressions and default error messages exported by the optional peer [`@samline/formatter`](https://github.com/samline/formatter), not by `@samline/forms`.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# `observe(field, callback)`
|
|
2
|
+
|
|
3
|
+
Registers a callback that fires immediately with the current value and on every subsequent change. Returns an unsubscribe function.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
observe(
|
|
9
|
+
field: string,
|
|
10
|
+
callback: FormFieldWatcher
|
|
11
|
+
): () => void
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Parameters
|
|
15
|
+
|
|
16
|
+
| Name | Type | Required | Description |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| `field` | `string` | yes | The `name` attribute of the field to observe. |
|
|
19
|
+
| `callback` | [`FormFieldWatcher`](../typescript.md#formfieldwatcher) | yes | Invoked with `(value, field, form, state)`. Fires immediately with the current value, then on every change. `field` is the observed DOM element(s) (same shape as [`getField`](../typescript.md#formfieldelement--formfieldelement--null) returns). |
|
|
20
|
+
|
|
21
|
+
## Returns
|
|
22
|
+
|
|
23
|
+
A function. Calling it removes the observer for this field.
|
|
24
|
+
|
|
25
|
+
## Behaviour
|
|
26
|
+
|
|
27
|
+
- On registration, the callback is invoked once synchronously with the current value, the bound form, and a state snapshot.
|
|
28
|
+
- Subsequent user `input` events and programmatic [`setValue`](set-value.md) calls invoke the callback again. DOM mutations alone do not fire field watchers.
|
|
29
|
+
- A field can have multiple observers.
|
|
30
|
+
- The unsubscribe function is idempotent — calling it more than once is safe.
|
|
31
|
+
|
|
32
|
+
## Examples
|
|
33
|
+
|
|
34
|
+
### Observe and unsubscribe later
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { form } from '@samline/forms'
|
|
38
|
+
|
|
39
|
+
const profile = form('profile-form')
|
|
40
|
+
|
|
41
|
+
const stop = profile.observe('email', value => {
|
|
42
|
+
console.log('email is now:', value)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Later, when the form is unmounted:
|
|
46
|
+
stop()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Sync a derived model
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { form } from '@samline/forms'
|
|
53
|
+
|
|
54
|
+
const checkout = form('checkout-form')
|
|
55
|
+
|
|
56
|
+
const unsubscribe = checkout.observe('shippingMethod', (value, field, _form, state) => {
|
|
57
|
+
const total = computeTotal(state.values)
|
|
58
|
+
document.querySelector('#total')!.textContent = String(total)
|
|
59
|
+
if (field && 'name' in field) {
|
|
60
|
+
console.log(`updated ${field.name} ->`, value)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
window.addEventListener('beforeunload', unsubscribe)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Share a handler across fields
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { form } from '@samline/forms'
|
|
71
|
+
|
|
72
|
+
const signup = form('signup-form')
|
|
73
|
+
|
|
74
|
+
const track = (value: unknown, field: HTMLInputElement | HTMLInputElement[] | null) => {
|
|
75
|
+
const name = field && 'name' in field ? field.name : null
|
|
76
|
+
analytics.track('signup_field_changed', { field: name, value })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const offEmail = signup.observe('email', track)
|
|
80
|
+
const offPassword = signup.observe('password', track)
|
|
81
|
+
|
|
82
|
+
// Later, on teardown:
|
|
83
|
+
offEmail()
|
|
84
|
+
offPassword()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Manipulate the watched element directly
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { form } from '@samline/forms'
|
|
91
|
+
|
|
92
|
+
const profile = form('profile-form')
|
|
93
|
+
|
|
94
|
+
const stop = profile.observe('avatar', (_value, field) => {
|
|
95
|
+
if (field instanceof HTMLInputElement) {
|
|
96
|
+
field.setAttribute('data-touched', 'true')
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Multiple observers on the same field
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { form } from '@samline/forms'
|
|
105
|
+
|
|
106
|
+
const signup = form('signup-form')
|
|
107
|
+
|
|
108
|
+
const offA = signup.observe('password', value => updateStrength(value))
|
|
109
|
+
const offB = signup.observe('password', value => updateCapsLockWarning(value))
|
|
110
|
+
|
|
111
|
+
// Remove only the strength meter.
|
|
112
|
+
offA()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Edge cases
|
|
116
|
+
|
|
117
|
+
- **The initial fire happens synchronously inside `observe`**. If you do not want this, use [`watch`](watch.md) instead.
|
|
118
|
+
- **Observers persist across calls to [`reset`](reset.md) and [`setValue`](set-value.md)** — they are not affected by state changes.
|
|
119
|
+
- **The unsubscribe function only removes this specific callback.** Other observers on the same field continue to fire.
|
|
120
|
+
|
|
121
|
+
## Related
|
|
122
|
+
|
|
123
|
+
- [`watch`](watch.md) — chainable, no unsubscribe.
|
|
124
|
+
- [`unwatch`](unwatch.md) — bulk removal of watchers.
|
|
125
|
+
- [`subscribe`](subscribe.md) — global state observer.
|
|
126
|
+
- [`format`](format.md) — when a field is formatted, `observe('<field>', cb)` fires with the raw value and `observe('<field>_displayed', cb)` fires with the formatted value. Both fire on the same keystroke. See [the mirror convention](format.md#the-mirror-convention).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# `onSubmit(callback, preventDefault?)`
|
|
2
|
+
|
|
3
|
+
Registers a handler that runs when the form is submitted and validation passes. Multiple handlers can be registered; each runs in registration order.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
onSubmit(
|
|
9
|
+
callback: FormSubmitHandler,
|
|
10
|
+
preventDefault?: boolean
|
|
11
|
+
): FormController
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Parameters
|
|
15
|
+
|
|
16
|
+
| Name | Type | Required | Default | Description |
|
|
17
|
+
| --- | --- | --- | --- | --- |
|
|
18
|
+
| `callback` | [`FormSubmitHandler`](../typescript.md#formsubmithandler) | yes | — | Handler invoked when the form is valid. Receives `(form, data, formData, state)`. |
|
|
19
|
+
| `preventDefault` | `boolean` | no | `true` | Whether to intercept valid submissions. Invalid submissions are always intercepted. |
|
|
20
|
+
|
|
21
|
+
## Returns
|
|
22
|
+
|
|
23
|
+
The same [`FormController`](../typescript.md#formcontroller) — chainable.
|
|
24
|
+
|
|
25
|
+
## Behaviour
|
|
26
|
+
|
|
27
|
+
The submit pipeline runs in this order:
|
|
28
|
+
|
|
29
|
+
1. If `clearErrorsOnSubmit` is `true` (default), manual errors are cleared.
|
|
30
|
+
2. Validation runs for every field with rules. `isValidated` becomes `true`.
|
|
31
|
+
3. If validation fails, the browser’s native submit is prevented, submit handlers are **not** invoked, `aria-invalid="true"` is synchronized, the first connected, enabled, non-hidden invalid control receives focus (including the first failing `each` member), `submitCount` is incremented, and subscribers are notified.
|
|
32
|
+
4. If validation passes:
|
|
33
|
+
- `submitCount` is incremented.
|
|
34
|
+
- Subscribers are notified.
|
|
35
|
+
- If at least one handler was registered with `preventDefault: true`, the native submit is prevented. Otherwise (all handlers `preventDefault: false`), the native submit proceeds.
|
|
36
|
+
- Every registered handler is invoked in registration order. When the event supplies a successful named submit button, its name/value is included in `data` and `formData`.
|
|
37
|
+
- Promise-returning handlers are tracked concurrently. Subscribers receive `isSubmitting: true` while the batch is pending and another update when all promises in it have fulfilled or rejected.
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Default: intercept and send with `fetch`
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { form } from '@samline/forms'
|
|
45
|
+
|
|
46
|
+
const profile = form('profile-form')
|
|
47
|
+
|
|
48
|
+
profile.onSubmit(async (_element, _data, formData) => {
|
|
49
|
+
await fetch('/profile', { method: 'POST', body: formData })
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Pass `false` to allow native submit
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { form } from '@samline/forms'
|
|
57
|
+
|
|
58
|
+
const bladeForm = form('blade-form', {
|
|
59
|
+
validators: { email: { required: true } }
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
bladeForm.onSubmit(() => {
|
|
63
|
+
console.log('client-side hooks ran')
|
|
64
|
+
}, false)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use this when you want the browser to perform the native submit (Blade, ERB, classic Rails, etc.) but still run a client-side hook first.
|
|
68
|
+
|
|
69
|
+
### Multiple handlers
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
profile
|
|
73
|
+
.onSubmit((_form, data) => analytics.track('signup', data))
|
|
74
|
+
.onSubmit(async (_form, _data, formData) => {
|
|
75
|
+
await fetch('/signup', { method: 'POST', body: formData })
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Edge cases
|
|
80
|
+
|
|
81
|
+
- **Handlers receive a fresh `FormData`** built from the live form on each invocation. Do not cache it.
|
|
82
|
+
- **`data` is a plain object mirror of `FormData`.** Repeated names become arrays (e.g. `interests: ['design', 'code']`).
|
|
83
|
+
- **Handlers do not receive errors as an argument.** If you need to inspect the validation result, read [`getState()`](get-state.md) or call [`validate()`](validate.md) from inside the handler.
|
|
84
|
+
- **`onSubmit` cannot be removed** — there is no `offSubmit` API. If you need conditional submission, guard inside the handler or recreate the controller.
|
|
85
|
+
- **Handlers may return `void` or `Promise<void>`.** Invocation still starts synchronously and native submit prevention is decided before promises settle; async resolution does not gate the browser.
|
|
86
|
+
- **Handlers in one submission run concurrently.** The controller invokes them in registration order without awaiting between calls, then tracks all returned promises together.
|
|
87
|
+
- **Rejected promises settle submission state.** They do not become form errors and are consumed by the controller's all-settled tracking, so `isSubmitting` returns to `false` when no other submission work remains.
|
|
88
|
+
- **Overlapping submissions are tracked independently.** `isSubmitting` remains `true` until every pending submission batch has settled.
|
|
89
|
+
|
|
90
|
+
## Related
|
|
91
|
+
|
|
92
|
+
- [`autoSubmit`](auto-submit.md) — submit on every change.
|
|
93
|
+
- [`validate`](validate.md) — run validation without submitting.
|
|
94
|
+
- [`getData`](get-data.md) — what the handler receives.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# `parseFormData(formElement, submitter?)`
|
|
2
|
+
|
|
3
|
+
Pure serializer. Returns a plain object mirror of the form plus a fresh `FormData` instance. Does not require a controller — useful for one-off serialization (e.g. server-side hydration, scripts that read forms outside of a binding).
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
function parseFormData(
|
|
9
|
+
formElement: HTMLFormElement,
|
|
10
|
+
submitter?: HTMLElement | null
|
|
11
|
+
): SerializedFormResult
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
interface SerializedFormResult {
|
|
16
|
+
data: Record<string, SerializedFormValue>
|
|
17
|
+
formData: FormData
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SerializedFormValue = FormDataPrimitive | FormDataPrimitive[]
|
|
21
|
+
type FormDataPrimitive = FormDataEntryValue // string | File
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Parameters
|
|
25
|
+
|
|
26
|
+
| Name | Type | Required | Description |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `formElement` | `HTMLFormElement` | yes | The form to serialize. |
|
|
29
|
+
| `submitter` | `HTMLElement \| null` | no | Successful named submit button whose contribution should be included. |
|
|
30
|
+
|
|
31
|
+
## Returns
|
|
32
|
+
|
|
33
|
+
A [`SerializedFormResult`](../typescript.md#serializedformresult) with two fresh values:
|
|
34
|
+
|
|
35
|
+
- `data` — plain object mirror. Repeated names become arrays.
|
|
36
|
+
- `formData` — a fresh `FormData` instance.
|
|
37
|
+
|
|
38
|
+
## Behaviour
|
|
39
|
+
|
|
40
|
+
- Reads directly from the live DOM via `new FormData(formElement, submitter)` when a submitter is supplied.
|
|
41
|
+
- Preserves fields named `constructor` or `__proto__` as ordinary own properties in `data`.
|
|
42
|
+
- Drops entries where the value is an empty `File` (`size === 0` and `name === ''`).
|
|
43
|
+
- Builds the `data` mirror while iterating, grouping repeated names into arrays.
|
|
44
|
+
- Does not mutate the form or any global state.
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
|
|
48
|
+
### One-off serialization
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { parseFormData } from '@samline/forms'
|
|
52
|
+
|
|
53
|
+
const formElement = document.querySelector<HTMLFormElement>('#contact-form')
|
|
54
|
+
if (formElement) {
|
|
55
|
+
const { data, formData } = parseFormData(formElement)
|
|
56
|
+
console.log(data, formData)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Hydrate from a snapshot
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { parseFormData } from '@samline/forms'
|
|
64
|
+
|
|
65
|
+
const element = document.querySelector<HTMLFormElement>('#profile-form')
|
|
66
|
+
|
|
67
|
+
// Snapshot on every input:
|
|
68
|
+
element?.addEventListener('input', () => {
|
|
69
|
+
if (!element) return
|
|
70
|
+
const { data } = parseFormData(element)
|
|
71
|
+
localStorage.setItem('draft', JSON.stringify(data))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Restore on load:
|
|
75
|
+
const saved = localStorage.getItem('draft')
|
|
76
|
+
if (saved && element) {
|
|
77
|
+
const data = JSON.parse(saved) as Record<string, string>
|
|
78
|
+
for (const [key, value] of Object.entries(data)) {
|
|
79
|
+
const field = element.querySelector<HTMLInputElement>(`[name="${key}"]`)
|
|
80
|
+
if (field) field.value = value
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Edge cases
|
|
86
|
+
|
|
87
|
+
- **Empty file inputs are filtered out.** This matches what [`getData()`](get-data.md) does inside a controller.
|
|
88
|
+
- **`parseFormData` does not validate** — pass the result through [`validateValues`](validate-values.md) if you need validation.
|
|
89
|
+
- **No type coercion.** All values come from `FormData` directly.
|
|
90
|
+
|
|
91
|
+
## Related
|
|
92
|
+
|
|
93
|
+
- [`getData`](get-data.md) — controller-bound equivalent.
|
|
94
|
+
- [`validateValues`](validate-values.md) — run validation against a `FormValues` map.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# `prefill(fieldName?)`
|
|
2
|
+
|
|
3
|
+
Populates the form (or a single field) from the current URL query string. Useful for sharing prefilled links, marketing tracking params, or restoring form state from `location.search`.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
prefill(): FormController
|
|
9
|
+
prefill(fieldName: string): FormController
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Parameters
|
|
13
|
+
|
|
14
|
+
| Name | Type | Required | Description |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| `fieldName` | `string` | no | When provided, only this field is prefilled. When omitted, every query parameter is matched. |
|
|
17
|
+
|
|
18
|
+
## Returns
|
|
19
|
+
|
|
20
|
+
The same [`FormController`](../typescript.md#formcontroller) — chainable.
|
|
21
|
+
|
|
22
|
+
## Behaviour
|
|
23
|
+
|
|
24
|
+
- Reads `window.location.search` once with `URLSearchParams`.
|
|
25
|
+
- For each entry, calls [`setValue`](set-value.md) with the key/value pair.
|
|
26
|
+
- When `fieldName` is provided, only the matching key is written.
|
|
27
|
+
- Values are always strings. If a field expects a different shape (e.g. `number`), the consumer must convert.
|
|
28
|
+
|
|
29
|
+
Because the method delegates to [`setValue`](set-value.md), each prefilled field:
|
|
30
|
+
|
|
31
|
+
- Receives a synthetic `input` event, so watchers and subscribers fire.
|
|
32
|
+
- Triggers validation if it has rules and `autoValidate` is enabled.
|
|
33
|
+
- Triggers autoSubmit if it is enabled.
|
|
34
|
+
- Updates the `css-filled` / `css-error` attributes.
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
### Prefill the whole form
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { form } from '@samline/forms'
|
|
42
|
+
|
|
43
|
+
// URL: https://example.com/contact?utm_source=newsletter&email=hi@example.com
|
|
44
|
+
const contact = form('contact-form')
|
|
45
|
+
contact.prefill()
|
|
46
|
+
|
|
47
|
+
contact.getValue('email') // 'hi@example.com'
|
|
48
|
+
contact.getValue('utm_source') // 'newsletter'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Prefill a single field
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const contact = form('contact-form')
|
|
55
|
+
contact.prefill('email')
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Round-trip a draft through the URL
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { form } from '@samline/forms'
|
|
62
|
+
|
|
63
|
+
const draft = form('draft-form')
|
|
64
|
+
|
|
65
|
+
// User types into fields, watchers debounce-serialize and push to the URL:
|
|
66
|
+
draft.subscribe(state => {
|
|
67
|
+
const params = new URLSearchParams()
|
|
68
|
+
for (const [key, value] of Object.entries(state.values)) {
|
|
69
|
+
if (typeof value === 'string') params.set(key, value)
|
|
70
|
+
}
|
|
71
|
+
history.replaceState(null, '', `?${params.toString()}`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// On next mount:
|
|
75
|
+
draft.prefill()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Edge cases
|
|
79
|
+
|
|
80
|
+
- **`window` must exist.** The browser bundle uses `window.location`; in non-DOM environments `prefill` is a no-op.
|
|
81
|
+
- **Values are always strings.** Prefilling a `<select>` requires the query value to match one of the option values exactly.
|
|
82
|
+
- **Repeated query keys** (`?tag=a&tag=b`) are processed in iteration order. Only the last value wins because `URLSearchParams.forEach` visits both entries; the second `setValue` call overwrites the first.
|
|
83
|
+
- **Prefill runs synchronously.** Call it after the form is in the DOM.
|
|
84
|
+
|
|
85
|
+
## Related
|
|
86
|
+
|
|
87
|
+
- [`setValue`](set-value.md) — underlying primitive.
|
|
88
|
+
- [`getValue`](get-value.md) — read the prefilled values.
|
|
89
|
+
- [`format`](format.md) — when a field is formatted, prefilling the canonical name writes the raw and reformats the visible; prefilling the display name writes the formatted and recomputes the raw. See [the mirror convention](format.md#the-mirror-convention).
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# `regex`
|
|
2
|
+
|
|
3
|
+
> **Heads up — this is a peer dependency.** `regex` is exported by [`@samline/formatter`](https://github.com/samline/formatter), **not** by `@samline/forms`. It only exists when `@samline/formatter` is installed in your project. The forms package does not bundle it, ship it, or declare it as a hard dependency — it is an optional peer. Importing it without installing the peer will throw a module-not-found error at build / runtime, and the page below is only meaningful when the peer is on disk.
|
|
4
|
+
|
|
5
|
+
A named dictionary of common regular expressions and their default error messages. Use it to feed the [`pattern`](../options.md#validators) rule of any field validator without hand-rolling a `RegExp` and a string for every form.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { regex } from '@samline/formatter'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`@samline/formatter` is listed as an optional peer in `package.json`. If you already have it installed for the `format()` / `formatAll()` masks, the import works out of the box. If you do not, install it with:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @samline/formatter
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add @samline/formatter
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun add @samline/formatter
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Signature
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const regex: Record<string, { pattern: RegExp; errorMessage: string }>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Each entry is keyed by a short name (`'email'`, `'url'`, `'phone'`, …) and exposes:
|
|
34
|
+
|
|
35
|
+
| Field | Type | Description |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `pattern` | `RegExp` | The expression to test the value against. |
|
|
38
|
+
| `errorMessage` | `string` | A human-readable message describing the failure. Use it as the default `message` for the `pattern` rule. |
|
|
39
|
+
|
|
40
|
+
The exact key set lives in the `@samline/formatter` package and can grow over time — refer to its [options reference](https://github.com/samline/formatter/blob/main/docs/options.md) for the full list. Common entries include `email`, `url`, `phone`, and `slug`.
|
|
41
|
+
|
|
42
|
+
## Parameters
|
|
43
|
+
|
|
44
|
+
None — `regex` is a plain object, not a function.
|
|
45
|
+
|
|
46
|
+
## Returns
|
|
47
|
+
|
|
48
|
+
A plain `Record<string, { pattern: RegExp; errorMessage: string }>` that you can index by name.
|
|
49
|
+
|
|
50
|
+
## Behaviour
|
|
51
|
+
|
|
52
|
+
`regex` is purely declarative data. The forms package does not call it for you — you opt in by passing `regex.<key>.pattern` (and optionally `regex.<key>.errorMessage`) into a field's `validators` config. The controller's built-in `pattern` rule then runs the expression and pushes the message when the value does not match.
|
|
53
|
+
|
|
54
|
+
`regex` works the same whether you read it directly, destructure the entries you need, or merge it into a project-wide constant. The patterns are owned by `@samline/formatter`; treat the object as read-only.
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
### Validate an email with the built-in pattern
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { form } from '@samline/forms'
|
|
62
|
+
import { regex } from '@samline/formatter'
|
|
63
|
+
|
|
64
|
+
const contact = form('contact-form', {
|
|
65
|
+
validators: {
|
|
66
|
+
email: {
|
|
67
|
+
required: true,
|
|
68
|
+
pattern: regex.email.pattern
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This is the same `pattern` you'd write by hand (`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`); the only difference is that it ships with the formatter peer.
|
|
75
|
+
|
|
76
|
+
### Pair the pattern with the default error message
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { form } from '@samline/forms'
|
|
80
|
+
import { regex } from '@samline/formatter'
|
|
81
|
+
|
|
82
|
+
const signup = form('signup-form', {
|
|
83
|
+
validators: {
|
|
84
|
+
email: {
|
|
85
|
+
required: true,
|
|
86
|
+
pattern: {
|
|
87
|
+
value: regex.email.pattern,
|
|
88
|
+
message: regex.email.errorMessage
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
When the value fails the pattern, the controller pushes `regex.email.errorMessage` into `state.errors.email` and applies the `css-error` attribute to the field — see [Validation](../options.md#validators) and [CSS styling](../css-styling.md).
|
|
96
|
+
|
|
97
|
+
### Use it without a controller
|
|
98
|
+
|
|
99
|
+
The patterns and messages are plain values, so they work the same in any validation pipeline:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { regex } from '@samline/formatter'
|
|
103
|
+
import { validateFieldValue } from '@samline/forms'
|
|
104
|
+
|
|
105
|
+
const errors = validateFieldValue(
|
|
106
|
+
'email',
|
|
107
|
+
'not-an-email',
|
|
108
|
+
{ pattern: regex.email.pattern },
|
|
109
|
+
{}
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// errors = ['<default pattern message, sourced from @samline/formatter>']
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
See [`validateFieldValue`](validate-field-value.md) for the pure helper signature.
|
|
116
|
+
|
|
117
|
+
### Spread `regex` into a browser global alongside `browser`
|
|
118
|
+
|
|
119
|
+
The vanilla entrypoint exposes a `browser` singleton with `form`, `newForm`, `destroyForm`, and `available`. The most common pattern is to spread it together with `regex` into a single project global — see [Browser registry helpers](../getting-started.md#browser-registry-helpers) in the getting-started guide:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { browser } from '@samline/forms'
|
|
123
|
+
import { regex } from '@samline/formatter' // optional, from your project
|
|
124
|
+
|
|
125
|
+
window.Form = { ...browser, regex }
|
|
126
|
+
|
|
127
|
+
window.Form.newForm({
|
|
128
|
+
id: 'contact-form',
|
|
129
|
+
options: {
|
|
130
|
+
validators: {
|
|
131
|
+
email: {
|
|
132
|
+
required: true,
|
|
133
|
+
pattern: regex.email.pattern
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The same shape is available from the IIFE bundle under `window.Forms`; the spread pattern works the same way when the project loads `@samline/forms/browser` instead.
|
|
141
|
+
|
|
142
|
+
## Edge cases
|
|
143
|
+
|
|
144
|
+
- **Missing peer.** `import { regex } from '@samline/formatter'` throws a module-not-found error when the peer is not installed. `@samline/forms` does not catch this — install the peer before you import the symbol.
|
|
145
|
+
- **No types are shipped by `@samline/forms` for `regex`.** The shim in [`src/types/formatter.d.ts`](../../src/types/formatter.d.ts) declares the type as `Record<string, { pattern: RegExp; errorMessage: string }>` for projects that do not yet have the peer on disk, but the real types come from the installed peer at compile time.
|
|
146
|
+
- **`regex` is not enumerated at runtime.** The key set lives in `@samline/formatter`; iterate with `Object.keys(regex)` if you need a dynamic listing.
|
|
147
|
+
- **The controller never reads `regex` for you.** You opt in per field via the `pattern` rule. There is no global switch that turns on `regex.email` for every `email` field.
|
|
148
|
+
- **Browser global key collision.** Spreading `{ ...browser, regex }` into `window.Form` is the documented pattern; assigning `window.Forms.regex` directly is fine too — the registry is shared across spreads.
|
|
149
|
+
|
|
150
|
+
## Related
|
|
151
|
+
|
|
152
|
+
- [`validators`](../options.md#validators) — the option that consumes `regex.<key>.pattern`.
|
|
153
|
+
- [`validateFieldValue`](validate-field-value.md) — the pure helper that runs the `pattern` rule.
|
|
154
|
+
- [`format`](format.md) — also depends on `@samline/formatter` being installed.
|
|
155
|
+
- [`@samline/formatter` options reference](https://github.com/samline/formatter/blob/main/docs/options.md) — the canonical list of `regex` keys.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# `reset()`
|
|
2
|
+
|
|
3
|
+
Restores the form to its initial state: native field values reset, manual and validation errors cleared, visual attributes synchronized, subscribers notified.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
reset(): FormController
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Returns
|
|
12
|
+
|
|
13
|
+
The same [`FormController`](../typescript.md#formcontroller) — chainable.
|
|
14
|
+
|
|
15
|
+
## Behaviour
|
|
16
|
+
|
|
17
|
+
When invoked, the controller:
|
|
18
|
+
|
|
19
|
+
1. Calls `state.element.reset()` (the native form reset).
|
|
20
|
+
2. Clears `state.manualErrors` and `state.validationErrors`.
|
|
21
|
+
3. Restores matching visible/raw defaults for formatted fields.
|
|
22
|
+
4. Removes every `css-filled`, `css-error`, and `aria-invalid` attribute from associated fields.
|
|
23
|
+
5. Re-syncs visual state. Controls whose native default value is non-empty regain `css-filled`, even when `autoValidate` is `false`; errors and `aria-invalid` stay cleared.
|
|
24
|
+
6. Notifies subscribers.
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
### Reset after a successful submit
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { form } from '@samline/forms'
|
|
32
|
+
|
|
33
|
+
const profile = form('profile-form')
|
|
34
|
+
|
|
35
|
+
profile.onSubmit(async (_element, _data, formData) => {
|
|
36
|
+
await fetch('/profile', { method: 'POST', body: formData })
|
|
37
|
+
profile.reset()
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Reset on cancel
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const signup = form('signup-form')
|
|
45
|
+
|
|
46
|
+
document.querySelector('#cancel')?.addEventListener('click', () => {
|
|
47
|
+
signup.reset()
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Edge cases
|
|
52
|
+
|
|
53
|
+
- **Native `reset()` does not dispatch `input` events** for fields whose value changes because of the reset. The controller restores formatted mirrors and visual/ARIA state explicitly.
|
|
54
|
+
- **Submit handlers are not invoked.** `reset()` is purely a UI / state operation.
|
|
55
|
+
- **The `submitCount` from [`getState()`](get-state.md) is not reset.** It tracks the lifetime of the controller, not the lifetime of a single form session.
|
|
56
|
+
- **Pending async submit work is not cancelled.** `isSubmitting` remains true until the outstanding `onSubmit` promises settle.
|
|
57
|
+
|
|
58
|
+
## Related
|
|
59
|
+
|
|
60
|
+
- [`destroy`](destroy.md) — full teardown.
|
|
61
|
+
- [`clearErrors`](clear-errors.md) — clear errors without resetting field values.
|