@samline/forms 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +88 -186
  2. package/dist/browser/global.d.ts +152 -0
  3. package/dist/browser/global.global.js +780 -472
  4. package/dist/browser/global.global.js.map +1 -1
  5. package/dist/index.cjs +755 -468
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +141 -3
  8. package/dist/index.d.ts +141 -3
  9. package/dist/index.js +960 -13
  10. package/dist/index.js.map +1 -1
  11. package/package.json +8 -43
  12. package/dist/chunk-4NEGCRY2.js +0 -125
  13. package/dist/chunk-4NEGCRY2.js.map +0 -1
  14. package/dist/chunk-KIJEFIHP.js +0 -572
  15. package/dist/chunk-KIJEFIHP.js.map +0 -1
  16. package/dist/chunk-KKQD25KI.js +0 -11
  17. package/dist/chunk-KKQD25KI.js.map +0 -1
  18. package/dist/core/index.cjs +0 -154
  19. package/dist/core/index.cjs.map +0 -1
  20. package/dist/core/index.d.cts +0 -11
  21. package/dist/core/index.d.ts +0 -11
  22. package/dist/core/index.js +0 -13
  23. package/dist/core/index.js.map +0 -1
  24. package/dist/react/index.cjs +0 -730
  25. package/dist/react/index.cjs.map +0 -1
  26. package/dist/react/index.d.cts +0 -10
  27. package/dist/react/index.d.ts +0 -10
  28. package/dist/react/index.js +0 -43
  29. package/dist/react/index.js.map +0 -1
  30. package/dist/svelte/index.cjs +0 -760
  31. package/dist/svelte/index.cjs.map +0 -1
  32. package/dist/svelte/index.d.cts +0 -20
  33. package/dist/svelte/index.d.ts +0 -20
  34. package/dist/svelte/index.js +0 -72
  35. package/dist/svelte/index.js.map +0 -1
  36. package/dist/types-C0_1rYlm.d.cts +0 -94
  37. package/dist/types-C0_1rYlm.d.ts +0 -94
  38. package/dist/vanilla/index.cjs +0 -701
  39. package/dist/vanilla/index.cjs.map +0 -1
  40. package/dist/vanilla/index.d.cts +0 -7
  41. package/dist/vanilla/index.d.ts +0 -7
  42. package/dist/vanilla/index.js +0 -12
  43. package/dist/vanilla/index.js.map +0 -1
  44. package/dist/vue/index.cjs +0 -737
  45. package/dist/vue/index.cjs.map +0 -1
  46. package/dist/vue/index.d.cts +0 -13
  47. package/dist/vue/index.d.ts +0 -13
  48. package/dist/vue/index.js +0 -56
  49. package/dist/vue/index.js.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
- # @samline/forms
1
+ # Forms
2
2
 
3
- Form controller for native HTML forms, React, Vue, Svelte, and direct browser usage.
3
+ > A small, framework-free form controller for vanilla JS and direct browser usage.
4
+
5
+ > It binds to an `HTMLFormElement`, keeps field state in sync with the DOM, runs validation, lets you react to changes via watchers or subscribers, and ships a serialized payload for `fetch` flows.
6
+
7
+ ---
4
8
 
5
9
  ## Table of Contents
6
10
 
