adaptform 2.1.0 → 2.2.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 +52 -4
- package/CHANGELOG.ru.md +52 -4
- package/README.md +43 -9
- package/README.ru.md +43 -9
- package/dist/{Form-kPYL4f-n.d.cts → Form-uHUaTUZS.d.cts} +81 -9
- package/dist/{Form-kPYL4f-n.d.ts → Form-uHUaTUZS.d.ts} +81 -9
- package/dist/index.cjs +735 -236
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +735 -236
- package/dist/index.mjs.map +1 -1
- package/dist/luxon.d.cts +1 -1
- package/dist/luxon.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
[English](CHANGELOG.md) · [Русский](CHANGELOG.ru.md)
|
|
4
4
|
|
|
5
|
-
All notable changes to AdaptForm are documented here.
|
|
5
|
+
All notable changes to AdaptForm are documented here. Version `2.1.0` was published to npm on 2026-10-14.
|
|
6
6
|
|
|
7
7
|
**[Interactive demo](https://arhimen.github.io/AdaptForm/)** · [Demo source](https://github.com/ArhiMEN/AdaptForm/tree/master/demo) · [Documentation](https://github.com/ArhiMEN/AdaptForm/blob/master/README.md)
|
|
8
8
|
|
|
9
|
-
## [2.
|
|
9
|
+
## [2.2.0] - Unreleased (source prepared)
|
|
10
|
+
|
|
11
|
+
### Added — headless form state foundation
|
|
12
|
+
|
|
13
|
+
- **HF-005:** Specify the field/form lifecycle, cleaned baselines, touched-vs-dirty semantics, snapshots, synchronous batches and compatibility rules in English and Russian. ([state contract](docs/ai/HEADLESS_STATE_CONTRACT.md))
|
|
14
|
+
- **HF-006:** Track cleaned initial values with `Field.isDirty` and `commitBaseline()`, including semantic comparisons of Dates, optional Luxon values, plugin-cleaned values and supported nested structures.
|
|
15
|
+
- **HF-007:** Expose derived `isDirty` and baseline commits on `Form`, `ArrayField` and `ArrayFormField`, including collection membership, order and nested values.
|
|
16
|
+
- **HF-008:** Add `subscribe(listener)`, idempotent unsubscribe, stable immutable `getSnapshot()` and publicly exported `SnapshotValue`, `NodeSnapshot`, `FieldSnapshot`, `FormSnapshot`, `ArrayFieldSnapshot`, `ArrayFormSnapshot` types; no required UI framework or runtime dependencies.
|
|
17
|
+
- **HF-009:** Add synchronous nested `Form.batch(callback)` with coalesced notifications, deferred automatic validation using final parent context and deterministic exception finalization. Completed writes are **not rolled back**.
|
|
18
|
+
- **HF-010:** Propagate changes from children, nested forms and collections to parents; reconcile subscriptions when nodes are removed or replaced and release ownership links when observers leave. ([PR #16](https://github.com/ArhiMEN/AdaptForm/pull/16))
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `ArrayField.remove()/clear()` and `ArrayFormField.remove()/clear()` now defer automatic validation inside `Form.batch()`, while retaining synchronous checks outside a batch and immediate explicit `checkValid()`. Added 34 specific regressions.
|
|
23
|
+
- `Form.allErrors` copies child error arrays rather than mutating or freezing live arrays when global errors and immutable snapshots are combined.
|
|
24
|
+
|
|
25
|
+
### Compatibility and constraints
|
|
26
|
+
|
|
27
|
+
- Existing class-based APIs, synchronous validation outside batches, `isTouched`, `isValid`, `reset()` behavior (including existing collection membership), serialization and the optional `adaptform/luxon` entry point are preserved.
|
|
28
|
+
- `commitBaseline()` changes the dirty-comparison reference, **not** the target of `reset()`; after committing a different baseline, reset can leave `isDirty=true`.
|
|
29
|
+
- Snapshots are detached immutable data descriptions, not original mutable objects; custom instances may be opaque. Direct in-place changes to exposed legacy arrays/options and arbitrary Form-property replacement are not automatically observed.
|
|
30
|
+
- No `reinitialize()`, HF-004 collection-reset redesign, HF-011+ dependency graph, async validation, submission lifecycle, UI adapter or performance benchmark is included.
|
|
31
|
+
|
|
32
|
+
### Verification and release status
|
|
33
|
+
|
|
34
|
+
- Accepted integration PR [#16](https://github.com/ArhiMEN/AdaptForm/pull/16) passed **563 tests in 27 files**, lint, source/demo type-checks, package/demo builds and dependency audit (0 known vulnerabilities). Independent review and [documentation sync PR #17](https://github.com/ArhiMEN/AdaptForm/pull/17) were merged. The 2.2.0 release candidate requires its **own** successful CI.
|
|
35
|
+
- The separate `tsconfig.test.json` check still has **54 existing diagnostics** and is not reported as passing; an older mapped-type `TS2589` reproduces on 2.1.1 as well. No new diagnostics were established in HF-005–HF-010.
|
|
36
|
+
- **Not published.** The source/manifest is being prepared as `2.2.0`; do not interpret this entry or a GitHub merge as npm publication, a tag or a GitHub Release. The last independently verified npm `latest` was `2.1.0`.
|
|
37
|
+
|
|
38
|
+
## [2.1.1] - Unreleased (prepared)
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **HF-002:** Defer `ArrayField` collection validation during `Form.setFields(data, false)`; with enabled validation, check the final collection once after all form values are assigned, using the parent form as context. Avoid validation against a transient empty array, while preserving the public `itemsValue` setter and its immediate validation of the completed replacement. ([PR #10](https://github.com/ArhiMEN/AdaptForm/pull/10))
|
|
43
|
+
- **HF-003:** Reject incomplete decimal strings such as `"123."`, `"0."` and `"-123."` on explicit validation, including with `gt`/`ge`/`lt`/`le` bounds. Preserve the editable input, comma normalization, existing format-error messages and complete-number precision checks. ([PR #12](https://github.com/ArhiMEN/AdaptForm/pull/12))
|
|
44
|
+
|
|
45
|
+
### Tests and verification
|
|
46
|
+
|
|
47
|
+
- Add the HF-001 lifecycle characterization baseline and update its expectations after the two targeted fixes. ([PR #8](https://github.com/ArhiMEN/AdaptForm/pull/8))
|
|
48
|
+
- The accepted HF-003 branch passed **477 tests in 21 files**, lint, source type-check, package build, package-content verification and npm audit (0 reported vulnerabilities). The 2.1.1 release candidate must pass its own CI before publication.
|
|
49
|
+
|
|
50
|
+
### Compatibility and release status
|
|
51
|
+
|
|
52
|
+
- **Intentional behavior change:** trailing-decimal values may still be entered and serialized, but no longer pass `Field.checkAndSetValid()` or `Form.checkValid()`. Applications must validate before submitting; `toJSON()` itself does not validate.
|
|
53
|
+
- Public types, `rawValue`/`valueClear` representations, serialization shapes and the optional Luxon entry point are unchanged. No new runtime dependencies.
|
|
54
|
+
- The separate test type-check `tsconfig.test.json` has **54 existing diagnostics** unchanged by HF-002/HF-003; it is not part of required CI. These are tracked as technical debt, not represented as passing.
|
|
55
|
+
- **Release not yet published.** Version `2.1.1` is being prepared; do not infer npm publication, a GitHub release or a tag from this entry.
|
|
56
|
+
|
|
57
|
+
## [2.1.0] - 2026-10-14
|
|
10
58
|
|
|
11
59
|
### Added
|
|
12
60
|
|
|
@@ -57,9 +105,9 @@ All notable changes to AdaptForm are documented here. The `2.1.0` section is pre
|
|
|
57
105
|
|
|
58
106
|
### Compatibility notes
|
|
59
107
|
|
|
60
|
-
- Built-in library validation messages and their language have not been changed in this release
|
|
108
|
+
- Built-in library validation messages and their language have not been changed in this release. Localization and any structured error-code contract are deferred to a separate coordinated task.
|
|
61
109
|
- PlayLease-specific integration work is explicitly out of scope for this branch.
|
|
62
|
-
-
|
|
110
|
+
- Version `2.1.0` was published manually to npm. No GitHub release or release tag has been created.
|
|
63
111
|
|
|
64
112
|
## [2.0.5] - 2026-07-22
|
|
65
113
|
|
package/CHANGELOG.ru.md
CHANGED
|
@@ -2,11 +2,59 @@
|
|
|
2
2
|
|
|
3
3
|
[English](CHANGELOG.md) · [Русский](CHANGELOG.ru.md)
|
|
4
4
|
|
|
5
|
-
Здесь описаны все значимые изменения AdaptForm.
|
|
5
|
+
Здесь описаны все значимые изменения AdaptForm. Версия `2.1.0` опубликована в npm 2026-10-14.
|
|
6
6
|
|
|
7
7
|
**[Интерактивное демо](https://arhimen.github.io/AdaptForm/?lang=ru)** · [Исходный код демо](https://github.com/ArhiMEN/AdaptForm/tree/master/demo) · [Документация](https://github.com/ArhiMEN/AdaptForm/blob/master/README.ru.md)
|
|
8
8
|
|
|
9
|
-
## [2.
|
|
9
|
+
## [2.2.0] — Не опубликована (исходники подготовлены)
|
|
10
|
+
|
|
11
|
+
### Добавлено — фундамент headless-состояний
|
|
12
|
+
|
|
13
|
+
- **HF-005:** Спецификация жизненного цикла полей и форм, baseline очищенных значений, touched/dirty, snapshots, синхронных batch и совместимости на EN/RU. ([контракт](docs/ai/HEADLESS_STATE_CONTRACT.ru.md))
|
|
14
|
+
- **HF-006:** `Field.isDirty` и `commitBaseline()` с семантическим сравнением Date, необязательного Luxon, данных после плагинов и поддерживаемых вложенных структур.
|
|
15
|
+
- **HF-007:** Производное `isDirty` и фиксация baseline у `Form`, `ArrayField`, `ArrayFormField`: учитываются состав, порядок коллекций и вложенные значения.
|
|
16
|
+
- **HF-008:** `subscribe(listener)`, идемпотентная отписка, стабильный неизменяемый `getSnapshot()` и публичные типы `SnapshotValue`, `NodeSnapshot`, `FieldSnapshot`, `FormSnapshot`, `ArrayFieldSnapshot`, `ArrayFormSnapshot`. Новых обязательных runtime/UI-зависимостей нет.
|
|
17
|
+
- **HF-009:** Синхронный вложенный `Form.batch(callback)` с объединёнными уведомлениями, отложенной автоматической валидацией по итоговым данным и корректным завершением при исключении. Уже выполненные изменения **не откатываются**.
|
|
18
|
+
- **HF-010:** Передача изменений от потомков и коллекций родителю, обновление связей при удалении/замене и очистка владения при отписке. ([PR #16](https://github.com/ArhiMEN/AdaptForm/pull/16))
|
|
19
|
+
|
|
20
|
+
### Исправлено
|
|
21
|
+
|
|
22
|
+
- `ArrayField.remove()/clear()` и `ArrayFormField.remove()/clear()` откладывают автоматическую проверку внутри `Form.batch()`, но проверяют синхронно вне batch; явные `checkValid()` остаются немедленными. Добавлено 34 целевых регрессии.
|
|
23
|
+
- `Form.allErrors` копирует массивы ошибок потомков, исключая мутацию и заморозку живых массивов при совмещении глобальных ошибок и snapshots.
|
|
24
|
+
|
|
25
|
+
### Совместимость и ограничения
|
|
26
|
+
|
|
27
|
+
- Сохранены прежние class-based API, синхронные проверки вне batch, `isTouched`, `isValid`, поведение `reset()` (включая состав коллекций), сериализация и необязательный `adaptform/luxon`.
|
|
28
|
+
- `commitBaseline()` меняет точку сравнения dirty, **не** цель `reset()`. После фиксации другого baseline сброс может оставить `isDirty=true`.
|
|
29
|
+
- Snapshots — независимые неизменяемые представления, а не исходные изменяемые объекты; пользовательские экземпляры классов могут быть непрозрачными. Прямые мутации прежних массивов/options и произвольная замена свойств Form не перехватываются автоматически.
|
|
30
|
+
- Нет `reinitialize()`, изменения reset коллекций (HF-004), графа зависимостей (HF-011+), асинхронных валидаторов, submit lifecycle, UI-адаптеров или бенчмарков.
|
|
31
|
+
|
|
32
|
+
### Проверки и выпуск
|
|
33
|
+
|
|
34
|
+
- Принятый [PR #16](https://github.com/ArhiMEN/AdaptForm/pull/16) прошёл **563 теста в 27 файлах**, lint, проверку типов исходников/демо, сборки библиотеки/демо и аудит (0 известных уязвимостей). Независимое ревью выполнено, [PR #17 с документацией](https://github.com/ArhiMEN/AdaptForm/pull/17) объединён. Для кандидата 2.2.0 требуется **собственный** успешный CI.
|
|
35
|
+
- Отдельная проверка `tsconfig.test.json` сохраняет **54 прежние диагностики** и не объявляется пройденной; старый `TS2589` mapped type воспроизводится также на 2.1.1. Новых диагностик в HF-005–HF-010 не установлено.
|
|
36
|
+
- **Не опубликована.** В исходниках готовится `2.2.0`, но запись changelog или merge в GitHub не равны публикации npm, тегу или GitHub Release. Последняя отдельно проверенная npm-версия `latest` — `2.1.0`.
|
|
37
|
+
|
|
38
|
+
## [2.1.1] — Не опубликована (подготовка)
|
|
39
|
+
|
|
40
|
+
### Исправлено
|
|
41
|
+
|
|
42
|
+
- **HF-002:** Проверка коллекции `ArrayField` откладывается при `Form.setFields(data, false)`. При включённой валидации завершённая коллекция проверяется один раз после присваивания всех полей, с контекстом родительской формы. Промежуточный пустой массив больше не проверяется; публичный setter `itemsValue` сохранён и проверяет итоговую замену немедленно. ([PR #10](https://github.com/ArhiMEN/AdaptForm/pull/10))
|
|
43
|
+
- **HF-003:** Незавершённые десятичные значения, например `"123."`, `"0."` и `"-123."`, отклоняются при явной проверке, в том числе при ограничениях `gt`/`ge`/`lt`/`le`. Ввод остаётся редактируемым; нормализация запятой, существующие сообщения об ошибках формата и точность завершённых чисел сохранены. ([PR #12](https://github.com/ArhiMEN/AdaptForm/pull/12))
|
|
44
|
+
|
|
45
|
+
### Тесты и проверки
|
|
46
|
+
|
|
47
|
+
- Добавлены characterization-тесты жизненного цикла HF-001 и обновлены их ожидания после двух исправлений. ([PR #8](https://github.com/ArhiMEN/AdaptForm/pull/8))
|
|
48
|
+
- В принятой ветке HF-003 прошли **477 тестов в 21 файле**, lint, типы исходного кода, сборка, проверка пакета и npm audit (0 известных уязвимостей). Кандидат 2.1.1 должен пройти собственный CI до публикации.
|
|
49
|
+
|
|
50
|
+
### Совместимость и статус
|
|
51
|
+
|
|
52
|
+
- **Намеренное изменение поведения:** строки с завершающим десятичным разделителем можно редактировать и сериализовать, но они больше не проходят `Field.checkAndSetValid()` и `Form.checkValid()`. Перед отправкой приложение должно проверить форму; `toJSON()` не запускает проверку.
|
|
53
|
+
- Публичные типы, представления `rawValue`/`valueClear`, формат сериализации и необязательная точка входа Luxon не изменены. Новых обязательных зависимостей нет.
|
|
54
|
+
- Отдельная типизация тестов `tsconfig.test.json` сохраняет **54 ранее существовавшие диагностики** после HF-002/HF-003; она не является обязательным этапом CI и не объявляется успешной.
|
|
55
|
+
- **Версия ещё не опубликована.** `2.1.1` только готовится: эта запись не означает публикацию в npm, создание GitHub Release или тега.
|
|
56
|
+
|
|
57
|
+
## [2.1.0] — 2026-10-14
|
|
10
58
|
|
|
11
59
|
### Добавлено
|
|
12
60
|
|
|
@@ -57,9 +105,9 @@
|
|
|
57
105
|
|
|
58
106
|
### Совместимость
|
|
59
107
|
|
|
60
|
-
- Встроенные сообщения валидации библиотеки и их язык в этом
|
|
108
|
+
- Встроенные сообщения валидации библиотеки и их язык в этом выпуске не изменены. Их локализация и контракт структурированных кодов ошибок отложены до отдельной согласованной задачи.
|
|
61
109
|
- Интеграция с PlayLease явно исключена из области этой ветки.
|
|
62
|
-
-
|
|
110
|
+
- Версия `2.1.0` опубликована в npm вручную. Релиз и тег GitHub не создавались.
|
|
63
111
|
|
|
64
112
|
## [2.0.5] — 2026-07-22
|
|
65
113
|
|
package/README.md
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
|
|
10
|
-
**[Open the interactive demo](https://arhimen.github.io/AdaptForm/)** · **[Read the
|
|
10
|
+
**[Open the interactive demo](https://arhimen.github.io/AdaptForm/)** · **[Read the changelog](https://github.com/ArhiMEN/AdaptForm/blob/master/CHANGELOG.md)** · **[Browse the demo source](https://github.com/ArhiMEN/AdaptForm/tree/master/demo)**
|
|
11
11
|
|
|
12
12
|
Maintainers: [automatic npm publication setup](docs/NPM_PUBLISH.md).
|
|
13
13
|
|
|
14
|
-
AdaptForm is a TypeScript-first form
|
|
14
|
+
AdaptForm is a TypeScript-first, framework-independent, class-based headless form engine inspired by Django Forms and Pydantic. Forms and fields manage values, casting, validation, errors, baselines, subscriptions and observable state without requiring UI framework components.
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
@@ -25,6 +25,9 @@ AdaptForm is a TypeScript-first form modeling library inspired by Django Forms a
|
|
|
25
25
|
- JSON and `FormData` serialization.
|
|
26
26
|
- Optional Luxon integration through the `adaptform/luxon` entry point.
|
|
27
27
|
- Framework-agnostic core with no required runtime dependencies.
|
|
28
|
+
- Clean-value baselines with `isDirty` and `commitBaseline()` on fields, forms and collections.
|
|
29
|
+
- Stable immutable `getSnapshot()` data, `subscribe()` and unsubscribe for reactive UIs.
|
|
30
|
+
- Synchronous nested `Form.batch()` and propagation of nested form/collection updates.
|
|
28
31
|
|
|
29
32
|
## Installation
|
|
30
33
|
|
|
@@ -40,7 +43,7 @@ Luxon integration is optional:
|
|
|
40
43
|
npm install luxon
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
The [interactive demo](https://arhimen.github.io/AdaptForm/) runs from repository source, with automatic deployment from `v2.1.0` and `master`.
|
|
46
|
+
The [interactive demo](https://arhimen.github.io/AdaptForm/) runs from repository source, with automatic deployment from `v2.1.0` and `master`. **Headless APIs in the 2.2.0 source candidate are not yet published to npm.** Check the [npm package](https://www.npmjs.com/package/adaptform) for available published versions.
|
|
44
47
|
|
|
45
48
|
The demo supports English and Russian with a persistent language selector: [English](https://arhimen.github.io/AdaptForm/?lang=en) · [Русский](https://arhimen.github.io/AdaptForm/?lang=ru). Switching languages preserves form data and validation state.
|
|
46
49
|
|
|
@@ -127,7 +130,7 @@ const price = new Field(DecimalType, null, {
|
|
|
127
130
|
})
|
|
128
131
|
```
|
|
129
132
|
|
|
130
|
-
Supports `gt`, `ge`, `lt`, `le`, `decimalPlaces`, and `maxDigits`. Numeric inputs with excess precision are rejected rather than silently rounded. String inputs may use a comma or a period as the decimal separator.
|
|
133
|
+
Supports `gt`, `ge`, `lt`, `le`, `decimalPlaces`, and `maxDigits`. Numeric inputs with excess precision are rejected rather than silently rounded. String inputs may use a comma or a period as the decimal separator. Incomplete values such as `"123."` retain their editable text but fail explicit validation; call `Form.checkValid()` before submitting. `toJSON()` does not validate automatically.
|
|
131
134
|
|
|
132
135
|
### BooleanType
|
|
133
136
|
|
|
@@ -180,7 +183,7 @@ Validators are synchronous; returning a Promise is not supported. For asynchrono
|
|
|
180
183
|
- If you change dependent fields one at a time, call `form.checkValid()` after the changes to recompute cross-field rules.
|
|
181
184
|
- `field.setRawValue(value, false)` and `form.setFields(data, false)` allow advanced callers to defer validation. Call `checkValid()` before relying on the validity state.
|
|
182
185
|
- `reset()` restores defaults, clears errors, and leaves validity unset until the next validation. Collection validity is false after reset until the collection is checked again.
|
|
183
|
-
- Adding, removing, or clearing collection items refreshes collection validation. Invalid removal indices are ignored.
|
|
186
|
+
- Adding, removing, or clearing collection items refreshes collection validation outside `Form.batch()`; inside a batch automatic validation is deferred. Invalid removal indices are ignored.
|
|
184
187
|
|
|
185
188
|
## Dynamic collections
|
|
186
189
|
|
|
@@ -346,10 +349,10 @@ const date = new Field(DateLuxonType, null, {
|
|
|
346
349
|
|
|
347
350
|
| Class | Main API |
|
|
348
351
|
| --- | --- |
|
|
349
|
-
| `Form` | `fields`, `setFields()`, `fieldsValue`, `checkValid()`, `isValid`, `allErrors`, `globalErrors`, `errors`, `setGlobalError()`, `clearGlobalErrors()`, `reset()`, `toJSON()`, `toFormData()` |
|
|
350
|
-
| `Field` | `rawValue`, `setRawValue()`, `valueClear`, `errors`, `error`, `isValid`, `isTouched`, `isEmpty`, `checkValid()`, `reset()` |
|
|
351
|
-
| `ArrayField` | `items`, `itemsValue`, `add()`, `remove()`, `clear()`, `checkValid()`, `reset()` |
|
|
352
|
-
| `ArrayFormField` | `forms`, `formsFieldsValue`, `add()`, `remove()`, `clear()`, `checkValid()`, `reset()` |
|
|
352
|
+
| `Form` | `fields`, `setFields()`, `fieldsValue`, `checkValid()`, `isValid`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `batch()`, `allErrors`, `globalErrors`, `errors`, `setGlobalError()`, `clearGlobalErrors()`, `reset()`, `toJSON()`, `toFormData()` |
|
|
353
|
+
| `Field` | `rawValue`, `setRawValue()`, `valueClear`, `errors`, `error`, `isValid`, `isTouched`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `isEmpty`, `checkValid()`, `reset()` |
|
|
354
|
+
| `ArrayField` | `items`, `itemsValue`, `add()`, `remove()`, `clear()`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `checkValid()`, `reset()` |
|
|
355
|
+
| `ArrayFormField` | `forms`, `formsFieldsValue`, `add()`, `remove()`, `clear()`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `checkValid()`, `reset()` |
|
|
353
356
|
| `ErrorsSchema` | `staticKey()`, `dynamicKey()`, `fallback()`, `transformValue()`, `parseErrors()`, `parseErrorsDetailed()`, `nest()`, `clone()`, `merge()`, `forArray()`, `fromObject()` |
|
|
354
357
|
|
|
355
358
|
## Development
|
|
@@ -386,3 +389,34 @@ The demo is built by GitHub Actions from `v2.1.0` and `master`. GitHub Pages is
|
|
|
386
389
|
## License
|
|
387
390
|
|
|
388
391
|
MIT © Stanislav Orlov
|
|
392
|
+
|
|
393
|
+
## Headless form state (2.2.0 source candidate)
|
|
394
|
+
|
|
395
|
+
HF-005–HF-010 were merged after independent review. The **source candidate is 2.2.0**, but npm publication is separate, so these APIs may not yet be available from `npm install adaptform`.
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
import {Field, Form, NumberType, StringType} from 'adaptform'
|
|
399
|
+
|
|
400
|
+
class SettingsForm extends Form {
|
|
401
|
+
displayName = new Field(StringType, 'Alex')
|
|
402
|
+
notifications = new Field(NumberType, 1)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const form = new SettingsForm()
|
|
406
|
+
const unsubscribe = form.subscribe(() => {
|
|
407
|
+
const snapshot = form.getSnapshot()
|
|
408
|
+
console.log(snapshot.isDirty, snapshot.isValid, snapshot.fieldsValue)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
form.batch(() => {
|
|
412
|
+
form.displayName.rawValue = 'Alexandra'
|
|
413
|
+
form.notifications.rawValue = 2
|
|
414
|
+
}) // One coherent notification after final automatic validation
|
|
415
|
+
|
|
416
|
+
form.commitBaseline() // Accept current cleaned values; isDirty becomes false
|
|
417
|
+
unsubscribe() // Release the listener
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
`isTouched` keeps its former semantics. `commitBaseline()` changes the dirty-comparison baseline, **not** the constructor defaults restored by `reset()`; a reset may leave `isDirty=true`. Snapshots are detached read-only descriptions, not mutable source objects; `toJSON()` still does not validate.
|
|
421
|
+
|
|
422
|
+
Automatic validation inside `Form.batch()` is deferred, whereas explicit `checkValid()` is immediate. Batch callbacks must be synchronous and do not roll back changes on errors. Direct in-place changes to legacy public arrays or arbitrary form properties are not automatically observed. See [usage and limitations](docs/ai/HEADLESS_STATE_USAGE.md) and the [2.1.1 compatibility table](docs/ai/HEADLESS_STATE_CONTRACT.md#compatibility-with-211).
|
package/README.ru.md
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
|
|
10
|
-
**[Открыть интерактивное демо](https://arhimen.github.io/AdaptForm/?lang=ru)** · **[История изменений
|
|
10
|
+
**[Открыть интерактивное демо](https://arhimen.github.io/AdaptForm/?lang=ru)** · **[История изменений](https://github.com/ArhiMEN/AdaptForm/blob/master/CHANGELOG.ru.md)** · **[Исходный код демо](https://github.com/ArhiMEN/AdaptForm/tree/master/demo)**
|
|
11
11
|
|
|
12
12
|
Для сопровождающих: [настройка автоматической публикации в npm](docs/NPM_PUBLISH.ru.md).
|
|
13
13
|
|
|
14
|
-
AdaptForm —
|
|
14
|
+
AdaptForm — независимый от UI-фреймворков headless-движок форм с class-based API и приоритетом TypeScript, вдохновлённый Django Forms и Pydantic. Формы и поля управляют значениями, приведением типов, валидацией, ошибками, baseline, подписками и наблюдаемым состоянием без зависимости от UI-компонентов.
|
|
15
15
|
|
|
16
16
|
## Возможности
|
|
17
17
|
|
|
@@ -25,6 +25,9 @@ AdaptForm — библиотека моделирования форм с при
|
|
|
25
25
|
- Сериализация в JSON и `FormData`.
|
|
26
26
|
- Необязательная интеграция с Luxon через точку входа `adaptform/luxon`.
|
|
27
27
|
- Независимое от фреймворков ядро без обязательных зависимостей времени выполнения.
|
|
28
|
+
- Исходные очищенные значения с `isDirty` и `commitBaseline()` у полей, форм и коллекций.
|
|
29
|
+
- Стабильные неизменяемые снимки `getSnapshot()`, подписки и отписка через `subscribe()`.
|
|
30
|
+
- Вложенный синхронный `Form.batch()` и распространение обновлений вложенных форм и коллекций.
|
|
28
31
|
|
|
29
32
|
## Установка
|
|
30
33
|
|
|
@@ -40,7 +43,7 @@ npm install adaptform
|
|
|
40
43
|
npm install luxon
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
[Интерактивное демо](https://arhimen.github.io/AdaptForm/?lang=ru) использует исходный код репозитория и автоматически публикуется из `v2.1.0` и `master`.
|
|
46
|
+
[Интерактивное демо](https://arhimen.github.io/AdaptForm/?lang=ru) использует исходный код репозитория и автоматически публикуется из `v2.1.0` и `master`. **Новые headless API в кандидатных исходниках 2.2.0 ещё не опубликованы в npm.** Доступные для установки версии проверяйте на [npm](https://www.npmjs.com/package/adaptform).
|
|
44
47
|
|
|
45
48
|
Демо поддерживает английский и русский языки с сохранением выбора: [English](https://arhimen.github.io/AdaptForm/?lang=en) · [Русский](https://arhimen.github.io/AdaptForm/?lang=ru). Переключение языка сохраняет данные формы и состояние валидации.
|
|
46
49
|
|
|
@@ -127,7 +130,7 @@ const price = new Field(DecimalType, null, {
|
|
|
127
130
|
})
|
|
128
131
|
```
|
|
129
132
|
|
|
130
|
-
Поддерживаются `gt`, `ge`, `lt`, `le`, `decimalPlaces` и `maxDigits`. Числовые значения с избыточной точностью отклоняются без скрытого округления. В строковых значениях десятичным разделителем может быть запятая или точка.
|
|
133
|
+
Поддерживаются `gt`, `ge`, `lt`, `le`, `decimalPlaces` и `maxDigits`. Числовые значения с избыточной точностью отклоняются без скрытого округления. В строковых значениях десятичным разделителем может быть запятая или точка. Незавершённые значения вроде `"123."` сохраняются при редактировании, но не проходят явную проверку; перед отправкой вызывайте `Form.checkValid()`. Метод `toJSON()` не выполняет валидацию автоматически.
|
|
131
134
|
|
|
132
135
|
### BooleanType
|
|
133
136
|
|
|
@@ -180,7 +183,7 @@ class PasswordForm extends Form {
|
|
|
180
183
|
- Если зависимые поля меняются по отдельности, после изменений вызовите `form.checkValid()` для пересчёта зависимых правил.
|
|
181
184
|
- `field.setRawValue(value, false)` и `form.setFields(data, false)` позволяют отложить валидацию. Прежде чем полагаться на корректность формы, вызовите `checkValid()`.
|
|
182
185
|
- `reset()` восстанавливает значения по умолчанию, очищает ошибки и оставляет корректность неопределённой до следующей проверки. Корректность коллекции после сброса равна false до её повторной проверки.
|
|
183
|
-
- Добавление, удаление и очистка элементов
|
|
186
|
+
- Добавление, удаление и очистка элементов проверяют коллекции вне `Form.batch()`; внутри batch автоматическая валидация откладывается. Некорректные индексы удаления игнорируются.
|
|
184
187
|
|
|
185
188
|
## Динамические коллекции
|
|
186
189
|
|
|
@@ -346,10 +349,10 @@ const date = new Field(DateLuxonType, null, {
|
|
|
346
349
|
|
|
347
350
|
| Класс | Основной API |
|
|
348
351
|
| --- | --- |
|
|
349
|
-
| `Form` | `fields`, `setFields()`, `fieldsValue`, `checkValid()`, `isValid`, `allErrors`, `globalErrors`, `errors`, `setGlobalError()`, `clearGlobalErrors()`, `reset()`, `toJSON()`, `toFormData()` |
|
|
350
|
-
| `Field` | `rawValue`, `setRawValue()`, `valueClear`, `errors`, `error`, `isValid`, `isTouched`, `isEmpty`, `checkValid()`, `reset()` |
|
|
351
|
-
| `ArrayField` | `items`, `itemsValue`, `add()`, `remove()`, `clear()`, `checkValid()`, `reset()` |
|
|
352
|
-
| `ArrayFormField` | `forms`, `formsFieldsValue`, `add()`, `remove()`, `clear()`, `checkValid()`, `reset()` |
|
|
352
|
+
| `Form` | `fields`, `setFields()`, `fieldsValue`, `checkValid()`, `isValid`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `batch()`, `allErrors`, `globalErrors`, `errors`, `setGlobalError()`, `clearGlobalErrors()`, `reset()`, `toJSON()`, `toFormData()` |
|
|
353
|
+
| `Field` | `rawValue`, `setRawValue()`, `valueClear`, `errors`, `error`, `isValid`, `isTouched`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `isEmpty`, `checkValid()`, `reset()` |
|
|
354
|
+
| `ArrayField` | `items`, `itemsValue`, `add()`, `remove()`, `clear()`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `checkValid()`, `reset()` |
|
|
355
|
+
| `ArrayFormField` | `forms`, `formsFieldsValue`, `add()`, `remove()`, `clear()`, `isDirty`, `commitBaseline()`, `subscribe()`, `getSnapshot()`, `checkValid()`, `reset()` |
|
|
353
356
|
| `ErrorsSchema` | `staticKey()`, `dynamicKey()`, `fallback()`, `transformValue()`, `parseErrors()`, `parseErrorsDetailed()`, `nest()`, `clone()`, `merge()`, `forArray()`, `fromObject()` |
|
|
354
357
|
|
|
355
358
|
## Разработка
|
|
@@ -386,3 +389,34 @@ npm run dev -- --config demo/vite.config.ts
|
|
|
386
389
|
## Лицензия
|
|
387
390
|
|
|
388
391
|
MIT © Stanislav Orlov
|
|
392
|
+
|
|
393
|
+
## Headless-состояния форм (кандидат исходников 2.2.0)
|
|
394
|
+
|
|
395
|
+
HF-005–HF-010 прошли независимое ревью и объединены. **Кандидат исходников — 2.2.0**, а публикация в npm — отдельный этап, поэтому новые API могут отсутствовать в пакете, установленном через `npm install adaptform`.
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
import {Field, Form, NumberType, StringType} from 'adaptform'
|
|
399
|
+
|
|
400
|
+
class SettingsForm extends Form {
|
|
401
|
+
displayName = new Field(StringType, 'Alex')
|
|
402
|
+
notifications = new Field(NumberType, 1)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const form = new SettingsForm()
|
|
406
|
+
const unsubscribe = form.subscribe(() => {
|
|
407
|
+
const snapshot = form.getSnapshot()
|
|
408
|
+
console.log(snapshot.isDirty, snapshot.isValid, snapshot.fieldsValue)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
form.batch(() => {
|
|
412
|
+
form.displayName.rawValue = 'Alexandra'
|
|
413
|
+
form.notifications.rawValue = 2
|
|
414
|
+
}) // One coherent notification after final automatic validation
|
|
415
|
+
|
|
416
|
+
form.commitBaseline() // Accept current cleaned values; isDirty becomes false
|
|
417
|
+
unsubscribe() // Release the listener
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
`isTouched` сохраняет прежнюю семантику. `commitBaseline()` меняет точку сравнения dirty, **не** значения конструктора, возвращаемые `reset()`; после сброса `isDirty` иногда остаётся `true`. Snapshots — независимые неизменяемые представления, а не ссылки на изменяемые данные. `toJSON()` по-прежнему не выполняет валидацию.
|
|
421
|
+
|
|
422
|
+
Автоматическая проверка внутри `Form.batch()` откладывается, явные `checkValid()` выполняются сразу. Batch только синхронный; при исключении выполненные изменения не откатываются. Прямые мутации старых публичных массивов и произвольная замена свойств формы не создают автоматических уведомлений. Подробности: [использование и ограничения](docs/ai/HEADLESS_STATE_USAGE.ru.md), [совместимость с 2.1.1](docs/ai/HEADLESS_STATE_CONTRACT.ru.md#совместимость-с-211).
|
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
/** Shared mutation boundary; subclasses only describe state and their mutations. */
|
|
2
|
+
declare abstract class ObservableNode<TSnapshot> {
|
|
3
|
+
private readonly _stateStore;
|
|
4
|
+
protected abstract captureState(): TSnapshot;
|
|
5
|
+
protected stateChildren(): readonly ObservableNode<unknown>[];
|
|
6
|
+
private contains;
|
|
7
|
+
protected validateAfterMutation(callback: () => unknown): void;
|
|
8
|
+
protected batchUpdate<TResult>(callback: () => TResult, validate: () => unknown): TResult;
|
|
9
|
+
subscribe(listener: () => void): () => void;
|
|
10
|
+
getSnapshot(): TSnapshot;
|
|
11
|
+
protected update<TResult>(callback: () => TResult): TResult;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Snapshots contain data only: mutable built-ins and opaque instances are described. */
|
|
15
|
+
type SnapshotValue = null | undefined | string | number | boolean | bigint | readonly SnapshotValue[] | {
|
|
16
|
+
readonly [key: string]: SnapshotValue;
|
|
17
|
+
};
|
|
18
|
+
interface NodeSnapshot {
|
|
19
|
+
readonly isValid: boolean | null;
|
|
20
|
+
readonly isTouched: boolean;
|
|
21
|
+
readonly isDirty: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface FieldSnapshot extends NodeSnapshot {
|
|
24
|
+
readonly rawValue: SnapshotValue;
|
|
25
|
+
readonly valueClear: SnapshotValue;
|
|
26
|
+
readonly errors: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
interface FormSnapshot extends NodeSnapshot {
|
|
29
|
+
readonly fieldsValue: Readonly<Record<string, SnapshotValue>>;
|
|
30
|
+
readonly allErrors: Readonly<Record<string, readonly string[]>>;
|
|
31
|
+
readonly globalErrors: Readonly<Record<string, string>>;
|
|
32
|
+
readonly fields: Readonly<Record<string, FieldSnapshot | ArrayFieldSnapshot | ArrayFormSnapshot>>;
|
|
33
|
+
}
|
|
34
|
+
interface ArrayFieldSnapshot extends NodeSnapshot {
|
|
35
|
+
readonly itemsValue: readonly SnapshotValue[];
|
|
36
|
+
readonly errors: readonly string[];
|
|
37
|
+
readonly items: readonly FieldSnapshot[];
|
|
38
|
+
}
|
|
39
|
+
interface ArrayFormSnapshot extends NodeSnapshot {
|
|
40
|
+
readonly formsFieldsValue: readonly Readonly<Record<string, SnapshotValue>>[];
|
|
41
|
+
readonly errors: readonly string[];
|
|
42
|
+
readonly forms: readonly FormSnapshot[];
|
|
43
|
+
}
|
|
44
|
+
|
|
1
45
|
interface FieldPlugin<T = any> {
|
|
2
46
|
pluginOptions?: PluginOptions;
|
|
3
47
|
init?(field: Field<T>): void;
|
|
@@ -9,15 +53,23 @@ declare abstract class BasePlugin<T = any> implements FieldPlugin<T> {
|
|
|
9
53
|
constructor(pluginOptions?: PluginOptions);
|
|
10
54
|
}
|
|
11
55
|
|
|
12
|
-
declare class ArrayField<T = any> {
|
|
56
|
+
declare class ArrayField<T = any> extends ObservableNode<ArrayFieldSnapshot> {
|
|
13
57
|
protected factory: (value?: T) => Field<T>;
|
|
58
|
+
protected captureState(): ArrayFieldSnapshot;
|
|
14
59
|
protected options: ArrayFieldOptions;
|
|
15
60
|
protected _items: Field<T>[];
|
|
16
61
|
protected _errors: string[];
|
|
62
|
+
protected stateChildren(): readonly ObservableNode<unknown>[];
|
|
63
|
+
private _baseline;
|
|
17
64
|
private _hasValidationResult;
|
|
18
|
-
|
|
65
|
+
private _isTouched;
|
|
66
|
+
get isTouched(): boolean;
|
|
67
|
+
set isTouched(value: boolean);
|
|
19
68
|
constructor(factory: (value?: T) => Field<T>, options?: ArrayFieldOptions, initial?: (T | null)[]);
|
|
69
|
+
get isDirty(): boolean;
|
|
70
|
+
commitBaseline(): void;
|
|
20
71
|
set itemsValue(items: (T | null)[]);
|
|
72
|
+
setItemsValue(items: (T | null)[], validate?: boolean): void;
|
|
21
73
|
get itemsValue(): (T | null)[];
|
|
22
74
|
get items(): Field<T>[];
|
|
23
75
|
add(value?: T, force_check?: boolean): Field<T>;
|
|
@@ -33,14 +85,21 @@ declare class ArrayField<T = any> {
|
|
|
33
85
|
reset(): void;
|
|
34
86
|
}
|
|
35
87
|
|
|
36
|
-
declare class ArrayFormField<T extends Form = Form> {
|
|
88
|
+
declare class ArrayFormField<T extends Form = Form> extends ObservableNode<ArrayFormSnapshot> {
|
|
37
89
|
private FormConstructor;
|
|
38
90
|
private options;
|
|
91
|
+
protected captureState(): ArrayFormSnapshot;
|
|
39
92
|
private _forms;
|
|
40
93
|
private _errors;
|
|
94
|
+
protected stateChildren(): readonly ObservableNode<unknown>[];
|
|
95
|
+
private _baseline;
|
|
41
96
|
private _hasValidationResult;
|
|
42
|
-
|
|
97
|
+
private _isTouched;
|
|
98
|
+
get isTouched(): boolean;
|
|
99
|
+
set isTouched(value: boolean);
|
|
43
100
|
constructor(FormConstructor: new () => T, options?: ArrayFormFieldOptions, initial?: unknown[]);
|
|
101
|
+
get isDirty(): boolean;
|
|
102
|
+
commitBaseline(): void;
|
|
44
103
|
set forms(forms: unknown[]);
|
|
45
104
|
setForms(forms: unknown[], validate?: boolean): void;
|
|
46
105
|
get forms(): T[];
|
|
@@ -170,15 +229,21 @@ declare abstract class FieldType<T, O extends FieldOptions = FieldOptions> {
|
|
|
170
229
|
}
|
|
171
230
|
|
|
172
231
|
type FieldTypeConstructor<T, O extends FieldOptions = FieldOptions> = new (options: O) => FieldType<T, O>;
|
|
173
|
-
declare class Field<T = any, O extends FieldOptions = FieldOptions> {
|
|
232
|
+
declare class Field<T = any, O extends FieldOptions = FieldOptions> extends ObservableNode<FieldSnapshot> {
|
|
174
233
|
protected _rawValue: any;
|
|
175
234
|
protected _valueClear: T | null;
|
|
176
235
|
protected fieldType: FieldType<T, O>;
|
|
177
236
|
protected defaultValue: T | null;
|
|
237
|
+
private _baseline;
|
|
178
238
|
protected _errors: string[];
|
|
179
|
-
|
|
180
|
-
|
|
239
|
+
private _isValid;
|
|
240
|
+
get isValid(): boolean | null;
|
|
241
|
+
set isValid(value: boolean | null);
|
|
242
|
+
private _isTouched;
|
|
243
|
+
get isTouched(): boolean;
|
|
244
|
+
set isTouched(value: boolean);
|
|
181
245
|
constructor(TypeConstructor: FieldTypeConstructor<T, O>, defaultValue?: T | null, options?: O | null);
|
|
246
|
+
protected captureState(): FieldSnapshot;
|
|
182
247
|
protected get plugins(): FieldPlugin<any>[];
|
|
183
248
|
get options(): O;
|
|
184
249
|
set options(options: O);
|
|
@@ -190,6 +255,8 @@ declare class Field<T = any, O extends FieldOptions = FieldOptions> {
|
|
|
190
255
|
get error(): string;
|
|
191
256
|
set error(message: string | null);
|
|
192
257
|
get valueClear(): T | null;
|
|
258
|
+
get isDirty(): boolean;
|
|
259
|
+
commitBaseline(): void;
|
|
193
260
|
private isRequired;
|
|
194
261
|
checkValid(form?: Form): boolean;
|
|
195
262
|
checkAndSetValid(form?: Form): boolean;
|
|
@@ -280,7 +347,10 @@ type FormFieldValue<TField> = TField extends Field<infer TValue, infer _Options>
|
|
|
280
347
|
type FormFieldsValue<TForm extends Form> = {
|
|
281
348
|
[K in keyof TForm as TForm[K] extends Field<infer _Value, infer _Options> | ArrayField<infer _ArrayValue> | ArrayFormField<infer _NestedForm> ? K : never]: FormFieldValue<TForm[K]>;
|
|
282
349
|
};
|
|
283
|
-
declare abstract class Form {
|
|
350
|
+
declare abstract class Form extends ObservableNode<FormSnapshot> {
|
|
351
|
+
protected captureState(): FormSnapshot;
|
|
352
|
+
protected stateChildren(): readonly ObservableNode<unknown>[];
|
|
353
|
+
batch<TResult>(callback: () => TResult & (TResult extends PromiseLike<unknown> ? never : unknown)): TResult;
|
|
284
354
|
private _globalErrors;
|
|
285
355
|
errorsShema: ErrorsSchema;
|
|
286
356
|
protected get _fields(): Record<string, Field<any> | ArrayField<any> | ArrayFormField<any>>;
|
|
@@ -305,6 +375,8 @@ declare abstract class Form {
|
|
|
305
375
|
setGlobalError(key: string, message: string): void;
|
|
306
376
|
clearGlobalErrors(): void;
|
|
307
377
|
reset(): void;
|
|
378
|
+
get isDirty(): boolean;
|
|
379
|
+
commitBaseline(): void;
|
|
308
380
|
get isTouched(): boolean;
|
|
309
381
|
get isValid(): boolean;
|
|
310
382
|
toFormData(): FormData;
|
|
@@ -312,4 +384,4 @@ declare abstract class Form {
|
|
|
312
384
|
private serializeJsonValue;
|
|
313
385
|
}
|
|
314
386
|
|
|
315
|
-
export { ArrayField as A, BasePlugin as B, type DecimalOptions as D, ErrorsSchema as E, Field as F, type MaskPluginOptions as M, type NumberOptions as N, type PluginOptions as P, type StringOptions as S, FieldType as a, type BooleanOptions as b, type DateOptions as c, type ArrayFieldOptions as d,
|
|
387
|
+
export { ArrayField as A, BasePlugin as B, type messagesOptions as C, type DecimalOptions as D, ErrorsSchema as E, Field as F, type messagesStringOptions as G, type MaskPluginOptions as M, type NumberOptions as N, type PluginOptions as P, type StringOptions as S, FieldType as a, type BooleanOptions as b, type DateOptions as c, type ArrayFieldOptions as d, type ArrayFieldSnapshot as e, ArrayFormField as f, type ArrayFormFieldOptions as g, type ArrayFormSnapshot as h, DefaultErrorsSchema as i, type DynamicRule as j, type FieldOptions as k, type FieldPlugin as l, type FieldSnapshot as m, Form as n, type FormFieldValue as o, type FormFieldsValue as p, type FormSnapshot as q, type NodeSnapshot as r, type ParsedError as s, type SnapshotValue as t, type messagesArrayFieldOptions as u, type messagesArrayFormFieldOptions as v, type messagesBooleanOptions as w, type messagesDateOptions as x, type messagesDecimalOptions as y, type messagesNumberOptions as z };
|