form-father 0.6.0 → 0.7.1

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/demos/main.js DELETED
@@ -1,95 +0,0 @@
1
- const api = window.FormFather || {};
2
- const Form = api.default || api.Form || api;
3
- const { createLengthValidator, registerValidator, sameAsField } = api;
4
- const output = document.querySelector('#demo-output');
5
-
6
- function writeOutput(title, payload) {
7
- output.textContent = `${title}\n\n${JSON.stringify(payload, null, 2)}`;
8
- }
9
-
10
- function json(body, status = 200) {
11
- return new Response(JSON.stringify(body), {
12
- status,
13
- headers: { 'Content-Type': 'application/json' },
14
- });
15
- }
16
-
17
- window.fetch = async (url, options = {}) => {
18
- const requestUrl = String(url);
19
- const method = options.method || 'GET';
20
-
21
- if (requestUrl.startsWith('/demo/login')) {
22
- return json({
23
- success: true,
24
- message: 'Logged in',
25
- method,
26
- });
27
- }
28
-
29
- if (requestUrl.startsWith('/demo/callback')) {
30
- return json(
31
- {
32
- success: false,
33
- error: true,
34
- 'error-msg': 'Сервер вернул ошибки по полям.',
35
- errors: [{ name: 'tel', 'error-msg': 'Этот номер уже есть в заявках' }],
36
- },
37
- 422,
38
- );
39
- }
40
-
41
- if (requestUrl.startsWith('/demo/search')) {
42
- return json({
43
- success: true,
44
- query: requestUrl,
45
- results: ['Validation API', 'Server response format', 'Demo recipes'],
46
- });
47
- }
48
-
49
- if (requestUrl.startsWith('/demo/upload')) {
50
- return json({
51
- success: true,
52
- message: 'FormData accepted',
53
- contentType: options.headers?.['Content-Type'] || 'browser-managed multipart boundary',
54
- });
55
- }
56
-
57
- return json({ success: false, error: true, 'error-msg': `No demo route for ${requestUrl}` }, 404);
58
- };
59
-
60
- registerValidator('min-length', createLengthValidator({ min: 6 }), 'Слишком короткое значение', { override: true });
61
- registerValidator(
62
- 'available-email',
63
- value =>
64
- new Promise(resolve => {
65
- setTimeout(() => resolve(value.toLowerCase() !== 'taken@example.com'), 350);
66
- }),
67
- 'Email is already used',
68
- { override: true },
69
- );
70
-
71
- Form.initAll('form[data-form-father]', {
72
- inputSelector: '.input',
73
- inputWrapperSelector: '.field',
74
- validateOn: ['blur', 'change'],
75
- revalidateOn: ['input', 'change'],
76
- validationDebounce: 120,
77
- focusFirstErroredInput: true,
78
- loaderColor: 'currentColor',
79
- errorSummary: {
80
- title: 'Проверьте поля формы',
81
- focus: true,
82
- },
83
- formValidators: sameAsField('passwordConfirm', 'password', 'Пароли не совпадают'),
84
- onValidationError(errors) {
85
- writeOutput('Client validation errors', errors);
86
- },
87
- onResponse(responseBody, form) {
88
- writeOutput(`${form.$el.dataset.demo || 'form'} response`, responseBody);
89
- },
90
- onResponseSuccess(responseBody, form) {
91
- if (form.$el.dataset.demo === 'login' || form.$el.dataset.demo === 'upload') {
92
- form.clearInputs();
93
- }
94
- },
95
- });
package/demos/styles.css DELETED
@@ -1,261 +0,0 @@
1
- :root {
2
- color-scheme: light;
3
- --bg: #f5f7fb;
4
- --panel: #ffffff;
5
- --text: #172033;
6
- --muted: #637083;
7
- --line: #d9e0ea;
8
- --accent: #176b87;
9
- --accent-strong: #0f5268;
10
- --danger: #b3261e;
11
- --success: #176b3a;
12
- }
13
-
14
- * {
15
- box-sizing: border-box;
16
- }
17
-
18
- body {
19
- margin: 0;
20
- background: var(--bg);
21
- color: var(--text);
22
- font: 16px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
23
- }
24
-
25
- .demo-header {
26
- display: flex;
27
- align-items: flex-end;
28
- justify-content: space-between;
29
- gap: 24px;
30
- padding: 40px min(6vw, 72px) 24px;
31
- border-bottom: 1px solid var(--line);
32
- background: #ffffff;
33
- }
34
-
35
- .demo-header h1,
36
- .panel-title h2 {
37
- margin: 0;
38
- letter-spacing: 0;
39
- }
40
-
41
- .demo-header h1 {
42
- max-width: 760px;
43
- font-size: clamp(32px, 4vw, 54px);
44
- line-height: 1.05;
45
- }
46
-
47
- .eyebrow {
48
- margin: 0 0 8px;
49
- color: var(--accent);
50
- font-size: 13px;
51
- font-weight: 700;
52
- letter-spacing: 0;
53
- text-transform: uppercase;
54
- }
55
-
56
- .github-link,
57
- button {
58
- display: inline-flex;
59
- align-items: center;
60
- justify-content: center;
61
- min-height: 42px;
62
- padding: 0 16px;
63
- border: 1px solid var(--accent);
64
- border-radius: 6px;
65
- background: var(--accent);
66
- color: #ffffff;
67
- font: inherit;
68
- font-weight: 700;
69
- text-decoration: none;
70
- cursor: pointer;
71
- }
72
-
73
- .github-link {
74
- background: #ffffff;
75
- color: var(--accent);
76
- }
77
-
78
- button:hover,
79
- button:focus-visible {
80
- background: var(--accent-strong);
81
- border-color: var(--accent-strong);
82
- }
83
-
84
- .github-link:hover,
85
- .github-link:focus-visible {
86
- border-color: var(--accent-strong);
87
- color: var(--accent-strong);
88
- }
89
-
90
- .demo-grid {
91
- display: grid;
92
- grid-template-columns: repeat(2, minmax(0, 1fr));
93
- gap: 18px;
94
- padding: 24px min(6vw, 72px);
95
- }
96
-
97
- .demo-panel,
98
- .demo-output {
99
- border: 1px solid var(--line);
100
- border-radius: 8px;
101
- background: var(--panel);
102
- padding: 22px;
103
- }
104
-
105
- .panel-title {
106
- margin-bottom: 18px;
107
- }
108
-
109
- .panel-title h2 {
110
- font-size: 22px;
111
- line-height: 1.2;
112
- }
113
-
114
- form {
115
- display: grid;
116
- gap: 14px;
117
- }
118
-
119
- [data-form-father-summary]:not([hidden]) {
120
- display: grid;
121
- gap: 8px;
122
- padding: 12px;
123
- border: 1px solid rgba(179, 38, 30, 0.28);
124
- border-radius: 6px;
125
- background: rgba(179, 38, 30, 0.06);
126
- color: var(--danger);
127
- font-size: 14px;
128
- }
129
-
130
- [data-form-father-summary-title] {
131
- margin: 0;
132
- font-weight: 700;
133
- }
134
-
135
- [data-form-father-summary-list] {
136
- display: grid;
137
- gap: 4px;
138
- margin: 0;
139
- padding-left: 18px;
140
- }
141
-
142
- [data-form-father-summary] button {
143
- min-height: 0;
144
- padding: 0;
145
- border: 0;
146
- background: transparent;
147
- color: inherit;
148
- font-weight: 700;
149
- text-align: left;
150
- text-decoration: underline;
151
- }
152
-
153
- [data-form-father-summary] button:hover,
154
- [data-form-father-summary] button:focus-visible {
155
- background: transparent;
156
- color: var(--accent-strong);
157
- }
158
-
159
- .field {
160
- display: grid;
161
- gap: 6px;
162
- }
163
-
164
- .field span {
165
- color: var(--muted);
166
- font-size: 14px;
167
- font-weight: 700;
168
- }
169
-
170
- .input {
171
- width: 100%;
172
- min-height: 42px;
173
- padding: 8px 10px;
174
- border: 1px solid var(--line);
175
- border-radius: 6px;
176
- background: #ffffff;
177
- color: var(--text);
178
- font: inherit;
179
- }
180
-
181
- .input:focus {
182
- border-color: var(--accent);
183
- outline: 3px solid rgba(23, 107, 135, 0.16);
184
- }
185
-
186
- .input[aria-invalid="true"] {
187
- border-color: var(--danger);
188
- }
189
-
190
- .input[aria-busy="true"] {
191
- opacity: 0.72;
192
- }
193
-
194
- .input[data-form-father-state="valid"] {
195
- border-color: var(--success);
196
- }
197
-
198
- .input[data-form-father-state="invalid"] {
199
- border-color: var(--danger);
200
- }
201
-
202
- [data-form-father-error],
203
- .field-error {
204
- color: var(--danger);
205
- font-size: 13px;
206
- }
207
-
208
- .input__wrapper--error .input {
209
- border-color: var(--danger);
210
- }
211
-
212
- .button--loading {
213
- opacity: 0.74;
214
- pointer-events: none;
215
- }
216
-
217
- .button__loader {
218
- display: inline-flex;
219
- width: 18px;
220
- height: 18px;
221
- margin-right: 8px;
222
- }
223
-
224
- .button__loader svg {
225
- width: 18px;
226
- height: 18px;
227
- }
228
-
229
- .error-block-under-input__wrapper {
230
- color: var(--danger);
231
- font-size: 14px;
232
- }
233
-
234
- .demo-output {
235
- margin: 0 min(6vw, 72px) 40px;
236
- }
237
-
238
- pre {
239
- min-height: 130px;
240
- margin: 0;
241
- overflow: auto;
242
- padding: 16px;
243
- border: 1px solid var(--line);
244
- border-radius: 8px;
245
- background: #101820;
246
- color: #d8f3dc;
247
- font-size: 13px;
248
- white-space: pre-wrap;
249
- }
250
-
251
- @media (max-width: 760px) {
252
- .demo-header {
253
- display: grid;
254
- align-items: start;
255
- padding-top: 28px;
256
- }
257
-
258
- .demo-grid {
259
- grid-template-columns: 1fr;
260
- }
261
- }
package/docs/en/README.md DELETED
@@ -1,266 +0,0 @@
1
- # Form Father
2
-
3
- [![npm version](https://img.shields.io/npm/v/form-father)](https://www.npmjs.com/package/form-father)
4
- [![npm downloads](https://img.shields.io/npm/dm/form-father)](https://www.npmjs.com/package/form-father)
5
-
6
- [🇷🇺 Документация на русском](https://github.com/Poliklot/form-father/blob/master/README.md)
7
-
8
- **Form Father** is a library for handling forms in pure JavaScript, providing convenient validation and form submission
9
- with TypeScript support.
10
-
11
- ## Installation
12
-
13
- Install the library via npm:
14
-
15
- ```bash
16
- npm install form-father
17
- ```
18
-
19
- ## Usage
20
-
21
- ```javascript
22
- import Form, { isUrlValid, serializeToFormData } from 'form-father';
23
-
24
- Form.setDefaultParams({
25
- showLoaderButton: false,
26
- scrollToFirstErroredInput: false,
27
- logging: true,
28
- });
29
-
30
- const formElement = document.querySelector('#myForm');
31
- const options = {
32
- onSubmit: formInstance => {
33
- // Actions on form submission
34
- },
35
- onResponse: (responseBody, formInstance) => {
36
- // Actions on receiving server response
37
- },
38
- // Other options...
39
- };
40
-
41
- const form = new Form(formElement, options);
42
- const simpleForm = new Form(document.querySelector('#simpleForm')); // options are optional
43
-
44
- const forms = Form.initAll('form[data-form-father]', {
45
- inputWrapperSelector: '.field',
46
- validateOn: ['blur', 'change'],
47
- revalidateOn: ['input', 'change'],
48
- });
49
- ```
50
-
51
- ```ts
52
- import Form, {
53
- FORM_ERROR_FIELD,
54
- type FormOptions,
55
- type ValidationSchema,
56
- type ResponseBody,
57
- type ValidationError,
58
- type SubmitResult,
59
- type FormResetOptions,
60
- type FormValidator,
61
- type ValidationIssue,
62
- type FormValidatorPredicate,
63
- type ErrorSummaryOptions,
64
- } from 'form-father';
65
- ```
66
-
67
- ## Options
68
-
69
- - **onSubmit**: Callback function executed on form submission.
70
- - **onBeforeSubmit**: Runs before sending an already valid form; returning `false` cancels the request.
71
- - **onSubmitError**: Runs when form submission throws.
72
- - **onResponse**: Callback function executed when a server response is received.
73
- - **onResponseSuccess**: Callback function executed for a successful HTTP response with `success: true`.
74
- - **onResponseUnsuccess**: Callback function executed for unsuccessful HTTP responses, `success !== true`, or invalid JSON.
75
- - **onValidationError**: Runs when client-side validation fails.
76
- - **showLoaderButton**: Whether to display a loader in the submit button. Defaults to `true`.
77
- - **scrollToFirstErroredInput**: Whether to scroll to the first input with an error. Defaults to `true`.
78
- - **focusFirstErroredInput**: Whether to focus the first invalid field. Defaults to `false`.
79
- - **customTypeError**: Custom error type.
80
- - **loaderColor**: Color of the loader in the submit button.
81
- - **logging**: Specifies whether to log data to the console. Defaults to false.
82
- - **validateOn**: Field live-validation events: `submit`, `input`, `blur`, `change`.
83
- - **revalidateOn**: Events used to re-check already invalid fields. Defaults to `input` and `change`.
84
- - **validationDebounce**: Live-validation delay in milliseconds.
85
- - **errorContainerAttribute**: Attribute containing the CSS selector for a custom error container. Defaults to
86
- `data-error-container`.
87
- - **validationStateAttribute**: Field state attribute: `validating`, `valid`, or `invalid`. Defaults to
88
- `data-form-father-state`.
89
- - **ariaDescribeErrors**: Links inline error text to the field with `aria-describedby`. Defaults to `true`.
90
- - **errorIdPrefix**: Prefix for automatically generated error element ids. Defaults to `form-father-error`.
91
- - **errorSummary**: Form error summary: `true` for the default renderer or `{ selector, title, focus, render }`.
92
- - **observeMutations**: Watches dynamically added fields and submit buttons.
93
- - **formValidators**: A form validator or array of validators for cross-field rules.
94
-
95
- ## Form Submission
96
-
97
- - Submit elements are `button[type="submit"]`, `button` without `type`, `input[type="submit"]`, and
98
- `input[type="image"]`.
99
- - `submit()` is public and returns `Promise<SubmitResult | undefined>`.
100
- - For `method="GET"` and `method="HEAD"`, data is appended to the `action` query string and no request body is sent.
101
- - `wrapData` applies to every supported `enctype`: `application/x-www-form-urlencoded`, `multipart/form-data`,
102
- `text/plain`, and `application/json`.
103
- - `onResponseSuccess` runs only for a successful HTTP response with `success: true`.
104
- - `onResponseUnsuccess` runs for unsuccessful HTTP responses, `success !== true`, or invalid JSON responses.
105
-
106
- ## Methods
107
-
108
- - **Form.initAll(selector, options)**: Initializes all forms matching a selector and reuses existing instances.
109
- - **updateOptions(options)**: Updates one form instance and rebinds live validation.
110
- - **validate()**: Validates the whole form.
111
- - **validateField(field)**: Validates one field by name or DOM element.
112
- - **showFieldError(field, message, source)**: Shows a field error manually.
113
- - **setErrors(errors, source)**: Applies backend/form-level errors from an object, array, or `ErrorResponse[]`.
114
- - **getErrors()**: Returns current errors as `{ field, rule, message, source }`.
115
- - **getValues()**: Returns form values as a plain object.
116
- - **setValues(values)**: Fills fields by `name` and dispatches `input`/`change`.
117
- - **clearErrors()**: Clears errors without changing field values.
118
- - **reset(options)**: Calls native `form.reset()` and clears errors by default.
119
- - **clearInputs()**: Clears all input fields in the form.
120
- - **setDefaultParams(params):** The setDefaultParams method is used to set default values for all instances of the form. These parameters can be overridden when initializing a specific form.
121
- - **destroy()**: Removes event listeners and runtime artifacts.
122
-
123
- ## Validation
124
-
125
- ```html
126
- <input
127
- name="email"
128
- type="email"
129
- data-validate="required|email"
130
- data-error-required="Email is required"
131
- data-error-email="Enter a valid email"
132
- data-error-container="#email-error"
133
- />
134
- <small id="email-error" hidden></small>
135
- ```
136
-
137
- - `data-validate` accepts rules separated by `|`, spaces, or commas.
138
- - `data-error-rule-name` overrides a message for one rule.
139
- - `data-error-container` points to a custom error container without requiring wrapper markup.
140
- - During async validation, fields receive `aria-busy="true"` and a `validating` state attribute; stale validator
141
- responses are ignored.
142
- - Inline errors receive `role="alert"` and are linked to fields with `aria-describedby`.
143
- - `data-custom-validate` is still supported for backward compatibility.
144
-
145
- ### Error summary and a11y
146
-
147
- ```html
148
- <form data-form-father novalidate>
149
- <div data-form-father-summary hidden></div>
150
- <label>
151
- Email
152
- <input class="input" name="email" data-validate="required|email" />
153
- </label>
154
- <button type="submit">Submit</button>
155
- </form>
156
- ```
157
-
158
- ```ts
159
- new Form($form, {
160
- inputWrapperSelector: 'label',
161
- errorSummary: {
162
- title: 'Please check the form',
163
- focus: true,
164
- },
165
- });
166
- ```
167
-
168
- - If the form already contains `[data-form-father-summary]`, Form Father uses it; otherwise `errorSummary: true` creates
169
- a summary at the start of the form.
170
- - `selector` sends the summary into your own container.
171
- - `render(errors, form)` lets you fully replace the summary markup.
172
- - `ariaDescribeErrors: false` disables automatic error id insertion into `aria-describedby`.
173
-
174
- Final rule order:
175
-
176
- ```text
177
- required → schema.rules → data-validate → data-custom-validate
178
- ```
179
-
180
- ### Cross-field and form-level validation
181
-
182
- ```ts
183
- import { dateOrder, requiredIf, sameAsField } from 'form-father';
184
-
185
- const form = new Form($form, {
186
- formValidators: [
187
- sameAsField('passwordConfirm', 'password', 'Passwords do not match'),
188
- requiredIf('email', ({ values }) => Boolean(values.subscribe), 'Email is required for subscription'),
189
- dateOrder('start', 'end', 'End date must be after start'),
190
- ],
191
- });
192
- ```
193
-
194
- Return a string or an issue without `field` to show a form-level error. In `getErrors()`, it uses
195
- `FORM_ERROR_FIELD` (`"_form"`).
196
-
197
- ```ts
198
- form.setErrors({
199
- email: 'Email is already used',
200
- [FORM_ERROR_FIELD]: 'Please check the form',
201
- });
202
- ```
203
-
204
- ### Rule DX helpers
205
-
206
- ```ts
207
- import { createLengthValidator, createPatternValidator, registerFieldValidator } from 'form-father';
208
-
209
- registerFieldValidator('starts-with-a', value => value.startsWith('A'), 'Must start with A');
210
- registerValidator('slug', createPatternValidator(/^[a-z0-9-]+$/), 'Slug characters only');
211
- registerValidator('short-name', createLengthValidator({ min: 2, max: 24 }), 'Use 2-24 characters');
212
- ```
213
-
214
- ## Schema Adapters
215
-
216
- Form Father has no schema-library runtime dependency, but can wrap Zod/Valibot/Yup-like schemas with `safeParse()` or
217
- `parse()`.
218
-
219
- ```ts
220
- import { registerSchemaValidator } from 'form-father';
221
-
222
- registerSchemaValidator('company-email', z.string().email(), 'Use a company email');
223
- ```
224
-
225
- For simple checks, use `createFieldValidator(predicate, message)`.
226
-
227
- ## Demos and Recipes
228
-
229
- - `demos/index.html` is a static login/callback/search/upload demo after `npm run build`.
230
- - `npm run demos` builds the package and starts the Vite demo server.
231
- - `docs/recipes/README.md` contains short API, data-attribute, adapter, and server-error recipes.
232
-
233
- ## Helpers
234
-
235
- The library provides a number of utility functions:
236
-
237
- - **serializeToFormData($element)**: Serializes form data into a `FormData` object.
238
- - **isEmailValid(value)**: Checks if a string is a valid email address.
239
- - **isUrlValid(value)**: Checks an `http(s)` URL, domain, IP, or `localhost`; the scheme may be omitted.
240
- - **isPhoneValid(value)**: Checks if a string is a valid phone number.
241
- - **closest($el, selector)**: Finds the closest parent element matching the given selector.
242
- - **blockScrollBody()**: Blocks page scrolling.
243
- - **unblockScrollBody()**: Unblocks page scrolling.
244
- - **parseCommonResponseProperties(responseBody)**: Processes common properties from the server response.
245
- - **serializeFormToJSON(form)**: Serializes form data into a plain object.
246
-
247
- ## Build and Development
248
-
249
- **Rollup** is used for building the project. Main commands:
250
-
251
- - **npm run build**: Builds the project.
252
- - **npm run watch**: Builds the project in watch mode.
253
- - **npm run demos**: Runs a demo version using Vite.
254
-
255
- ## Contributing
256
-
257
- We welcome your suggestions and improvements! Please create [issues](https://github.com/poliklot/form-father/issues) and
258
- submit [pull requests](https://github.com/poliklot/form-father/pulls).
259
-
260
- ## License
261
-
262
- [MIT](LICENSE)
263
-
264
- ---
265
-
266
- © 2024 Poliklot