form-father 0.2.10 → 0.3.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 +79 -0
- package/README.md +77 -2
- package/demos/index.html +141 -0
- package/demos/main.js +92 -0
- package/demos/styles.css +221 -0
- package/dist/FormFather.min.js +1 -1
- package/dist/index.d.ts +158 -7
- package/dist/index.js +641 -130
- package/dist/index.js.map +1 -1
- package/dist/types/adapters.d.ts +28 -0
- package/dist/types/index.d.ts +128 -4
- package/dist/types/validators.d.ts +3 -2
- package/docs/en/README.md +192 -0
- package/docs/recipes/README.md +89 -0
- package/package.json +5 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
## 0.3.0 - 2026-04-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added convenience API: `Form.initAll()`, `updateOptions()`, public `submit()`, `validateField()`, `showFieldError()`,
|
|
10
|
+
`getErrors()`, `getValues()`, `setValues()`, `clearErrors()`, and `reset()`.
|
|
11
|
+
- Added client validation hooks: `onValidationError`, `onBeforeSubmit`, and `onSubmitError`.
|
|
12
|
+
- Added live validation options: `validateOn`, `revalidateOn`, `validationDebounce`, and `focusFirstErroredInput`.
|
|
13
|
+
- Added `data-validate`, `data-error-*`, and `data-error-container` support while keeping `data-custom-validate`.
|
|
14
|
+
- Added optional mutation observing for dynamic fields and submit buttons.
|
|
15
|
+
- Added race-safe async field validation state with `aria-busy` and configurable field state attributes.
|
|
16
|
+
- Added dependency-free schema adapters: `createSchemaValidator()`, `registerSchemaValidator()`, and
|
|
17
|
+
`createFieldValidator()`.
|
|
18
|
+
- Added static demos for login, callback/server errors, GET search, and multipart upload.
|
|
19
|
+
- Added recipes documentation for common API, data-attribute, adapter, and server-error flows.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Validation now merges every matching schema rule for a field before applying data-attribute rules.
|
|
24
|
+
- Server-side field errors now populate the public `getErrors()` list.
|
|
25
|
+
- Package contents now include docs, demos, and the changelog.
|
|
26
|
+
|
|
27
|
+
### Tests
|
|
28
|
+
|
|
29
|
+
- Test suite expanded to 109 tests.
|
|
30
|
+
- Coverage baseline: 98.10% statements, 86.15% branches, 95.69% functions, 100% lines.
|
|
31
|
+
|
|
32
|
+
## 0.2.11 - 2026-04-25
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Added release checklist, package smoke checks, CI workflow, and release gate scripts.
|
|
37
|
+
- Added npm pack dry-run script with a temporary npm cache for local cache permission issues.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Updated package metadata and release workflow after `0.2.10` had already been published.
|
|
42
|
+
|
|
43
|
+
## 0.2.10 - 2026-04-25
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- Exported public helper APIs from the package entrypoint: `serializeToFormData`, validators, URL/email/phone helpers, scroll helpers, and response helpers.
|
|
48
|
+
- Exported TypeScript types for public consumers: `FormOptions`, `ValidationRule`, `ValidationSchema`, `ErrorResponse`, and `ResponseBody`.
|
|
49
|
+
- Added package smoke checks for built files and public exports.
|
|
50
|
+
- Added CI workflow for tests, build, smoke checks, and npm pack dry-run.
|
|
51
|
+
- Added coverage thresholds to keep the stabilization baseline from regressing.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- `new Form(formEl)` now works without an options object.
|
|
56
|
+
- Submit detection now supports implicit submit buttons (`<button>`) and `input[type="image"]`.
|
|
57
|
+
- `GET` and `HEAD` forms now append form data to the action query string and do not send a request body.
|
|
58
|
+
- `wrapData` now applies consistently across supported encodings: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`, and `application/json`.
|
|
59
|
+
- Non-200 HTTP responses, `success !== true`, and invalid JSON responses now flow through `onResponseUnsuccess`.
|
|
60
|
+
- Server-side field errors can be applied to `input`, `textarea`, and `select`.
|
|
61
|
+
- `serializeToFormData` now skips disabled controls, controls marked with `data-no-serialize`, and button controls.
|
|
62
|
+
- Built-in validators are registered idempotently across repeated module imports.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- Loader and `waitResponse` cleanup now runs when `fetch` or response parsing fails.
|
|
67
|
+
- Required radio groups now produce one validation error per group.
|
|
68
|
+
- Field error rendering no longer requires a wrapper-level `showError()` method.
|
|
69
|
+
- Release script now exits with `0` on success and handles missing temporary files during cleanup.
|
|
70
|
+
- Rollup IIFE build no longer warns about mixed default and named exports.
|
|
71
|
+
|
|
72
|
+
### Tests
|
|
73
|
+
|
|
74
|
+
- Test coverage baseline: 98.32% statements, 86.34% branches, 93.18% functions, 100% lines.
|
|
75
|
+
- Test suite baseline: 93 tests passing.
|
|
76
|
+
|
|
77
|
+
## 0.2.9
|
|
78
|
+
|
|
79
|
+
- Previous published patch release.
|
package/README.md
CHANGED
|
@@ -59,10 +59,23 @@ 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, {
|
|
71
|
+
import Form, {
|
|
72
|
+
type FormOptions,
|
|
73
|
+
type ValidationSchema,
|
|
74
|
+
type ResponseBody,
|
|
75
|
+
type ValidationError,
|
|
76
|
+
type SubmitResult,
|
|
77
|
+
type FormResetOptions,
|
|
78
|
+
} from 'form-father';
|
|
66
79
|
```
|
|
67
80
|
|
|
68
81
|
## Формат ответов сервера
|
|
@@ -75,19 +88,31 @@ import Form, { type FormOptions, type ValidationSchema, type ResponseBody } from
|
|
|
75
88
|
## Опции
|
|
76
89
|
|
|
77
90
|
- **onSubmit**: Функция обратного вызова, вызываемая при отправке формы.
|
|
91
|
+
- **onBeforeSubmit**: Функция вызывается перед отправкой уже валидной формы; `false` отменяет отправку.
|
|
92
|
+
- **onSubmitError**: Функция вызывается при исключении во время отправки.
|
|
78
93
|
- **onResponse**: Функция обратного вызова при получении ответа от сервера.
|
|
79
94
|
- **onResponseSuccess**: Функция вызывается при успешном HTTP-ответе и `success: true`.
|
|
80
95
|
- **onResponseUnsuccess**: Функция вызывается при неуспешном HTTP-ответе, `success !== true` или некорректном JSON.
|
|
96
|
+
- **onValidationError**: Функция вызывается, когда клиентская валидация не прошла.
|
|
81
97
|
- **showLoaderButton**: Показывать ли лоадер в кнопке отправки. По умолчанию `true`.
|
|
82
98
|
- **scrollToFirstErroredInput**: Прокручивать ли к первому полю с ошибкой. По умолчанию `true`.
|
|
99
|
+
- **focusFirstErroredInput**: Переводить ли фокус в первое поле с ошибкой. По умолчанию `false`.
|
|
83
100
|
- **customTypeError**: Кастомный тип ошибки.
|
|
84
101
|
- **loaderColor**: Цвет лоадера в кнопке отправки.
|
|
85
102
|
- **logging**: Нужно ли выводить данные в консоль. По умолчанию `false`.
|
|
103
|
+
- **validateOn**: События live-валидации поля: `submit`, `input`, `blur`, `change`.
|
|
104
|
+
- **revalidateOn**: События повторной проверки уже ошибочных полей. По умолчанию `input` и `change`.
|
|
105
|
+
- **validationDebounce**: Задержка live-валидации в миллисекундах.
|
|
106
|
+
- **errorContainerAttribute**: Атрибут с CSS-селектором контейнера ошибки. По умолчанию `data-error-container`.
|
|
107
|
+
- **validationStateAttribute**: Атрибут состояния поля: `validating`, `valid`, `invalid`. По умолчанию
|
|
108
|
+
`data-form-father-state`.
|
|
109
|
+
- **observeMutations**: Следить за динамически добавленными полями и submit-кнопками.
|
|
86
110
|
|
|
87
111
|
## Отправка формы
|
|
88
112
|
|
|
89
113
|
- Submit-элементами считаются `button[type="submit"]`, `button` без `type`, `input[type="submit"]` и
|
|
90
114
|
`input[type="image"]`.
|
|
115
|
+
- `submit()` доступен публично и возвращает `Promise<SubmitResult | undefined>`.
|
|
91
116
|
- Для `method="GET"` и `method="HEAD"` данные добавляются в query string `action`, тело запроса не отправляется.
|
|
92
117
|
- `wrapData` применяется для всех поддержанных `enctype`: `application/x-www-form-urlencoded`, `multipart/form-data`,
|
|
93
118
|
`text/plain`, `application/json`.
|
|
@@ -96,6 +121,16 @@ import Form, { type FormOptions, type ValidationSchema, type ResponseBody } from
|
|
|
96
121
|
|
|
97
122
|
## Методы
|
|
98
123
|
|
|
124
|
+
- **Form.initAll(selector, options)**: Инициализирует все формы по селектору и переиспользует уже созданные инстансы.
|
|
125
|
+
- **updateOptions(options)**: Обновляет настройки конкретной формы и перепривязывает live-валидацию.
|
|
126
|
+
- **validate()**: Проверяет всю форму.
|
|
127
|
+
- **validateField(field)**: Проверяет одно поле по имени или DOM-элементу.
|
|
128
|
+
- **showFieldError(field, message, source)**: Показывает ошибку поля вручную.
|
|
129
|
+
- **getErrors()**: Возвращает текущие ошибки в формате `{ field, rule, message, source }`.
|
|
130
|
+
- **getValues()**: Возвращает значения формы обычным объектом.
|
|
131
|
+
- **setValues(values)**: Заполняет поля по `name` и диспатчит `input`/`change`.
|
|
132
|
+
- **clearErrors()**: Очищает ошибки, не меняя значения полей.
|
|
133
|
+
- **reset(options)**: Вызывает native `form.reset()` и по умолчанию очищает ошибки.
|
|
99
134
|
- **clearInputs()**: Очищает все поля ввода формы.
|
|
100
135
|
- **setDefaultParams(params)**: Метод setDefaultParams используется для установки значений по умолчанию для всех
|
|
101
136
|
экземпляров формы. Эти параметры можно переопределить при инициализации конкретной формы.
|
|
@@ -188,6 +223,27 @@ _Порядок применения для одного `<input>`:_
|
|
|
188
223
|
|
|
189
224
|
---
|
|
190
225
|
|
|
226
|
+
### Правила через data-атрибуты
|
|
227
|
+
|
|
228
|
+
```html
|
|
229
|
+
<input
|
|
230
|
+
name="email"
|
|
231
|
+
type="email"
|
|
232
|
+
data-validate="required|email"
|
|
233
|
+
data-error-required="Email обязателен"
|
|
234
|
+
data-error-email="Введите корректный email"
|
|
235
|
+
data-error-container="#email-error"
|
|
236
|
+
/>
|
|
237
|
+
<small id="email-error" hidden></small>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- `data-validate` принимает правила через `|`, пробел или запятую.
|
|
241
|
+
- `data-error-rule-name` переопределяет сообщение конкретного правила.
|
|
242
|
+
- `data-error-container` указывает контейнер для ошибки без обязательной wrapper-разметки.
|
|
243
|
+
- Во время async-валидации поле получает `aria-busy="true"` и state-атрибут `validating`; устаревшие ответы
|
|
244
|
+
валидаторов игнорируются.
|
|
245
|
+
- `data-custom-validate` сохранён для обратной совместимости.
|
|
246
|
+
|
|
191
247
|
### Правило через `data-custom-validate`
|
|
192
248
|
|
|
193
249
|
```html
|
|
@@ -259,12 +315,31 @@ validationSchema: {
|
|
|
259
315
|
### Итоговый порядок проверки
|
|
260
316
|
|
|
261
317
|
```
|
|
262
|
-
required → schema.rules → data-custom-validate
|
|
318
|
+
required → schema.rules → data-validate → data-custom-validate
|
|
263
319
|
```
|
|
264
320
|
|
|
265
321
|
- Для каждого поля отображается **только первое** найденное сообщение об ошибке.
|
|
266
322
|
- Неизвестные правила логируются и пропускаются до начала проверки, поэтому не влияют на результат валидации.
|
|
267
323
|
|
|
324
|
+
## Адаптеры схем
|
|
325
|
+
|
|
326
|
+
Form Father не тянет внешние зависимости, но умеет оборачивать Zod/Valibot/Yup-подобные схемы с `safeParse()` или
|
|
327
|
+
`parse()`.
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { registerSchemaValidator } from 'form-father';
|
|
331
|
+
|
|
332
|
+
registerSchemaValidator('company-email', z.string().email(), 'Введите корпоративный email');
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Для простых проверок есть `createFieldValidator(predicate, message)`.
|
|
336
|
+
|
|
337
|
+
## Демо и рецепты
|
|
338
|
+
|
|
339
|
+
- `demos/index.html` — статическое демо login/callback/search/upload после `npm run build`.
|
|
340
|
+
- `npm run demos` — сборка и запуск Vite-сервера для демо.
|
|
341
|
+
- `docs/recipes/README.md` — короткие рецепты по API, data-атрибутам, adapters и server errors.
|
|
342
|
+
|
|
268
343
|
## Сборка и разработка
|
|
269
344
|
|
|
270
345
|
Для сборки проекта используется **Rollup**. Основные команды:
|
package/demos/index.html
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
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
|
+
<button type="submit">Войти</button>
|
|
55
|
+
</form>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<section class="demo-panel">
|
|
59
|
+
<div class="panel-title">
|
|
60
|
+
<p class="eyebrow">Callback</p>
|
|
61
|
+
<h2>Server field errors</h2>
|
|
62
|
+
</div>
|
|
63
|
+
<form data-form-father action="/demo/callback" method="post" data-demo="callback" novalidate>
|
|
64
|
+
<label class="field">
|
|
65
|
+
<span>Имя</span>
|
|
66
|
+
<input class="input" name="name" data-validate="required" data-error-required="Укажите имя" />
|
|
67
|
+
</label>
|
|
68
|
+
|
|
69
|
+
<label class="field">
|
|
70
|
+
<span>Телефон</span>
|
|
71
|
+
<input
|
|
72
|
+
class="input"
|
|
73
|
+
name="tel"
|
|
74
|
+
type="tel"
|
|
75
|
+
placeholder="+79991234567"
|
|
76
|
+
data-validate="required|tel"
|
|
77
|
+
data-error-tel="Телефон в формате +7XXXXXXXXXX"
|
|
78
|
+
/>
|
|
79
|
+
</label>
|
|
80
|
+
|
|
81
|
+
<button type="submit">Заказать звонок</button>
|
|
82
|
+
</form>
|
|
83
|
+
</section>
|
|
84
|
+
|
|
85
|
+
<section class="demo-panel">
|
|
86
|
+
<div class="panel-title">
|
|
87
|
+
<p class="eyebrow">Search</p>
|
|
88
|
+
<h2>GET query</h2>
|
|
89
|
+
</div>
|
|
90
|
+
<form data-form-father action="/demo/search?source=demos" method="get" data-demo="search" novalidate>
|
|
91
|
+
<label class="field">
|
|
92
|
+
<span>Запрос</span>
|
|
93
|
+
<input class="input" name="q" value="form father" data-validate="required" />
|
|
94
|
+
</label>
|
|
95
|
+
|
|
96
|
+
<label class="field">
|
|
97
|
+
<span>Раздел</span>
|
|
98
|
+
<select class="input" name="section">
|
|
99
|
+
<option value="docs">Docs</option>
|
|
100
|
+
<option value="api">API</option>
|
|
101
|
+
<option value="examples">Examples</option>
|
|
102
|
+
</select>
|
|
103
|
+
</label>
|
|
104
|
+
|
|
105
|
+
<button type="submit">Найти</button>
|
|
106
|
+
</form>
|
|
107
|
+
</section>
|
|
108
|
+
|
|
109
|
+
<section class="demo-panel">
|
|
110
|
+
<div class="panel-title">
|
|
111
|
+
<p class="eyebrow">Multipart</p>
|
|
112
|
+
<h2>FormData upload</h2>
|
|
113
|
+
</div>
|
|
114
|
+
<form data-form-father action="/demo/upload" method="post" enctype="multipart/form-data" data-demo="upload" novalidate>
|
|
115
|
+
<label class="field">
|
|
116
|
+
<span>Название</span>
|
|
117
|
+
<input class="input" name="title" data-validate="required" />
|
|
118
|
+
</label>
|
|
119
|
+
|
|
120
|
+
<label class="field">
|
|
121
|
+
<span>Файл</span>
|
|
122
|
+
<input class="input" name="attachment" type="file" />
|
|
123
|
+
</label>
|
|
124
|
+
|
|
125
|
+
<button type="submit">Отправить</button>
|
|
126
|
+
</form>
|
|
127
|
+
</section>
|
|
128
|
+
</main>
|
|
129
|
+
|
|
130
|
+
<section class="demo-output">
|
|
131
|
+
<div class="panel-title">
|
|
132
|
+
<p class="eyebrow">Output</p>
|
|
133
|
+
<h2>Последний ответ</h2>
|
|
134
|
+
</div>
|
|
135
|
+
<pre id="demo-output">Заполните любую форму.</pre>
|
|
136
|
+
</section>
|
|
137
|
+
|
|
138
|
+
<script src="../dist/FormFather.min.js"></script>
|
|
139
|
+
<script src="./main.js"></script>
|
|
140
|
+
</body>
|
|
141
|
+
</html>
|
package/demos/main.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
onValidationError(errors) {
|
|
82
|
+
writeOutput('Client validation errors', errors);
|
|
83
|
+
},
|
|
84
|
+
onResponse(responseBody, form) {
|
|
85
|
+
writeOutput(`${form.$el.dataset.demo || 'form'} response`, responseBody);
|
|
86
|
+
},
|
|
87
|
+
onResponseSuccess(responseBody, form) {
|
|
88
|
+
if (form.$el.dataset.demo === 'login' || form.$el.dataset.demo === 'upload') {
|
|
89
|
+
form.clearInputs();
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
package/demos/styles.css
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
.field {
|
|
120
|
+
display: grid;
|
|
121
|
+
gap: 6px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.field span {
|
|
125
|
+
color: var(--muted);
|
|
126
|
+
font-size: 14px;
|
|
127
|
+
font-weight: 700;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.input {
|
|
131
|
+
width: 100%;
|
|
132
|
+
min-height: 42px;
|
|
133
|
+
padding: 8px 10px;
|
|
134
|
+
border: 1px solid var(--line);
|
|
135
|
+
border-radius: 6px;
|
|
136
|
+
background: #ffffff;
|
|
137
|
+
color: var(--text);
|
|
138
|
+
font: inherit;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.input:focus {
|
|
142
|
+
border-color: var(--accent);
|
|
143
|
+
outline: 3px solid rgba(23, 107, 135, 0.16);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.input[aria-invalid="true"] {
|
|
147
|
+
border-color: var(--danger);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.input[aria-busy="true"] {
|
|
151
|
+
opacity: 0.72;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.input[data-form-father-state="valid"] {
|
|
155
|
+
border-color: var(--success);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.input[data-form-father-state="invalid"] {
|
|
159
|
+
border-color: var(--danger);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
[data-form-father-error],
|
|
163
|
+
.field-error {
|
|
164
|
+
color: var(--danger);
|
|
165
|
+
font-size: 13px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.input__wrapper--error .input {
|
|
169
|
+
border-color: var(--danger);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.button--loading {
|
|
173
|
+
opacity: 0.74;
|
|
174
|
+
pointer-events: none;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.button__loader {
|
|
178
|
+
display: inline-flex;
|
|
179
|
+
width: 18px;
|
|
180
|
+
height: 18px;
|
|
181
|
+
margin-right: 8px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.button__loader svg {
|
|
185
|
+
width: 18px;
|
|
186
|
+
height: 18px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.error-block-under-input__wrapper {
|
|
190
|
+
color: var(--danger);
|
|
191
|
+
font-size: 14px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.demo-output {
|
|
195
|
+
margin: 0 min(6vw, 72px) 40px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pre {
|
|
199
|
+
min-height: 130px;
|
|
200
|
+
margin: 0;
|
|
201
|
+
overflow: auto;
|
|
202
|
+
padding: 16px;
|
|
203
|
+
border: 1px solid var(--line);
|
|
204
|
+
border-radius: 8px;
|
|
205
|
+
background: #101820;
|
|
206
|
+
color: #d8f3dc;
|
|
207
|
+
font-size: 13px;
|
|
208
|
+
white-space: pre-wrap;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@media (max-width: 760px) {
|
|
212
|
+
.demo-header {
|
|
213
|
+
display: grid;
|
|
214
|
+
align-items: start;
|
|
215
|
+
padding-top: 28px;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.demo-grid {
|
|
219
|
+
grid-template-columns: 1fr;
|
|
220
|
+
}
|
|
221
|
+
}
|