form-father 0.2.11 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,98 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ ## 0.4.0 - 2026-04-27
6
+
7
+ ### Added
8
+
9
+ - Added form-level and cross-field validation via `formValidators`.
10
+ - Added `setErrors()` for backend maps, `ErrorResponse[]`, global form errors, and custom validation issue objects.
11
+ - Added `FORM_ERROR_FIELD` constant for form-level errors in `getErrors()`.
12
+ - Added public TypeScript types for form validation contexts and issues.
13
+
14
+ ### Changed
15
+
16
+ - `validate()` now runs field-level rules first, then form-level validators over `getValues()`.
17
+ - Cross-field and backend errors share the same rendering path as field validation errors.
18
+
19
+ ### Tests
20
+
21
+ - Test suite expanded to 113 tests.
22
+ - Coverage baseline: 98.31% statements, 87.05% branches, 96.34% functions, 100% lines.
23
+
24
+ ## 0.3.0 - 2026-04-25
25
+
26
+ ### Added
27
+
28
+ - Added convenience API: `Form.initAll()`, `updateOptions()`, public `submit()`, `validateField()`, `showFieldError()`,
29
+ `getErrors()`, `getValues()`, `setValues()`, `clearErrors()`, and `reset()`.
30
+ - Added client validation hooks: `onValidationError`, `onBeforeSubmit`, and `onSubmitError`.
31
+ - Added live validation options: `validateOn`, `revalidateOn`, `validationDebounce`, and `focusFirstErroredInput`.
32
+ - Added `data-validate`, `data-error-*`, and `data-error-container` support while keeping `data-custom-validate`.
33
+ - Added optional mutation observing for dynamic fields and submit buttons.
34
+ - Added race-safe async field validation state with `aria-busy` and configurable field state attributes.
35
+ - Added dependency-free schema adapters: `createSchemaValidator()`, `registerSchemaValidator()`, and
36
+ `createFieldValidator()`.
37
+ - Added static demos for login, callback/server errors, GET search, and multipart upload.
38
+ - Added recipes documentation for common API, data-attribute, adapter, and server-error flows.
39
+
40
+ ### Changed
41
+
42
+ - Validation now merges every matching schema rule for a field before applying data-attribute rules.
43
+ - Server-side field errors now populate the public `getErrors()` list.
44
+ - Package contents now include docs, demos, and the changelog.
45
+
46
+ ### Tests
47
+
48
+ - Test suite expanded to 109 tests.
49
+ - Coverage baseline: 98.10% statements, 86.15% branches, 95.69% functions, 100% lines.
50
+
51
+ ## 0.2.11 - 2026-04-25
52
+
53
+ ### Added
54
+
55
+ - Added release checklist, package smoke checks, CI workflow, and release gate scripts.
56
+ - Added npm pack dry-run script with a temporary npm cache for local cache permission issues.
57
+
58
+ ### Changed
59
+
60
+ - Updated package metadata and release workflow after `0.2.10` had already been published.
61
+
62
+ ## 0.2.10 - 2026-04-25
63
+
64
+ ### Added
65
+
66
+ - Exported public helper APIs from the package entrypoint: `serializeToFormData`, validators, URL/email/phone helpers, scroll helpers, and response helpers.
67
+ - Exported TypeScript types for public consumers: `FormOptions`, `ValidationRule`, `ValidationSchema`, `ErrorResponse`, and `ResponseBody`.
68
+ - Added package smoke checks for built files and public exports.
69
+ - Added CI workflow for tests, build, smoke checks, and npm pack dry-run.
70
+ - Added coverage thresholds to keep the stabilization baseline from regressing.
71
+
72
+ ### Changed
73
+
74
+ - `new Form(formEl)` now works without an options object.
75
+ - Submit detection now supports implicit submit buttons (`<button>`) and `input[type="image"]`.
76
+ - `GET` and `HEAD` forms now append form data to the action query string and do not send a request body.
77
+ - `wrapData` now applies consistently across supported encodings: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`, and `application/json`.
78
+ - Non-200 HTTP responses, `success !== true`, and invalid JSON responses now flow through `onResponseUnsuccess`.
79
+ - Server-side field errors can be applied to `input`, `textarea`, and `select`.
80
+ - `serializeToFormData` now skips disabled controls, controls marked with `data-no-serialize`, and button controls.
81
+ - Built-in validators are registered idempotently across repeated module imports.
82
+
83
+ ### Fixed
84
+
85
+ - Loader and `waitResponse` cleanup now runs when `fetch` or response parsing fails.
86
+ - Required radio groups now produce one validation error per group.
87
+ - Field error rendering no longer requires a wrapper-level `showError()` method.
88
+ - Release script now exits with `0` on success and handles missing temporary files during cleanup.
89
+ - Rollup IIFE build no longer warns about mixed default and named exports.
90
+
91
+ ### Tests
92
+
93
+ - Test coverage baseline: 98.32% statements, 86.34% branches, 93.18% functions, 100% lines.
94
+ - Test suite baseline: 93 tests passing.
95
+
96
+ ## 0.2.9
97
+
98
+ - Previous published patch release.
package/README.md CHANGED
@@ -59,10 +59,26 @@ const options = {
59
59
 
60
60
  const form = new Form(formElement, options);
61
61
  const simpleForm = new Form(document.querySelector('#simpleForm')); // options можно не передавать
62
+
63
+ const forms = Form.initAll('form[data-form-father]', {
64
+ inputWrapperSelector: '.field',
65
+ validateOn: ['blur', 'change'],
66
+ revalidateOn: ['input', 'change'],
67
+ });
62
68
  ```
