@samline/forms 2.4.0 → 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 +319 -49
- 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 +134 -23
- 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-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 +58 -14
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# `append(options)`
|
|
2
|
+
|
|
3
|
+
Inserts a DOM node into the bound form. Useful for rendering banners, hints, or summary blocks that should live inside the `<form>` element so they are reset together with the form.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
append(options: AppendContentOptions): HTMLElement | null
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
interface AppendContentOptions {
|
|
13
|
+
tag: keyof HTMLElementTagNameMap
|
|
14
|
+
content: string
|
|
15
|
+
class?: string
|
|
16
|
+
atStart?: boolean
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Parameters
|
|
21
|
+
|
|
22
|
+
| Name | Type | Required | Default | Description |
|
|
23
|
+
| --- | --- | --- | --- | --- |
|
|
24
|
+
| `options.tag` | `keyof HTMLElementTagNameMap` | yes | — | The HTML tag to create (e.g. `'div'`, `'p'`, `'output'`). |
|
|
25
|
+
| `options.content` | `string` | yes | — | `innerHTML` content for the new node. |
|
|
26
|
+
| `options.class` | `string` | no | — | Class name. If a node with the same first class already exists inside the form, it is removed first. |
|
|
27
|
+
| `options.atStart` | `boolean` | no | `false` | When `true`, the node is inserted at the start of the form. When `false`, it is appended at the end. |
|
|
28
|
+
|
|
29
|
+
## Returns
|
|
30
|
+
|
|
31
|
+
The created `HTMLElement`, or `null` when the controller has no bound form.
|
|
32
|
+
|
|
33
|
+
## Behaviour
|
|
34
|
+
|
|
35
|
+
- Clears the field cache (because the form subtree changed).
|
|
36
|
+
- If `class` is provided and a descendant of the form matches the first class in the string, that descendant is removed before the new node is inserted. This gives you a single “render or replace” pattern.
|
|
37
|
+
- The created node receives `innerHTML = content`. The content is trusted HTML; escape it yourself if it comes from user input.
|
|
38
|
+
|
|
39
|
+
## Examples
|
|
40
|
+
|
|
41
|
+
### Render a static hint
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { form } from '@samline/forms'
|
|
45
|
+
|
|
46
|
+
const signup = form('signup-form')
|
|
47
|
+
|
|
48
|
+
signup.append({
|
|
49
|
+
tag: 'p',
|
|
50
|
+
content: 'We will never share your email.',
|
|
51
|
+
class: 'signup-hint'
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Render a live region for validation messages
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const profile = form('profile-form')
|
|
59
|
+
|
|
60
|
+
profile.append({
|
|
61
|
+
tag: 'output',
|
|
62
|
+
content: '',
|
|
63
|
+
class: 'profile-status',
|
|
64
|
+
atStart: true
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
profile.subscribe(state => {
|
|
68
|
+
const status = profile.element?.querySelector('.profile-status')
|
|
69
|
+
if (status) status.textContent = state.isValid ? 'Looks good' : 'Please fix errors'
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Replace a previous banner
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { form } from '@samline/forms'
|
|
77
|
+
|
|
78
|
+
const contact = form('contact-form')
|
|
79
|
+
|
|
80
|
+
contact.append({ tag: 'div', content: 'Draft saved.', class: 'banner banner-info' })
|
|
81
|
+
|
|
82
|
+
// Some time later — replaces the previous banner instead of stacking.
|
|
83
|
+
contact.append({ tag: 'div', content: 'Draft published.', class: 'banner banner-success' })
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Edge cases
|
|
87
|
+
|
|
88
|
+
- **Only the first class token is used for deduplication.** If you pass `'a b'`, the controller searches for `.a` inside the form and removes it. The other classes are still applied to the new node.
|
|
89
|
+
- **The returned element is a live reference.** It stays in the DOM until you remove it or until [`reset`](reset.md) is called (which strips `css-filled` / `css-error` attributes but does not remove nodes you added).
|
|
90
|
+
- **Appending does not trigger validation, watchers, or autoSubmit.** It only mutates the DOM.
|
|
91
|
+
- **Calling `append` from a non-form controller** returns `null` and does nothing.
|
|
92
|
+
|
|
93
|
+
## Related
|
|
94
|
+
|
|
95
|
+
- [`reset`](reset.md) — strips visual attributes but not custom nodes.
|
|
96
|
+
- [`destroy`](destroy.md) — removes listeners and observer but does not remove custom nodes.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# `autoSubmit(options?)`
|
|
2
|
+
|
|
3
|
+
Enables native auto-submit. Every change to a tracked field triggers `form.requestSubmit()`. Optionally debounced.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
autoSubmit(options?: boolean | AutoSubmitOptions): FormController
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Parameters
|
|
12
|
+
|
|
13
|
+
| Name | Type | Required | Default | Description |
|
|
14
|
+
| --- | --- | --- | --- | --- |
|
|
15
|
+
| `options` | `boolean \| AutoSubmitOptions` | no | `true` | Pass `true` to enable, `false` to disable, or an `{ debounce }` object to delay. |
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface AutoSubmitOptions {
|
|
19
|
+
debounce?: number
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Returns
|
|
24
|
+
|
|
25
|
+
The same [`FormController`](../typescript.md#formcontroller) — chainable.
|
|
26
|
+
|
|
27
|
+
## Behaviour
|
|
28
|
+
|
|
29
|
+
- `autoSubmit()` or `autoSubmit(true)` — enables auto-submit immediately on every change.
|
|
30
|
+
- `autoSubmit({ debounce: ms })` — schedules `form.requestSubmit()` `ms` milliseconds after the most recent change. Subsequent changes within the debounce window reset the timer.
|
|
31
|
+
- `autoSubmit(false)` — disables auto-submit (equivalent to [`disableAutoSubmit()`](disable-auto-submit.md)).
|
|
32
|
+
|
|
33
|
+
When auto-submit fires, the native submit is invoked. If the form is invalid, the controller’s `submit` listener prevents the browser from navigating. If the form is valid, the registered [`onSubmit`](on-submit.md) handlers run as usual.
|
|
34
|
+
|
|
35
|
+
Subscribers are notified when autoSubmit is enabled, disabled, or its debounce changes (because the state’s `autoSubmit` field changes).
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
### Autosave on every change
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { form } from '@samline/forms'
|
|
43
|
+
|
|
44
|
+
const draft = form('draft-form', {
|
|
45
|
+
validators: { title: { required: true } }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
draft.autoSubmit()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Debounced search
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { form } from '@samline/forms'
|
|
55
|
+
|
|
56
|
+
const search = form('search-form', {
|
|
57
|
+
autoSubmit: { debounce: 300 }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
search.onSubmit(async (_form, data) => {
|
|
61
|
+
await fetch('/search', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
body: JSON.stringify(data)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Toggle at runtime
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const formEl = form('search-form')
|
|
73
|
+
|
|
74
|
+
document.querySelector('#realtime')?.addEventListener('change', event => {
|
|
75
|
+
const enabled = (event.target as HTMLInputElement).checked
|
|
76
|
+
formEl.autoSubmit(enabled ? { debounce: 250 } : false)
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Edge cases
|
|
81
|
+
|
|
82
|
+
- **The submit is invoked via `form.requestSubmit()`** when available, falling back to clicking a submit button or `form.submit()` otherwise.
|
|
83
|
+
- **`autoSubmit` does not validate.** Validation runs as part of the submit pipeline, so an invalid form will not navigate and your [`onSubmit`](on-submit.md) handler will not run.
|
|
84
|
+
- **The debounce timer is cancelled by [`disableAutoSubmit()`](disable-auto-submit.md) and [`destroy()`](destroy.md).**
|
|
85
|
+
- **Setting `autoSubmit` from `options` and from the method can both run at mount time** — the method is called last, so it wins.
|
|
86
|
+
|
|
87
|
+
## Related
|
|
88
|
+
|
|
89
|
+
- [`disableAutoSubmit`](disable-auto-submit.md) — turn auto-submit off.
|
|
90
|
+
- [`onSubmit`](on-submit.md) — submit handlers.
|
|
91
|
+
- [`options.autoSubmit`](../options.md#autosubmit) — mount-time configuration.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# `clearErrors(fields?)`
|
|
2
|
+
|
|
3
|
+
Removes manual errors. Validation errors from rules are not touched.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
clearErrors(): FormController
|
|
9
|
+
clearErrors(fields: string[]): FormController
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Parameters
|
|
13
|
+
|
|
14
|
+
| Name | Type | Required | Description |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| `fields` | `string[]` | no | When provided, only the listed fields are cleared. When omitted, every manual error is cleared. |
|
|
17
|
+
|
|
18
|
+
## Returns
|
|
19
|
+
|
|
20
|
+
The same [`FormController`](../typescript.md#formcontroller) — chainable.
|
|
21
|
+
|
|
22
|
+
## Behaviour
|
|
23
|
+
|
|
24
|
+
- Only manual errors (set via [`setErrors`](set-errors.md)) are removed. Validation errors from rules are recomputed the next time validation runs.
|
|
25
|
+
- Visual attributes are re-synced for the affected fields — `css-error` is removed when no errors remain for a field.
|
|
26
|
+
- Subscribers are notified.
|
|
27
|
+
|
|
28
|
+
## Examples
|
|
29
|
+
|
|
30
|
+
### Clear a single field
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { form } from '@samline/forms'
|
|
34
|
+
|
|
35
|
+
const profile = form('profile-form')
|
|
36
|
+
|
|
37
|
+
profile.setErrors({ email: ['Already in use.'] })
|
|
38
|
+
|
|
39
|
+
profile.clearErrors(['email'])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Clear everything
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
profile.setErrors({
|
|
46
|
+
email: ['Already in use.'],
|
|
47
|
+
password: ['Too weak.']
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
profile.clearErrors()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Clear before re-running validation
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
profile.clearErrors()
|
|
57
|
+
profile.validate()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Edge cases
|
|
61
|
+
|
|
62
|
+
- **Validation errors are not cleared by `clearErrors`.** They live in a separate map and are only cleared by [`reset`](reset.md) or by a successful re-validation of the field.
|
|
63
|
+
- **`clearErrors` on a field with no manual error is a no-op** — the chain still returns the controller.
|
|
64
|
+
- **Passing an empty array `clearErrors([])` does nothing.** Use `clearErrors()` to clear everything.
|
|
65
|
+
|
|
66
|
+
## Related
|
|
67
|
+
|
|
68
|
+
- [`setErrors`](set-errors.md) — push manual errors.
|
|
69
|
+
- [`reset`](reset.md) — clear every error and reset field values.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# `destroy()`
|
|
2
|
+
|
|
3
|
+
Tears the controller down. Removes all DOM listeners, disconnects the `MutationObserver`, clears caches, drops subscribers and submit handlers, and resets internal state.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
destroy(): void
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Returns
|
|
12
|
+
|
|
13
|
+
`void`. Idempotent — calling `destroy()` more than once is a no-op.
|
|
14
|
+
|
|
15
|
+
## Behaviour
|
|
16
|
+
|
|
17
|
+
When invoked, the controller:
|
|
18
|
+
|
|
19
|
+
- Removes every listener it added (`input`, `submit`, formatter listeners, and document delegation for external controls).
|
|
20
|
+
- Cancels any pending auto-submit debounce timer.
|
|
21
|
+
- Disconnects the form `MutationObserver`.
|
|
22
|
+
- Clears `watchedFields`, `subscribers`, `submitHandlers`, and `fieldCache`.
|
|
23
|
+
- Resets manual and validation error maps.
|
|
24
|
+
|
|
25
|
+
After `destroy()` the controller object still exists and its methods remain callable. Direct reads and writes can still inspect or mutate the bound DOM, but the removed controller listeners no longer react to resulting events.
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
### SPA route unmount
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { form } from '@samline/forms'
|
|
33
|
+
|
|
34
|
+
let profile: ReturnType<typeof form> | null = null
|
|
35
|
+
|
|
36
|
+
function mountProfileForm(element: HTMLFormElement) {
|
|
37
|
+
profile = form(element, { validators: { email: { required: true } } })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unmountProfileForm() {
|
|
41
|
+
profile?.destroy()
|
|
42
|
+
profile = null
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Cleanup on script teardown
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const profile = form('profile-form')
|
|
50
|
+
|
|
51
|
+
window.addEventListener('beforeunload', () => {
|
|
52
|
+
profile.destroy()
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Edge cases
|
|
57
|
+
|
|
58
|
+
- **`destroy()` does not remove `css-filled` / `css-error` attributes** from fields. If you need that, call [`reset()`](reset.md) before destroy, or strip the attributes yourself.
|
|
59
|
+
- **Subscribers and watchers stop receiving notifications immediately**, but their closures remain until you release them.
|
|
60
|
+
- **Reusing the controller reference after destroy is possible but usually misleading.** Reads still inspect the bound form and methods such as `setValue()` can still write fields, but watchers, subscribers, validation-on-input, and auto-submit no longer run.
|
|
61
|
+
|
|
62
|
+
## Related
|
|
63
|
+
|
|
64
|
+
- [`reset`](reset.md) — restore form values without removing listeners.
|
|
65
|
+
- [`form`](form.md) — bind a new controller.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# `disableAutoSubmit()`
|
|
2
|
+
|
|
3
|
+
Turns auto-submit off and cancels any pending debounce timer.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
disableAutoSubmit(): FormController
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Returns
|
|
12
|
+
|
|
13
|
+
The same [`FormController`](../typescript.md#formcontroller) — chainable.
|
|
14
|
+
|
|
15
|
+
## Behaviour
|
|
16
|
+
|
|
17
|
+
- Sets `state.autoSubmitEnabled` to `false`.
|
|
18
|
+
- Cancels `state.autoSubmitTimer` if one is pending.
|
|
19
|
+
- Notifies subscribers (because the state’s `autoSubmit` field changes).
|
|
20
|
+
|
|
21
|
+
Equivalent to calling [`autoSubmit(false)`](auto-submit.md).
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
### Pause autosave while typing resumes
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { form } from '@samline/forms'
|
|
29
|
+
|
|
30
|
+
const draft = form('draft-form', { autoSubmit: { debounce: 800 } })
|
|
31
|
+
|
|
32
|
+
document.querySelector('#pause')?.addEventListener('click', () => {
|
|
33
|
+
draft.disableAutoSubmit()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
document.querySelector('#resume')?.addEventListener('click', () => {
|
|
37
|
+
draft.autoSubmit({ debounce: 800 })
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Disable on blur
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { form } from '@samline/forms'
|
|
45
|
+
|
|
46
|
+
const search = form('search-form', { autoSubmit: { debounce: 250 } })
|
|
47
|
+
|
|
48
|
+
search.element?.addEventListener('focusin', () => {
|
|
49
|
+
search.autoSubmit({ debounce: 250 })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
search.element?.addEventListener('focusout', () => {
|
|
53
|
+
search.disableAutoSubmit()
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Edge cases
|
|
58
|
+
|
|
59
|
+
- **Re-enabling later requires calling [`autoSubmit()`](auto-submit.md) again.** The previous debounce configuration is not preserved.
|
|
60
|
+
- **A pending debounce timer is removed**, but the timer was already scheduled; nothing fires for it.
|
|
61
|
+
|
|
62
|
+
## Related
|
|
63
|
+
|
|
64
|
+
- [`autoSubmit`](auto-submit.md) — enable / configure.
|
|
65
|
+
- [`destroy`](destroy.md) — also cancels any pending debounce.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# `element` and `f`
|
|
2
|
+
|
|
3
|
+
Read-only getters that return the bound `HTMLFormElement`, or `null` if the binding target was unresolved at construction time.
|
|
4
|
+
|
|
5
|
+
## Signatures
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
interface FormController {
|
|
9
|
+
readonly element: HTMLFormElement | null
|
|
10
|
+
readonly f: HTMLFormElement | null
|
|
11
|
+
readonly options: FormControllerOptions
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- `element` — the bound form.
|
|
16
|
+
- `f` — alias of `element`, kept short for fluent setup.
|
|
17
|
+
- `options` — the resolved options object (defaults merged with user-provided values).
|
|
18
|
+
|
|
19
|
+
## Returns
|
|
20
|
+
|
|
21
|
+
`HTMLFormElement | null`. `null` when the target passed to [`form()`](form.md) did not resolve to a form.
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
### Read the bound form
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { form } from '@samline/forms'
|
|
29
|
+
|
|
30
|
+
const profile = form('profile-form')
|
|
31
|
+
|
|
32
|
+
if (profile.element) {
|
|
33
|
+
profile.element.classList.add('mounted')
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Use the short alias
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { form } from '@samline/forms'
|
|
41
|
+
|
|
42
|
+
const profile = form('profile-form')
|
|
43
|
+
|
|
44
|
+
profile.f?.querySelector('[data-status]')?.setAttribute('data-state', 'ready')
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Read the resolved options
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { form } from '@samline/forms'
|
|
51
|
+
|
|
52
|
+
const profile = form('profile-form', {
|
|
53
|
+
autoValidate: false,
|
|
54
|
+
attributes: { error: 'is-invalid' },
|
|
55
|
+
formats: {
|
|
56
|
+
phone: { type: 'phone', field: 'phone', options: { country: 'MX' } }
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
profile.options.autoValidate // false (overridden)
|
|
61
|
+
profile.options.autoSubmit // false (default)
|
|
62
|
+
profile.options.clearErrorsOnSubmit // true (default)
|
|
63
|
+
profile.options.attributes.error // 'is-invalid'
|
|
64
|
+
profile.options.attributes.filled // 'css-filled' (default)
|
|
65
|
+
profile.options.formats.phone // FieldFormatConfig (overridden)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The `formats` key is the declarative counterpart to [`format()`](format.md) — see [the option reference](../options.md#formats) for the full shape.
|
|
69
|
+
|
|
70
|
+
## Edge cases
|
|
71
|
+
|
|
72
|
+
- The getter returns `null` when the target was `null`, `undefined`, a non-existent id, or an element that is not an `HTMLFormElement`.
|
|
73
|
+
- The reference is the same one the controller uses internally — do not replace the form node, or the controller will keep listening to the original.
|
|
74
|
+
|
|
75
|
+
## Related
|
|
76
|
+
|
|
77
|
+
- [`form(target, options?)`](form.md) — bind a controller.
|
|
78
|
+
- [`destroy`](destroy.md) — tear the controller down.
|
package/docs/api/form.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# `form(target, options?)`
|
|
2
|
+
|
|
3
|
+
Creates a new controller bound to a form. This is the main entry point of `@samline/forms`.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
function form(
|
|
9
|
+
target: FormTarget,
|
|
10
|
+
options?: FormControllerOptions
|
|
11
|
+
): FormController
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Parameters
|
|
15
|
+
|
|
16
|
+
| Name | Type | Required | Description |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| `target` | [`FormTarget`](../typescript.md#formtarget) | yes | What to bind to: a string id, an `HTMLFormElement`, a ref-like `{ current }` object, or `null` / `undefined`. |
|
|
19
|
+
| `options` | [`FormControllerOptions`](../typescript.md#formcontrolleroptions) | no | Controller configuration. See [docs/options.md](../options.md). |
|
|
20
|
+
|
|
21
|
+
## Returns
|
|
22
|
+
|
|
23
|
+
A [`FormController`](../typescript.md#formcontroller) — chainable for every method that does not return data or an unsubscribe function.
|
|
24
|
+
|
|
25
|
+
## Binding modes
|
|
26
|
+
|
|
27
|
+
### By id string
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { form } from '@samline/forms'
|
|
31
|
+
|
|
32
|
+
const profile = form('profile-form')
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The string is passed to `document.getElementById`. If no element is found, or the found element is not an `HTMLFormElement`, the controller is created but `controller.element` is `null` and most methods become no-ops.
|
|
36
|
+
|
|
37
|
+
### By `HTMLFormElement`
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const element = document.querySelector<HTMLFormElement>('#profile-form')
|
|
41
|
+
const profile = form(element)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Useful when you already have the element from a `querySelector` call.
|
|
45
|
+
|
|
46
|
+
### By ref-like object
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const ref = { current: document.querySelector<HTMLFormElement>('#profile-form') }
|
|
50
|
+
const profile = form(ref)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The controller reads `target.current` only at construction time. If you mount the form later, recreate the controller or call `destroy()` and re-bind.
|
|
54
|
+
|
|
55
|
+
### By `null` / `undefined`
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const profile = form()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Allowed for type-system ergonomics. The resulting controller has `element === null` and is effectively inert.
|
|
62
|
+
|
|
63
|
+
## Behaviour
|
|
64
|
+
|
|
65
|
+
On creation the controller:
|
|
66
|
+
|
|
67
|
+
1. Resolves the form element.
|
|
68
|
+
2. Merges the provided options with defaults (`autoValidate: true`, `clearErrorsOnSubmit: true`, `clearManualErrorsOnChange: true`, `attributes: { filled: 'css-filled', error: 'css-error' }`).
|
|
69
|
+
3. Wires delegated `input` and `submit` listeners, including controls associated through `form="id"`.
|
|
70
|
+
4. Starts a `MutationObserver` on the form subtree to track dynamic fields.
|
|
71
|
+
5. Optionally enables [`autoSubmit`](../api/auto-submit.md) when `options.autoSubmit` is truthy.
|
|
72
|
+
6. Runs an initial validation pass if `autoValidate` is enabled.
|
|
73
|
+
7. Notifies subscribers with the initial state.
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
### Minimal setup
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { form } from '@samline/forms'
|
|
81
|
+
|
|
82
|
+
const contact = form('contact-form')
|
|
83
|
+
|
|
84
|
+
contact.onSubmit(async (_element, _data, formData) => {
|
|
85
|
+
await fetch('/api/contact', { method: 'POST', body: formData })
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### With validators and options
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { form } from '@samline/forms'
|
|
93
|
+
|
|
94
|
+
const signup = form('signup-form', {
|
|
95
|
+
attributes: { filled: 'is-filled', error: 'is-invalid' },
|
|
96
|
+
autoValidate: true,
|
|
97
|
+
autoSubmit: { debounce: 600 },
|
|
98
|
+
clearManualErrorsOnChange: false,
|
|
99
|
+
validators: {
|
|
100
|
+
email: {
|
|
101
|
+
required: { value: true, message: 'Email is required.' },
|
|
102
|
+
pattern: {
|
|
103
|
+
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
104
|
+
message: 'Enter a valid email address.'
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
password: { required: true, minLength: 8 }
|
|
108
|
+
},
|
|
109
|
+
formats: {
|
|
110
|
+
phone: { type: 'phone', field: 'phone', options: { country: 'MX' } }
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Bind later via ref
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { form } from '@samline/forms'
|
|
119
|
+
|
|
120
|
+
const profileRef = { current: null as HTMLFormElement | null }
|
|
121
|
+
|
|
122
|
+
const profile = form(profileRef)
|
|
123
|
+
|
|
124
|
+
document.querySelector('#mount')?.appendChild(document.createElement('form'))
|
|
125
|
+
profileRef.current = document.querySelector<HTMLFormElement>('#mount form')
|
|
126
|
+
|
|
127
|
+
// Re-bind because the controller captured `current` at construction time.
|
|
128
|
+
profile.destroy()
|
|
129
|
+
form(profileRef)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Edge cases
|
|
133
|
+
|
|
134
|
+
- **The id is resolved once.** If the DOM element with that id is replaced, the controller keeps listening to the original element. Recreate the controller if you re-render the form.
|
|
135
|
+
- **`null` / `undefined` targets** are accepted and produce an inert controller. The chainable methods still return the controller, so they are safe to call as no-ops.
|
|
136
|
+
- **Reusing a target across multiple controllers** is supported but they each install their own listeners — call [`destroy()`](destroy.md) on the previous one to avoid duplicate work.
|
|
137
|
+
- **`createFormController` is also exported** as the lower-level factory; prefer `form` for ergonomic imports.
|
|
138
|
+
|
|
139
|
+
## `createFormController`
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
function createFormController(
|
|
143
|
+
target: FormTarget,
|
|
144
|
+
options?: FormControllerOptions
|
|
145
|
+
): FormController
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
This is the factory used by `form()` and accepts the same arguments. It is exported for integrations that prefer an explicit constructor-style name.
|
|
149
|
+
|
|
150
|
+
## Related
|
|
151
|
+
|
|
152
|
+
- [`FormController`](../typescript.md#formcontroller) — the returned controller type.
|
|
153
|
+
- [`destroy`](destroy.md) — tear the controller down.
|
|
154
|
+
- [docs/options.md](../options.md) — every option in detail.
|