@@ -8,12 +12,13 @@ Form controller for native HTML forms, React, Vue, Svelte, and direct browser us
8
12
  - [CDN / Browser](#cdn--browser)
9
13
  - [Entrypoints](#entrypoints)
10
14
  - [Quick Start](#quick-start)
11
- - [What You Can Do](#what-you-can-do)
12
- - [API Reference](#api-reference)
13
- - [Examples](#examples)
15
+ - [What You Can Build](#what-you-can-build)
16
+ - [API at a Glance](#api-at-a-glance)
14
17
  - [Documentation](#documentation)
15
18
  - [License](#license)
16
19
 
20
+ ---
21
+
17
22
  ## Installation
18
23
 
19
24
  ```bash
@@ -24,49 +29,58 @@ npm install @samline/forms
24
29
  pnpm add @samline/forms
25
30
  ```
26
31
 
27
- ```bash
28
- yarn add @samline/forms
29
- ```
30
-
31
32
  ```bash
32
33
  bun add @samline/forms
33
34
  ```
34
35
 
36
+ Requires Node 20+ when bundling. Runtime target is ES2020.
37
+
38
+ ---
39
+
35
40
  ## CDN / Browser
36
41
 
37
- Use the browser build when you do not have a bundler and need to run the package directly in HTML.
42
+ Use the browser build when you do not have a bundler and need to run the package directly in HTML, Shopify, WordPress, or any traditional template.
38
43
 
39
44
  ```html
40
- <script src="https://unpkg.com/@samline/forms@1.0.2/dist/browser/global.global.js"></script>
45
+ <script src="https://unpkg.com/@samline/forms@2.0.0/dist/browser/global.global.js"></script>
41
46
  ```
42
47
 
43
- Pin the version in production.
48
+ > Pin the version in production. Replace `2.0.0` with the version you ship.
44
49
 
45
- The browser build exposes `window.forms`.
50
+ The browser bundle exposes a single global: `window.Forms`.
46
51
 
47
52
  ```html
48
53
  <form id="contact-form">
49
54
  <input name="email" type="email" />
55
+ <button type="submit">Send</button>
50
56
  </form>
51
57
 
52
- <script src="https://unpkg.com/@samline/forms@1.0.2/dist/browser/global.global.js"></script>
58
+ <script src="https://unpkg.com/@samline/forms@2.0.0/dist/browser/global.global.js"></script>
53
59
  <script>
54
- const contactForm = window.forms.form('contact-form')
60
+ const contactForm = window.Forms.newForm({ id: 'contact-form' })
61
+
62
+ contactForm.onSubmit(async (form, data, formData) => {
63
+ await fetch('/api/contact', { method: 'POST', body: formData })
64
+ })
65
+
55
66
  contactForm.validate()
56
67
  </script>
57
68
  ```
58
69
 
70
+ The browser surface keeps a small registry under `Forms.available`, keyed by the `id` you pass to `Forms.newForm`. Each successful `newForm` call stores the returned controller there, and `Forms.destroyForm(id)` calls `destroy()` and removes the entry. Use `Forms.form` directly when you need the factory without the registry side-effect.
71
+
72
+ See [docs/browser.md](docs/browser.md) for the full browser surface.
73
+
74
+ ---
75
+
59
76
  ## Entrypoints
60
77
 
61
- | Entrypoint | Use |
78
+ | Entrypoint | When to use |
62
79
  | --- | --- |
63
- | `@samline/forms` | Main vanilla API |
64
- | `@samline/forms/core` | Types, serialization, and validation |
65
- | `@samline/forms/vanilla` | Explicit DOM API entrypoint |
66
- | `@samline/forms/react` | React hook |
67
- | `@samline/forms/vue` | Vue composable |
68
- | `@samline/forms/svelte` | Svelte store and action |
69
- | `@samline/forms/browser` | Browser global bundle |
80
+ | `@samline/forms` | Main vanilla API for bundlers, ESM, or CJS consumers. |
81
+ | `@samline/forms/browser` | Pre-bundled IIFE that registers `window.Forms` for direct `<script>` usage. |
82
+
83
+ ---
70
84
 
71
85
  ## Quick Start
72
86
 
@@ -82,193 +96,81 @@ const contactForm = form('contact-form', {
82
96
  }
83
97
  })
84
98
 
85
- contactForm.onSubmit((element, data, formData, state) => {
86
- console.log(element, data, formData, state)
99
+ contactForm.watch('email', value => {
100
+ console.log('email is now:', value)
87
101
  })
88
- ```
89
-
90
- ## What You Can Do
91
-
92
- - bind to a form by id, element, or ref-like target
93
- - read and write field values
94
- - serialize form values to both a plain object and `FormData`
95
- - watch individual fields and subscribe to global form state
96
- - prefill values from the current URL query string
97
- - mark filled and error states through DOM attributes for styling
98
- - run built-in validation rules and custom validators
99
- - trigger submit handlers with optional auto-submit behavior
100
- - reset, inspect, and destroy the controller cleanly
101
-
102
- ## API Reference
103
-
104
- ### form(target, options)
105
-
106
- Creates a controller from:
107
-
108
- - a form id string
109
- - a real `HTMLFormElement`
110
- - a ref-like object with `current`
111
-
112
- ### Properties
113
-
114
- - `element`: the bound `HTMLFormElement | null`
115
- - `f`: alias of `element`
116
- - `options`: normalized controller options
117
-
118
- ### Submission
119
-
120
- - `onSubmit(callback, preventDefault?)`
121
- - `autoSubmit(options?)`
122
- - `disableAutoSubmit()`
123
-
124
- `onSubmit` accepts an optional second argument named `preventDefault`.
125
-
126
- - `onSubmit(callback)` is equivalent to `onSubmit(callback, true)`
127
- - with `true`, valid submissions are intercepted, which is the right choice for `fetch` or AJAX flows
128
- - with `false`, valid submissions continue with the browser's native form submit behavior
129
- - invalid submissions are still prevented, even when you pass `false`
130
-
131
- ### Field observation
132
-
133
- - `watch(field, callback)`
134
- - `observe(field, callback)`
135
- - `subscribe(listener)`
136
- - `unwatch(field?, callback?)`
137
-
138
- ### Field values
139
-
140
- - `setValue(name, value)`
141
- - `getValue(name)`
142
- - `getField(name)`
143
- - `prefill(fieldName?)`
144
-
145
- ### Validation and errors
146
-
147
- - `validate(fields?)`
148
- - `revalidate(fields?)`
149
- - `setErrors(fields)`
150
- - `clearErrors(fields?)`
151
-
152
- ### Form lifecycle and state
153
-
154
- - `getData()`
155
- - `getState()`
156
- - `append(options)`
157
- - `reset()`
158
- - `destroy()`
159
-
160
- ### Validation options
161
-
162
- Built-in rules supported through `validators`:
163
-
164
- - `required`
165
- - `minLength`
166
- - `maxLength`
167
- - `pattern`
168
- - `validate` for custom callbacks
169
102
 
170
- ## Examples
171
-
172
- ### Bind by id
173
-
174
- ```ts
175
- import { form } from '@samline/forms'
176
-
177
- const profileForm = form('profile-form')
178
- ```
179
-
180
- ### Bind by element
181
-
182
- ```ts
183
- const element = document.querySelector('#profile-form') as HTMLFormElement
184
- const profileForm = form(element)
185
- ```
186
-
187
- ### Watch a field
188
-
189
- ```ts
190
- const profileForm = form('profile-form')
191
-
192
- profileForm.watch('email', value => {
193
- console.log('email changed:', value)
103
+ contactForm.onSubmit(async (_element, _data, formData) => {
104
+ await fetch('/api/contact', { method: 'POST', body: formData })
194
105
  })
195
106
  ```
196
107
 
197
- ### Validate with built-in rules
108
+ What this does:
198
109
 
199
- ```ts
200
- const profileForm = form('profile-form', {
201
- validators: {
202
- email: {
203
- required: true,
204
- pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
205
- },
206
- password: {
207
- minLength: 8
208
- }
209
- }
210
- })
110
+ - Binds to the form with id `contact-form`.
111
+ - Adds `css-filled` / `css-error` attributes on fields so you can style them with CSS.
112
+ - Validates `email` on every change and on submit.
113
+ - Intercepts valid submits (the default for `onSubmit`) and hands a real `FormData` instance to your handler.
211
114
 
212
- const result = profileForm.validate()
213
- console.log(result.isValid, result.errors)
214
- ```
115
+ ---
215
116
 
216
- ### Submit with fetch
117
+ ## What You Can Build
217
118
 
218
- ```ts
219
- const profileForm = form('profile-form')
119
+ - Contact, newsletter, login, signup, checkout, and profile forms.
120
+ - Forms that submit with `fetch` while keeping native `FormData` payloads.
121
+ - Autosave / autosubmit flows with optional debounce.
122
+ - Visual feedback driven by `css-filled` and `css-error` attributes.
123
+ - Progressive enhancement on top of any existing HTML form.
124
+ - Forms rendered server-side (Blade, Twig, ERB) that still want client-side validation.
220
125
 
221
- profileForm.onSubmit(async (_element, _data, formData) => {
222
- await fetch('/api/profile', {
223
- method: 'POST',
224
- body: formData
225
- })
226
- })
227
- ```
126
+ ---
228
127
 
229
- This uses the default `preventDefault = true`, so the package intercepts the valid submit and lets you handle the request yourself.
128
+ ## API at a Glance
230
129
 
231
- ### Submit with native form behavior
130
+ The controller is built around one factory and a small set of focused methods. Most methods are chainable.
232
131
 
233
- ```ts
234
- const profileForm = form('profile-form')
132
+ | Group | Methods |
133
+ | --- | --- |
134
+ | Lifecycle | [`form`](docs/api/form.md) · [`destroy`](docs/api/destroy.md) · [`reset`](docs/api/reset.md) |
135
+ | Properties | [`element`](docs/api/element.md) · [`options`](docs/options.md) |
136
+ | Submission | [`onSubmit`](docs/api/on-submit.md) · [`autoSubmit`](docs/api/auto-submit.md) · [`disableAutoSubmit`](docs/api/disable-auto-submit.md) |
137
+ | Field observation | [`watch`](docs/api/watch.md) · [`observe`](docs/api/observe.md) · [`unwatch`](docs/api/unwatch.md) · [`subscribe`](docs/api/subscribe.md) |
138
+ | Field values | [`setValue`](docs/api/set-value.md) · [`getValue`](docs/api/get-value.md) · [`getField`](docs/api/get-field.md) · [`prefill`](docs/api/prefill.md) · [`format`](docs/api/format.md) · [`formatAll`](docs/api/format.md) |
139
+ | Validation | [`validate`](docs/api/validate.md) · [`revalidate`](docs/api/revalidate.md) · [`setErrors`](docs/api/set-errors.md) · [`clearErrors`](docs/api/clear-errors.md) |
140
+ | State and data | [`getData`](docs/api/get-data.md) · [`getState`](docs/api/get-state.md) · [`append`](docs/api/append.md) |
141
+ | Pure helpers | [`parseFormData`](docs/api/parse-form-data.md) · [`validateValues`](docs/api/validate-values.md) · [`validateFieldValue`](docs/api/validate-field-value.md) |
235
142
 
236
- profileForm.onSubmit(() => {
237
- console.log('validation passed')
238
- }, false)
239
- ```
143
+ See the full per-method reference in [`docs/api/`](docs/api/index.md).
240
144
 
241
- Use `false` when the form should continue with its normal HTML submission after validation succeeds, for example in server-rendered applications such as Laravel with Blade.
145
+ ---
242
146
 
243
- If validation fails, the package still prevents the submit.
147
+ ## Optional peer: `@samline/formatter`
244
148
 
245
- ### Prefill from the URL
149
+ [`format`](docs/api/format.md) and [`formatAll`](docs/api/format.md) rely on the optional peer dependency [`@samline/formatter`](https://github.com/samline/formatter). Install it when you need input masks (phone, credit-card, date, time, numeral, general):
246
150
 
247
- ```ts
248
- const profileForm = form('profile-form')
249
- profileForm.prefill()
151
+ ```bash
152
+ npm install @samline/formatter
250
153
  ```
251
154
 
252
- ### Work with form state
155
+ If the peer is not installed, the methods log a single `console.error` explaining how to install it and return the controller unchanged — the rest of the form keeps working. See [docs/recipes.md → 13. Format inputs with `@samline/formatter`](docs/recipes.md#13-format-inputs-with-samlineformatter) for end-to-end examples.
253
156
 
254
- ```ts
255
- const profileForm = form('profile-form')
256
-
257
- const unsubscribe = profileForm.subscribe(state => {
258
- console.log(state.values)
259
- console.log(state.errors)
260
- })
261
-
262
- unsubscribe()
263
- ```
157
+ ---
264
158
 
265
159
  ## Documentation
266
160
 
267
- - [docs/vanilla.md](docs/vanilla.md)
268
- - [docs/react.md](docs/react.md)
269
- - [docs/vue.md](docs/vue.md)
270
- - [docs/svelte.md](docs/svelte.md)
271
- - [docs/browser.md](docs/browser.md)
161
+ Full API reference, guides, and examples are available at **[samline.github.io/forms](https://samline.github.io/forms)**.
162
+
163
+ | Doc | Purpose |
164
+ | --- | --- |
165
+ | [docs/getting-started.md](docs/getting-started.md) | Concepts, observable contract, lifecycle, and side-effect overview. |
166
+ | [docs/options.md](docs/options.md) | Full `FormControllerOptions` reference. |
167
+ | [docs/css-styling.md](docs/css-styling.md) | `css-filled` and `css-error` styling recipes. |
168
+ | [docs/typescript.md](docs/typescript.md) | Every exported TypeScript type, with examples. |
169
+ | [docs/api/index.md](docs/api/index.md) | One page per public method. |
170
+ | [docs/recipes.md](docs/recipes.md) | End-to-end patterns: fetch submit, server errors, autosave, multi-step, etc. |
171
+ | [docs/browser.md](docs/browser.md) | Browser global (`window.Forms`) usage. |
172
+
173
+ ---
272
174
 
273
175
  ## License
274
176
 
@@ -0,0 +1,152 @@
1
+ type FormatType = 'general' | 'phone' | 'numeral' | 'date' | 'time' | 'creditCard' | 'creditCardType';
2
+ interface FieldFormatConfig {
3
+ /** One of the supported `@samline/formatter` `FormatType` values. */
4
+ type: FormatType;
5
+ /** Name of the visible input(s) inside the bound form. */
6
+ field: string | string[];
7
+ /**
8
+ * Optional name for the hidden raw mirror. Defaults to
9
+ * `${fieldName}Raw`. The mirror is created on first `format()`
10
+ * call and reused on subsequent calls.
11
+ */
12
+ rawField?: string;
13
+ /**
14
+ * Format-specific options forwarded to `@samline/formatter`.
15
+ * Any key documented for the chosen `type` is accepted (e.g.
16
+ * `country`, `delimiter`, `numeralDecimalMark`, `datePattern`).
17
+ */
18
+ options?: Record<string, unknown>;
19
+ }
20
+ type FieldFormatConfigMap = Record<string, FieldFormatConfig>;
21
+ type FormDataPrimitive = FormDataEntryValue;
22
+ type SerializedFormValue = FormDataPrimitive | FormDataPrimitive[];
23
+ type FormFieldValue = string | string[] | File[] | undefined;
24
+ type FormValues = Record<string, FormFieldValue>;
25
+ type FormErrors = Record<string, string[]>;
26
+ type RuleConfig<T> = T | {
27
+ value: T;
28
+ message?: string;
29
+ };
30
+ interface FieldValidationContext {
31
+ field: string;
32
+ value: FormFieldValue;
33
+ values: FormValues;
34
+ }
35
+ type FieldValidator = (context: FieldValidationContext) => string | undefined | null | false | true;
36
+ interface FieldValidationRules {
37
+ required?: RuleConfig<boolean>;
38
+ minLength?: RuleConfig<number>;
39
+ maxLength?: RuleConfig<number>;
40
+ pattern?: RuleConfig<RegExp>;
41
+ validate?: FieldValidator | FieldValidator[];
42
+ }
43
+ type ValidationSchema = Record<string, FieldValidationRules>;
44
+ interface VisualAttributes {
45
+ filled: string;
46
+ error: string;
47
+ }
48
+ interface AutoSubmitOptions {
49
+ debounce?: number;
50
+ }
51
+ interface FormControllerOptions {
52
+ attributes?: Partial<VisualAttributes>;
53
+ autoValidate?: boolean;
54
+ autoSubmit?: boolean | AutoSubmitOptions;
55
+ clearManualErrorsOnChange?: boolean;
56
+ clearErrorsOnSubmit?: boolean;
57
+ validators?: ValidationSchema;
58
+ /**
59
+ * Declarative format configuration. Each entry is applied during
60
+ * `form()` initialization using the same logic as `controller.format(...)`.
61
+ * The key is just an identifier — the visible field name lives in
62
+ * `FieldFormatConfig.field`.
63
+ */
64
+ formats?: FieldFormatConfigMap;
65
+ }
66
+ interface SerializedFormResult {
67
+ data: Record<string, SerializedFormValue>;
68
+ formData: FormData;
69
+ }
70
+ interface ValidationResult {
71
+ isValid: boolean;
72
+ errors: FormErrors;
73
+ }
74
+ interface FormStateSnapshot {
75
+ values: FormValues;
76
+ errors: FormErrors;
77
+ filledFields: string[];
78
+ isValid: boolean;
79
+ isValidated: boolean;
80
+ autoSubmit: boolean;
81
+ submitCount: number;
82
+ }
83
+ type FormStateListener = (state: FormStateSnapshot) => void;
84
+ type FormFieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
85
+ type FormTarget = string | HTMLFormElement | {
86
+ current: HTMLFormElement | null;
87
+ } | null | undefined;
88
+ interface AppendContentOptions {
89
+ tag: keyof HTMLElementTagNameMap;
90
+ content: string;
91
+ class?: string;
92
+ atStart?: boolean;
93
+ }
94
+ type FormFieldWatcher = (value: FormFieldValue, field: FormFieldElement | FormFieldElement[] | null, form: HTMLFormElement, state: FormStateSnapshot) => void;
95
+ type FormSubmitHandler = (form: HTMLFormElement, data: Record<string, SerializedFormValue>, formData: FormData, state: FormStateSnapshot) => void;
96
+ interface FormController {
97
+ readonly element: HTMLFormElement | null;
98
+ readonly f: HTMLFormElement | null;
99
+ readonly options: FormControllerOptions;
100
+ onSubmit: (callback: FormSubmitHandler, preventDefault?: boolean) => FormController;
101
+ watch: (field: string, callback: FormFieldWatcher) => FormController;
102
+ observe: (field: string, callback: FormFieldWatcher) => () => void;
103
+ unwatch: (field?: string, callback?: FormFieldWatcher) => FormController;
104
+ subscribe: (listener: FormStateListener) => () => void;
105
+ prefill: (fieldName?: string) => FormController;
106
+ append: (options: AppendContentOptions) => HTMLElement | null;
107
+ setErrors: (fields: string[] | FormErrors) => FormController;
108
+ clearErrors: (fields?: string[]) => FormController;
109
+ setValue: (name: string, value: unknown) => FormController;
110
+ validate: (fields?: string[]) => ValidationResult;
111
+ revalidate: (fields?: string[]) => ValidationResult;
112
+ /**
113
+ * Apply the same `@samline/formatter` pipeline to a single field or
114
+ * to a list of fields sharing one configuration. Chainable.
115
+ *
116
+ * Requires the optional peer dependency `@samline/formatter`. When
117
+ * the peer is not installed the call logs a single `console.error`
118
+ * and returns the controller unchanged.
119
+ */
120
+ format: (config: FieldFormatConfig) => FormController;
121
+ formatAll: (config: FieldFormatConfig) => FormController;
122
+ reset: () => FormController;
123
+ autoSubmit: (options?: boolean | AutoSubmitOptions) => FormController;
124
+ disableAutoSubmit: () => FormController;
125
+ getValue: (name: string) => FormFieldValue;
126
+ getField: (name: string) => FormFieldElement | FormFieldElement[] | null;
127
+ getData: () => SerializedFormResult;
128
+ getState: () => FormStateSnapshot;
129
+ destroy: () => void;
130
+ }
131
+
132
+ interface NewFormInput {
133
+ id: string;
134
+ options?: FormControllerOptions;
135
+ }
136
+ interface FormsAvailable {
137
+ [id: string]: FormController;
138
+ }
139
+ interface FormsApi {
140
+ form: (target: FormTarget, options?: FormControllerOptions) => FormController;
141
+ newForm: (input: NewFormInput) => FormController | undefined;
142
+ destroyForm: (id: string) => void;
143
+ available: FormsAvailable;
144
+ }
145
+ declare const Forms: FormsApi;
146
+ declare global {
147
+ interface Window {
148
+ Forms: FormsApi;
149
+ }
150
+ }
151
+
152
+ export { type FormsApi, type FormsAvailable, type NewFormInput, Forms as default };