63
69
 
64
70
  ```ts
65
- import Form, { type FormOptions, type ValidationSchema, type ResponseBody } from 'form-father';
71
+ import Form, {
72
+ FORM_ERROR_FIELD,
73
+ type FormOptions,
74
+ type ValidationSchema,
75
+ type ResponseBody,
76
+ type ValidationError,
77
+ type SubmitResult,
78
+ type FormResetOptions,
79
+ type FormValidator,
80
+ type ValidationIssue,
81
+ } from 'form-father';
66
82
  ```
67
83
 
68
84
  ## Формат ответов сервера
@@ -75,19 +91,32 @@ import Form, { type FormOptions, type ValidationSchema, type ResponseBody } from
75
91
  ## Опции
76
92
 
77
93
  - **onSubmit**: Функция обратного вызова, вызываемая при отправке формы.
94
+ - **onBeforeSubmit**: Функция вызывается перед отправкой уже валидной формы; `false` отменяет отправку.
95
+ - **onSubmitError**: Функция вызывается при исключении во время отправки.
78
96
  - **onResponse**: Функция обратного вызова при получении ответа от сервера.
79
97
  - **onResponseSuccess**: Функция вызывается при успешном HTTP-ответе и `success: true`.
80
98
  - **onResponseUnsuccess**: Функция вызывается при неуспешном HTTP-ответе, `success !== true` или некорректном JSON.
99
+ - **onValidationError**: Функция вызывается, когда клиентская валидация не прошла.
81
100
  - **showLoaderButton**: Показывать ли лоадер в кнопке отправки. По умолчанию `true`.
82
101
  - **scrollToFirstErroredInput**: Прокручивать ли к первому полю с ошибкой. По умолчанию `true`.
102
+ - **focusFirstErroredInput**: Переводить ли фокус в первое поле с ошибкой. По умолчанию `false`.
83
103
  - **customTypeError**: Кастомный тип ошибки.
84
104
  - **loaderColor**: Цвет лоадера в кнопке отправки.
85
105
  - **logging**: Нужно ли выводить данные в консоль. По умолчанию `false`.
106
+ - **validateOn**: События live-валидации поля: `submit`, `input`, `blur`, `change`.
107
+ - **revalidateOn**: События повторной проверки уже ошибочных полей. По умолчанию `input` и `change`.
108
+ - **validationDebounce**: Задержка live-валидации в миллисекундах.
109
+ - **errorContainerAttribute**: Атрибут с CSS-селектором контейнера ошибки. По умолчанию `data-error-container`.
110
+ - **validationStateAttribute**: Атрибут состояния поля: `validating`, `valid`, `invalid`. По умолчанию
111
+ `data-form-father-state`.
112
+ - **observeMutations**: Следить за динамически добавленными полями и submit-кнопками.
113
+ - **formValidators**: Валидатор или массив валидаторов всей формы для cross-field правил.
86
114
 
87
115
  ## Отправка формы
88
116
 
89
117
  - Submit-элементами считаются `button[type="submit"]`, `button` без `type`, `input[type="submit"]` и
90
118
  `input[type="image"]`.
119
+ - `submit()` доступен публично и возвращает `Promise<SubmitResult | undefined>`.
91
120
  - Для `method="GET"` и `method="HEAD"` данные добавляются в query string `action`, тело запроса не отправляется.
92
121
  - `wrapData` применяется для всех поддержанных `enctype`: `application/x-www-form-urlencoded`, `multipart/form-data`,
93
122
  `text/plain`, `application/json`.
@@ -96,6 +125,17 @@ import Form, { type FormOptions, type ValidationSchema, type ResponseBody } from
96
125
 
97
126
  ## Методы
98
127
 
128
+ - **Form.initAll(selector, options)**: Инициализирует все формы по селектору и переиспользует уже созданные инстансы.
129
+ - **updateOptions(options)**: Обновляет настройки конкретной формы и перепривязывает live-валидацию.
130
+ - **validate()**: Проверяет всю форму.
131
+ - **validateField(field)**: Проверяет одно поле по имени или DOM-элементу.
132
+ - **showFieldError(field, message, source)**: Показывает ошибку поля вручную.
133
+ - **setErrors(errors, source)**: Применяет backend/form-level ошибки из объекта, массива или `ErrorResponse[]`.
134
+ - **getErrors()**: Возвращает текущие ошибки в формате `{ field, rule, message, source }`.
135
+ - **getValues()**: Возвращает значения формы обычным объектом.
136
+ - **setValues(values)**: Заполняет поля по `name` и диспатчит `input`/`change`.
137
+ - **clearErrors()**: Очищает ошибки, не меняя значения полей.
138
+ - **reset(options)**: Вызывает native `form.reset()` и по умолчанию очищает ошибки.
99
139
  - **clearInputs()**: Очищает все поля ввода формы.
100
140
  - **setDefaultParams(params)**: Метод setDefaultParams используется для установки значений по умолчанию для всех
101
141
  экземпляров формы. Эти параметры можно переопределить при инициализации конкретной формы.
@@ -188,6 +228,27 @@ _Порядок применения для одного `<input>`:_
188
228
 
189
229
  ---
190
230
 
231
+ ### Правила через data-атрибуты
232
+
233
+ ```html
234
+ <input
235
+ name="email"
236
+ type="email"
237
+ data-validate="required|email"
238
+ data-error-required="Email обязателен"
239
+ data-error-email="Введите корректный email"
240
+ data-error-container="#email-error"
241
+ />
242
+ <small id="email-error" hidden></small>
243
+ ```
244
+
245
+ - `data-validate` принимает правила через `|`, пробел или запятую.
246
+ - `data-error-rule-name` переопределяет сообщение конкретного правила.
247
+ - `data-error-container` указывает контейнер для ошибки без обязательной wrapper-разметки.
248
+ - Во время async-валидации поле получает `aria-busy="true"` и state-атрибут `validating`; устаревшие ответы
249
+ валидаторов игнорируются.
250
+ - `data-custom-validate` сохранён для обратной совместимости.
251
+
191
252
  ### Правило через `data-custom-validate`
192
253
 
193
254
  ```html
@@ -259,12 +320,59 @@ validationSchema: {
259
320
  ### Итоговый порядок проверки
260
321
 
261
322
  ```
262
- required → schema.rules → data-custom-validate
323
+ required → schema.rules → data-validate → data-custom-validate
263
324
  ```
264
325
 
265
326
  - Для каждого поля отображается **только первое** найденное сообщение об ошибке.
266
327
  - Неизвестные правила логируются и пропускаются до начала проверки, поэтому не влияют на результат валидации.
267
328
 
329
+ ### Cross-field и form-level validation
330
+
331
+ ```ts
332
+ const form = new Form($form, {
333
+ formValidators: [
334
+ ({ values }) =>
335
+ values.password === values.passwordConfirm
336
+ ? true
337
+ : {
338
+ field: 'passwordConfirm',
339
+ rule: 'same-as-password',
340
+ message: 'Пароли не совпадают',
341
+ },
342
+ ({ values }) => (values.start <= values.end ? true : { field: 'end', message: 'Дата окончания раньше начала' }),
343
+ ],
344
+ });
345
+ ```
346
+
347
+ Глобальную ошибку формы можно вернуть строкой или issue без `field`. В `getErrors()` она будет иметь поле
348
+ `FORM_ERROR_FIELD` (`"_form"`).
349
+
350
+ ```ts
351
+ form.setErrors({
352
+ email: 'Email уже занят',
353
+ [FORM_ERROR_FIELD]: 'Проверьте данные формы',
354
+ });
355
+ ```
356
+
357
+ ## Адаптеры схем
358
+
359
+ Form Father не тянет внешние зависимости, но умеет оборачивать Zod/Valibot/Yup-подобные схемы с `safeParse()` или
360
+ `parse()`.
361
+
362
+ ```ts
363
+ import { registerSchemaValidator } from 'form-father';
364
+
365
+ registerSchemaValidator('company-email', z.string().email(), 'Введите корпоративный email');
366
+ ```
367
+
368
+ Для простых проверок есть `createFieldValidator(predicate, message)`.
369
+
370
+ ## Демо и рецепты
371
+
372
+ - `demos/index.html` — статическое демо login/callback/search/upload после `npm run build`.
373
+ - `npm run demos` — сборка и запуск Vite-сервера для демо.
374
+ - `docs/recipes/README.md` — короткие рецепты по API, data-атрибутам, adapters и server errors.
375
+
268
376
  ## Сборка и разработка
269
377
 
270
378
  Для сборки проекта используется **Rollup**. Основные команды:
@@ -0,0 +1,153 @@
1
+ <!doctype html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Form Father demos</title>
7
+ <link rel="stylesheet" href="./styles.css" />
8
+ </head>
9
+ <body>
10
+ <header class="demo-header">
11
+ <div>
12
+ <p class="eyebrow">Form Father</p>
13
+ <h1>Демо форм, валидации и ответов сервера</h1>
14
+ </div>
15
+ <a class="github-link" href="https://github.com/Poliklot/form-father">GitHub</a>
16
+ </header>
17
+
18
+ <main class="demo-grid">
19
+ <section class="demo-panel">
20
+ <div class="panel-title">
21
+ <p class="eyebrow">Login</p>
22
+ <h2>Live validation</h2>
23
+ </div>
24
+ <form data-form-father action="/demo/login" method="post" enctype="application/json" data-demo="login" novalidate>
25
+ <label class="field">
26
+ <span>Email</span>
27
+ <input
28
+ class="input"
29
+ name="email"
30
+ type="email"
31
+ autocomplete="email"
32
+ data-validate="required|email|available-email"
33
+ data-error-required="Email обязателен"
34
+ data-error-email="Введите корректный email"
35
+ data-error-available-email="Этот email уже занят"
36
+ data-error-container="#login-email-error"
37
+ />
38
+ <small id="login-email-error" class="field-error" hidden></small>
39
+ </label>
40
+
41
+ <label class="field">
42
+ <span>Password</span>
43
+ <input
44
+ class="input"
45
+ name="password"
46
+ type="password"
47
+ autocomplete="current-password"
48
+ data-validate="required|min-length:6"
49
+ data-error-required="Пароль обязателен"
50
+ data-error-min-length="Минимум 6 символов"
51
+ />
52
+ </label>
53
+
54
+ <label class="field">
55
+ <span>Repeat password</span>
56
+ <input
57
+ class="input"
58
+ name="passwordConfirm"
59
+ type="password"
60
+ autocomplete="current-password"
61
+ data-validate="required"
62
+ data-error-required="Повторите пароль"
63
+ />
64
+ </label>
65
+
66
+ <button type="submit">Войти</button>
67
+ </form>
68
+ </section>
69
+
70
+ <section class="demo-panel">
71
+ <div class="panel-title">
72
+ <p class="eyebrow">Callback</p>
73
+ <h2>Server field errors</h2>
74
+ </div>
75
+ <form data-form-father action="/demo/callback" method="post" data-demo="callback" novalidate>
76
+ <label class="field">
77
+ <span>Имя</span>
78
+ <input class="input" name="name" data-validate="required" data-error-required="Укажите имя" />
79
+ </label>
80
+
81
+ <label class="field">
82
+ <span>Телефон</span>
83
+ <input
84
+ class="input"
85
+ name="tel"
86
+ type="tel"
87
+ placeholder="+79991234567"
88
+ data-validate="required|tel"
89
+ data-error-tel="Телефон в формате +7XXXXXXXXXX"
90
+ />
91
+ </label>
92
+
93
+ <button type="submit">Заказать звонок</button>
94
+ </form>
95
+ </section>
96
+
97
+ <section class="demo-panel">
98
+ <div class="panel-title">
99
+ <p class="eyebrow">Search</p>
100
+ <h2>GET query</h2>
101
+ </div>
102
+ <form data-form-father action="/demo/search?source=demos" method="get" data-demo="search" novalidate>
103
+ <label class="field">
104
+ <span>Запрос</span>
105
+ <input class="input" name="q" value="form father" data-validate="required" />
106
+ </label>
107
+
108
+ <label class="field">
109
+ <span>Раздел</span>
110
+ <select class="input" name="section">
111
+ <option value="docs">Docs</option>
112
+ <option value="api">API</option>
113
+ <option value="examples">Examples</option>
114
+ </select>
115
+ </label>
116
+
117
+ <button type="submit">Найти</button>
118
+ </form>
119
+ </section>
120
+
121
+ <section class="demo-panel">
122
+ <div class="panel-title">
123
+ <p class="eyebrow">Multipart</p>
124
+ <h2>FormData upload</h2>
125
+ </div>
126
+ <form data-form-father action="/demo/upload" method="post" enctype="multipart/form-data" data-demo="upload" novalidate>
127
+ <label class="field">
128
+ <span>Название</span>
129
+ <input class="input" name="title" data-validate="required" />
130
+ </label>
131
+
132
+ <label class="field">
133
+ <span>Файл</span>
134
+ <input class="input" name="attachment" type="file" />
135
+ </label>
136
+
137
+ <button type="submit">Отправить</button>
138
+ </form>
139
+ </section>
140
+ </main>
141
+
142
+ <section class="demo-output">
143
+ <div class="panel-title">
144
+ <p class="eyebrow">Output</p>
145
+ <h2>Последний ответ</h2>
146
+ </div>
147
+ <pre id="demo-output">Заполните любую форму.</pre>
148
+ </section>
149
+
150
+ <script src="../dist/FormFather.min.js"></script>
151
+ <script src="./main.js"></script>
152
+ </body>
153
+ </html>
package/demos/main.js ADDED
@@ -0,0 +1,103 @@
1
+ const api = window.FormFather || {};
2
+ const Form = api.default || api.Form || api;
3
+ const { registerValidator } = 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', (value, _input, _form, min) => value.length >= Number(min), 'Слишком короткое значение', {
61
+ override: true,
62
+ });
63
+ registerValidator(
64
+ 'available-email',
65
+ value =>
66
+ new Promise(resolve => {
67
+ setTimeout(() => resolve(value.toLowerCase() !== 'taken@example.com'), 350);
68
+ }),
69
+ 'Email is already used',
70
+ { override: true },
71
+ );
72
+
73
+ Form.initAll('form[data-form-father]', {
74
+ inputSelector: '.input',
75
+ inputWrapperSelector: '.field',
76
+ validateOn: ['blur', 'change'],
77
+ revalidateOn: ['input', 'change'],
78
+ validationDebounce: 120,
79
+ focusFirstErroredInput: true,
80
+ loaderColor: 'currentColor',
81
+ formValidators({ values }) {
82
+ if (values.password && values.passwordConfirm && values.password !== values.passwordConfirm) {
83
+ return {
84
+ field: 'passwordConfirm',
85
+ rule: 'same-as-password',
86
+ message: 'Пароли не совпадают',
87
+ };
88
+ }
89
+
90
+ return true;
91
+ },
92
+ onValidationError(errors) {
93
+ writeOutput('Client validation errors', errors);
94
+ },
95
+ onResponse(responseBody, form) {
96
+ writeOutput(`${form.$el.dataset.demo || 'form'} response`, responseBody);
97
+ },
98
+ onResponseSuccess(responseBody, form) {
99
+ if (form.$el.dataset.demo === 'login' || form.$el.dataset.demo === 'upload') {
100
+ form.clearInputs();
101
+ }
102
+ },
103
+ });