@reformer/core 11.0.0-beta.8 → 11.0.0-beta.9
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/README.md +91 -73
- package/dist/behaviors-value-HA_Q03hc.js +108 -0
- package/dist/behaviors.js +370 -242
- package/dist/{create-model-BBRHhHwK.js → create-model-BvcbNPgL.js} +6 -6
- package/dist/date-utils-90-qZbse.js +43 -0
- package/dist/{en-CjAqn7h0.js → en-HP8XEych.js} +6 -3
- package/dist/form/aggregate-signals.d.ts +21 -63
- package/dist/form/behaviors/collections.d.ts +28 -7
- package/dist/form/behaviors/context.d.ts +19 -2
- package/dist/form/behaviors/index.d.ts +3 -3
- package/dist/form/behaviors/internals.d.ts +9 -2
- package/dist/form/behaviors/node.d.ts +25 -2
- package/dist/form/behaviors/operators.d.ts +17 -9
- package/dist/form/behaviors/schema-operators.d.ts +6 -4
- package/dist/form/behaviors/types.d.ts +12 -4
- package/dist/form/create-form.d.ts +47 -35
- package/dist/form/flow/flow.d.ts +87 -0
- package/dist/form/form-bundle.d.ts +45 -28
- package/dist/form/form-config-base.d.ts +46 -0
- package/dist/form/form-level.d.ts +121 -0
- package/dist/form/form-proxy-builder.d.ts +6 -22
- package/dist/form/index.d.ts +16 -21
- package/dist/form/nodes/form-array-node.d.ts +100 -0
- package/dist/form/nodes/form-control-node.d.ts +73 -0
- package/dist/form/nodes/form-group-node.d.ts +66 -0
- package/dist/form/nodes/node-kind.d.ts +29 -0
- package/dist/form/nodes/state/availability.d.ts +6 -0
- package/dist/form/nodes/state/component.d.ts +6 -0
- package/dist/form/nodes/state/index.d.ts +12 -0
- package/dist/form/nodes/state/interaction.d.ts +13 -0
- package/dist/form/nodes/state/issues.d.ts +9 -0
- package/dist/form/nodes/state/pending.d.ts +6 -0
- package/dist/form/schema/compile.d.ts +59 -0
- package/dist/form/schema/compiled.d.ts +143 -0
- package/dist/form/schema/row-key.d.ts +11 -0
- package/dist/form/schema-controller.d.ts +30 -14
- package/dist/form/signal-node-registry.d.ts +6 -7
- package/dist/form/submission.d.ts +93 -0
- package/dist/form/type-guards.d.ts +14 -73
- package/dist/form/types/contracts.d.ts +19 -92
- package/dist/form/types/field-config.d.ts +27 -0
- package/dist/form/types/index.d.ts +5 -57
- package/dist/form/types/nodes.d.ts +159 -0
- package/dist/form/types/schema-node.d.ts +139 -63
- package/dist/form/types/validation-schema.d.ts +23 -27
- package/dist/form/validation/context.d.ts +42 -7
- package/dist/form/validation/index.d.ts +9 -8
- package/dist/form/validation/operators.d.ts +22 -30
- package/dist/form/validation/run.d.ts +93 -18
- package/dist/form/validation/strategy.d.ts +41 -13
- package/dist/form/validation/types.d.ts +74 -20
- package/dist/form/validators/date-utils.d.ts +13 -1
- package/dist/form/validators/email.d.ts +8 -7
- package/dist/form/validators/file-type.d.ts +8 -7
- package/dist/form/validators/file-utils.d.ts +2 -1
- package/dist/form/validators/future-date.d.ts +8 -7
- package/dist/form/validators/index.d.ts +1 -1
- package/dist/form/validators/integer.d.ts +8 -7
- package/dist/form/validators/is-date.d.ts +11 -9
- package/dist/form/validators/is-number.d.ts +11 -9
- package/dist/form/validators/max-age.d.ts +13 -9
- package/dist/form/validators/max-date.d.ts +8 -7
- package/dist/form/validators/max-file-size.d.ts +8 -7
- package/dist/form/validators/max-files.d.ts +8 -7
- package/dist/form/validators/max-length.d.ts +14 -7
- package/dist/form/validators/max-total-file-size.d.ts +8 -7
- package/dist/form/validators/max.d.ts +10 -8
- package/dist/form/validators/min-age.d.ts +12 -9
- package/dist/form/validators/min-date.d.ts +8 -7
- package/dist/form/validators/min-file-size.d.ts +8 -7
- package/dist/form/validators/min-files.d.ts +11 -9
- package/dist/form/validators/min-length.d.ts +14 -7
- package/dist/form/validators/min.d.ts +10 -8
- package/dist/form/validators/multiple-of.d.ts +10 -8
- package/dist/form/validators/non-negative.d.ts +8 -7
- package/dist/form/validators/non-zero.d.ts +8 -7
- package/dist/form/validators/past-date.d.ts +8 -7
- package/dist/form/validators/pattern.d.ts +9 -8
- package/dist/form/validators/phone.d.ts +8 -7
- package/dist/form/validators/required.d.ts +11 -9
- package/dist/form/validators/url.d.ts +20 -13
- package/dist/form/validators/validation-issue.d.ts +8 -0
- package/dist/i18n/descriptor.d.ts +1 -1
- package/dist/i18n/en.json.d.ts +5 -1
- package/dist/i18n/locale-store.d.ts +7 -0
- package/dist/i18n/message-format.d.ts +24 -4
- package/dist/i18n/ru.json.d.ts +5 -1
- package/dist/i18n/translator.d.ts +11 -0
- package/dist/i18n/validation-message.d.ts +42 -7
- package/dist/i18n.d.ts +7 -12
- package/dist/i18n.js +66 -283
- package/dist/index.d.ts +16 -7
- package/dist/index.js +924 -2855
- package/dist/loader-BWkigpVe.js +52 -0
- package/dist/locale/en.js +1 -1
- package/dist/locale/en.json +4 -1
- package/dist/locale/ru.js +4 -1
- package/dist/locale/ru.json +4 -1
- package/dist/locale-CG54AU2Q.js +219 -0
- package/dist/locale.js +1 -1
- package/dist/model/behaviors-value.d.ts +24 -6
- package/dist/model/create-model.d.ts +2 -2
- package/dist/model/derived-registry.d.ts +1 -1
- package/dist/model/index.d.ts +11 -5
- package/dist/model/model-nodes.d.ts +79 -12
- package/dist/model/model-signals-proxy.d.ts +5 -6
- package/dist/model/model-value-proxy.d.ts +20 -15
- package/dist/model/safe-effect.d.ts +33 -1
- package/dist/model/types.d.ts +63 -13
- package/dist/model.js +20 -18
- package/dist/node-kind-C-r6ZMdo.js +15 -0
- package/dist/platforms/react/field-binding/field-adapter.d.ts +1 -1
- package/dist/platforms/react/field-binding/field-handle.d.ts +5 -2
- package/dist/platforms/react/form-bundle-context.d.ts +2 -2
- package/dist/platforms/react/hooks/types.d.ts +57 -38
- package/dist/platforms/react/hooks/use-form-bundle.d.ts +19 -4
- package/dist/platforms/react/hooks/use-form-flow.d.ts +40 -0
- package/dist/platforms/react/hooks/use-form-validation.d.ts +12 -6
- package/dist/platforms/react/hooks/useArrayLength.d.ts +6 -6
- package/dist/platforms/react/hooks/useFormControl.d.ts +7 -9
- package/dist/platforms/react/hooks/useFormControlValue.d.ts +18 -19
- package/dist/platforms/react/hooks/useSignalSubscription.d.ts +9 -1
- package/dist/platforms/react/i18n/context.d.ts +6 -1
- package/dist/platforms/react/i18n/hooks.d.ts +16 -10
- package/dist/platforms/react/i18n/provider.d.ts +9 -2
- package/dist/platforms/react/index.d.ts +6 -4
- package/dist/react.d.ts +22 -0
- package/dist/react.js +505 -0
- package/dist/runtime-token.d.ts +1 -2
- package/dist/safe-effect-BbtX3KuN.js +643 -0
- package/dist/schema-controller-CpiO2JoK.js +143 -0
- package/dist/signals.d.ts +15 -9
- package/dist/signals.js +11 -6
- package/dist/strategy-CoFSemuP.js +375 -0
- package/dist/type-guards-D-N_5Ddh.js +19 -0
- package/dist/validation-issue-B9mjI8jz.js +7 -0
- package/dist/validation-message-Ced9MGxs.js +194 -0
- package/dist/validation.js +9 -9
- package/dist/validators/email.js +5 -5
- package/dist/validators/future-date.js +8 -8
- package/dist/validators/integer.js +3 -3
- package/dist/validators/is-date.js +4 -4
- package/dist/validators/is-number.js +4 -4
- package/dist/validators/max-age.js +8 -9
- package/dist/validators/max-date.js +8 -8
- package/dist/validators/max-length.js +7 -7
- package/dist/validators/max.js +4 -4
- package/dist/validators/min-age.js +10 -8
- package/dist/validators/min-date.js +3 -3
- package/dist/validators/min-length.js +5 -5
- package/dist/validators/min.js +4 -4
- package/dist/validators/multiple-of.js +11 -7
- package/dist/validators/non-negative.js +3 -3
- package/dist/validators/non-zero.js +3 -3
- package/dist/validators/past-date.js +8 -8
- package/dist/validators/pattern.js +5 -5
- package/dist/validators/phone.js +11 -9
- package/dist/validators/required.js +2 -2
- package/dist/validators/url.js +48 -12
- package/dist/validators.js +17 -17
- package/docs/llms/01-api-reference.md +60 -34
- package/docs/llms/02-quick-start.md +111 -80
- package/docs/llms/03-api-signatures.md +368 -153
- package/docs/llms/04-common-patterns.md +58 -56
- package/docs/llms/05-common-mistakes.md +69 -44
- package/docs/llms/06-troubleshooting.md +14 -9
- package/docs/llms/07-complete-import.md +74 -43
- package/docs/llms/08-form-types.md +11 -4
- package/docs/llms/09-formschema.md +129 -81
- package/docs/llms/10-arrays.md +106 -75
- package/docs/llms/11-async-watchfield.md +2 -2
- package/docs/llms/13-multi-step.md +134 -63
- package/docs/llms/14-extended-mistakes.md +20 -17
- package/docs/llms/15-project-structure.md +221 -74
- package/docs/llms/16-ui-components.md +55 -42
- package/docs/llms/17-nonexistent-api.md +199 -32
- package/docs/llms/18-conditional-fields.md +66 -32
- package/docs/llms/19-reading-values.md +15 -3
- package/docs/llms/20-compute-vs-watch.md +16 -10
- package/docs/llms/21-array-operations.md +51 -27
- package/docs/llms/22-cycle-detection.md +1 -1
- package/docs/llms/23-copy-from.md +2 -2
- package/docs/llms/24-sync-fields.md +6 -5
- package/docs/llms/25-reset-when.md +14 -7
- package/docs/llms/26-transform-value.md +4 -4
- package/docs/llms/27-revalidate-when.md +16 -15
- package/docs/llms/28-submit-and-reset.md +276 -128
- package/docs/llms/29-async-preload.md +39 -29
- package/docs/llms/30-type-safety-recipes.md +105 -51
- package/docs/llms/31-async-validator-debounce.md +96 -53
- package/docs/llms/33-validation-strategy.md +177 -65
- package/docs/llms/34-i18n.md +69 -43
- package/docs/llms/35-unified-contract.md +100 -39
- package/docs/llms/36-issue-messages.md +284 -0
- package/docs/llms/37-compiled-schema.md +332 -0
- package/docs/llms/38-form-flow.md +397 -0
- package/llms-index.json +1 -1
- package/llms.txt +6084 -3420
- package/package.json +23 -4
- package/dist/behaviors-value-C7dSEhCc.js +0 -134
- package/dist/date-utils-dTT_x_Be.js +0 -31
- package/dist/form/create-core-form.d.ts +0 -72
- package/dist/form/factories/index.d.ts +0 -6
- package/dist/form/factories/node-factory.d.ts +0 -189
- package/dist/form/form-submitter.d.ts +0 -121
- package/dist/form/nodes/array-node.d.ts +0 -276
- package/dist/form/nodes/field-node.d.ts +0 -297
- package/dist/form/nodes/form-node.d.ts +0 -326
- package/dist/form/nodes/group-node.d.ts +0 -284
- package/dist/form/nodes/model-array-node.d.ts +0 -126
- package/dist/form/nodes/subscription-manager.d.ts +0 -180
- package/dist/form/status-machine.d.ts +0 -115
- package/dist/form/types/deep-schema.d.ts +0 -107
- package/dist/form/types/form-proxy.d.ts +0 -141
- package/dist/form/unique-id.d.ts +0 -45
- package/dist/form/validation/config.d.ts +0 -73
- package/dist/form/validation/error-handler.d.ts +0 -166
- package/dist/form/validators/validation-error.d.ts +0 -8
- package/dist/index-BrNkQj1y.js +0 -105
- package/dist/loader-Dv_Hl7JC.js +0 -216
- package/dist/model-value-proxy-caTmAiUd.js +0 -419
- package/dist/node-BeV_lV5i.js +0 -100
- package/dist/strategy-DgzELWno.js +0 -204
- package/dist/validation-error-B-U3RsV0.js +0 -11
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
## 3. COMMON PATTERNS
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Значения живут в модели (`model.$.field`), поведение (`defineFormBehavior`) работает на живых
|
|
4
|
+
сигналах, валидация — отдельная схема (`defineValidation`, прогон по требованию через
|
|
5
|
+
раннер). Слои раздельны: схема формы правил НЕ несёт.
|
|
6
6
|
|
|
7
7
|
### Conditional Fields with Auto-Reset
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// поле включается по условию; при выключении — сброс к initial
|
|
13
|
-
enableWhen(model.$.propertyValue, () => model.loanType === 'mortgage', {
|
|
14
|
-
resetOnDisable: true,
|
|
15
|
-
});
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Или декларативно внутри `defineFormBehavior`:
|
|
9
|
+
`enableWhen` — оператор поведения: поле включается по условию, при выключении сбрасывается к
|
|
10
|
+
initial. Вызывается внутри `defineFormBehavior`:
|
|
19
11
|
|
|
20
12
|
```typescript
|
|
21
13
|
import { defineFormBehavior, enableWhen } from '@reformer/core/behaviors';
|
|
@@ -77,14 +69,14 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
|
|
|
77
69
|
});
|
|
78
70
|
```
|
|
79
71
|
|
|
80
|
-
> Низкоуровневый аналог — примитив `watchField(model.$.region, cb)` из `@reformer/core` (без debounce/AbortSignal;
|
|
72
|
+
> Низкоуровневый аналог — примитив `watchField(model.$.region, cb)` из `@reformer/core/model` (без debounce/AbortSignal;
|
|
81
73
|
> для сети предпочтителен `onChange`). См. `20-compute-vs-watch.md`, `32-async-options-loading.md`.
|
|
82
74
|
|
|
83
75
|
### Reading values
|
|
84
76
|
|
|
85
77
|
```typescript
|
|
86
|
-
// В React-компоненте — хуки
|
|
87
|
-
const { value,
|
|
78
|
+
// В React-компоненте — хуки (@reformer/core/react)
|
|
79
|
+
const { value, issues, disabled } = useFormControl(form.email);
|
|
88
80
|
const loanType = useFormControlValue(form.loanType); // значение напрямую
|
|
89
81
|
|
|
90
82
|
// Вне React — из модели
|
|
@@ -96,50 +88,56 @@ model.get(); // весь объект-снимок
|
|
|
96
88
|
|
|
97
89
|
### Submit + validation
|
|
98
90
|
|
|
99
|
-
Валидация — отдельная `
|
|
100
|
-
`
|
|
101
|
-
|
|
91
|
+
Валидация — отдельная `ValidationDefinition<T>` (`defineValidation`), а не часть layout; в сборку
|
|
92
|
+
она уходит полем `validation`. `bundle.submission.submit(handler)` прогоняет все правила один раз,
|
|
93
|
+
разносит замечания по нодам и зовёт обработчик только при статусе `valid` (`severity: 'warning'` не
|
|
94
|
+
блокирует). Результат — `SubmitResult`.
|
|
102
95
|
|
|
103
96
|
```typescript
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
model.reset(); // к initial-снимку
|
|
97
|
+
async function handleSubmit(event: React.FormEvent) {
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
const outcome = await bundle.submission.submit(async (values) => {
|
|
100
|
+
await api.send(values);
|
|
101
|
+
bundle.model.reset(); // к initial-снимку — только после успеха
|
|
102
|
+
});
|
|
103
|
+
if (outcome.status === 'invalid') showHint('Проверьте форму'); // поля уже подсвечены
|
|
112
104
|
}
|
|
113
105
|
```
|
|
114
106
|
|
|
115
|
-
|
|
116
|
-
`
|
|
117
|
-
|
|
107
|
+
Без сборки — раннер `validateModel(model, schema)`: сам разносит замечания по нодам и отвечает
|
|
108
|
+
`Promise<boolean>`.
|
|
109
|
+
|
|
110
|
+
Multi-step: шаги и их правила — поток сборки `createForm({ flow: { steps: [{ id, rules }] } })`
|
|
111
|
+
(`id` — `selector` шага), правила вне шагов — `validation: { extras }`. Сборка отдаёт
|
|
112
|
+
`bundle.validateStep(id)`, `bundle.validate()` и поток `bundle.flow` (`next` / `previous` / `goTo`);
|
|
113
|
+
визард принимает поток пропом `flow`. См. `13-multi-step.md`, `28-submit-and-reset.md`,
|
|
114
|
+
`38-form-flow.md`.
|
|
118
115
|
|
|
119
116
|
### Cross-field validation — через `cross`
|
|
120
117
|
|
|
121
|
-
Cross-field правило — обычная функция над
|
|
122
|
-
|
|
118
|
+
Cross-field правило — обычная функция над **снимком** модели. Оператор `cross` берётся из
|
|
119
|
+
аргумента схемы и вешает правило на поле-носитель ошибки; тип снимка выведен из схемы:
|
|
123
120
|
|
|
124
121
|
```typescript
|
|
125
|
-
import {
|
|
122
|
+
import { defineValidation, validate } from '@reformer/core/validation';
|
|
126
123
|
import { required, min } from '@reformer/core/validators';
|
|
127
|
-
import type {
|
|
124
|
+
import type { ValidationIssue } from '@reformer/core';
|
|
128
125
|
|
|
129
|
-
//
|
|
130
|
-
const initialPaymentVsProperty = (
|
|
131
|
-
|
|
132
|
-
? { code: '
|
|
126
|
+
// снимок формы читается напрямую — без каста, соседние поля доступны как поля объекта
|
|
127
|
+
const initialPaymentVsProperty = (form: MyForm): ValidationIssue | null =>
|
|
128
|
+
form.initialPayment && form.propertyValue && form.initialPayment > form.propertyValue
|
|
129
|
+
? { code: 'initialPaymentTooHigh' } // текст кода — в словаре формы (defineIssueMessages)
|
|
133
130
|
: null;
|
|
134
131
|
|
|
135
|
-
const schema =
|
|
132
|
+
const schema = defineValidation<MyForm>(({ model, cross }) => {
|
|
136
133
|
validate(model.$.initialPayment, [required(), min(0)]);
|
|
137
134
|
cross(model.$.initialPayment, initialPaymentVsProperty); // ошибка сядет на initialPayment
|
|
138
135
|
});
|
|
139
136
|
```
|
|
140
137
|
|
|
141
|
-
>
|
|
142
|
-
>
|
|
138
|
+
> Правила строки массива и подформы — отдельная схема над их типом, подключённая через
|
|
139
|
+
> `applyEach(model.$.items, rowRules)` / `apply(model.$.group, groupRules)`. У такой схемы своя
|
|
140
|
+
> область: её `cross` получает снимок строки или группы.
|
|
143
141
|
|
|
144
142
|
### Conditional validation — `validateWhen`
|
|
145
143
|
|
|
@@ -148,21 +146,25 @@ const schema = defineValidationSchema<MyForm>(({ model }) => {
|
|
|
148
146
|
включение/выключение самих проверок:
|
|
149
147
|
|
|
150
148
|
```typescript
|
|
151
|
-
import { validateWhen, validate
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
149
|
+
import { defineValidation, validateWhen, validate } from '@reformer/core/validation';
|
|
150
|
+
|
|
151
|
+
const loanRules = defineValidation<MyForm>(({ model, cross }) => {
|
|
152
|
+
validateWhen(
|
|
153
|
+
() => model.loanType === 'mortgage',
|
|
154
|
+
() => {
|
|
155
|
+
validate(model.$.propertyValue, [required(), min(1000000)]);
|
|
156
|
+
cross(model.$.initialPayment, initialPaymentVsProperty);
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
### Перезапуск прогона — `revalidateWhen` (мост поведение → валидация)
|
|
163
163
|
|
|
164
|
-
Схема прогоняется по требованию (submit/шаг)
|
|
165
|
-
`
|
|
164
|
+
Схема прогоняется по требованию (submit/шаг) либо живой стратегией сборки
|
|
165
|
+
(`validation: { strategy: 'change' }`, см. `33-validation-strategy.md`). Чтобы перезапустить
|
|
166
|
+
отдельную схему при изменении зависимости — `revalidateWhen` из **поведения** (мост
|
|
167
|
+
поведение → валидация):
|
|
166
168
|
|
|
167
169
|
```typescript
|
|
168
170
|
import { defineFormBehavior, revalidateWhen } from '@reformer/core/behaviors';
|
|
@@ -176,11 +178,11 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
|
|
|
176
178
|
### Extracting named rules
|
|
177
179
|
|
|
178
180
|
Когда тело правила растёт — выноси в именованную функцию. Field-правило типизируй `Rule<T>`
|
|
179
|
-
(`(value) =>
|
|
181
|
+
(`(value) => ValidationIssue | null`), cross-field — обычной `(f: Root) => ValidationIssue | null`.
|
|
180
182
|
Схема остаётся плоской и читается как оглавление:
|
|
181
183
|
|
|
182
184
|
```typescript
|
|
183
|
-
import {
|
|
185
|
+
import { defineValidation, validate } from '@reformer/core/validation';
|
|
184
186
|
import { required } from '@reformer/core/validators';
|
|
185
187
|
import type { Rule } from '@reformer/core/validation';
|
|
186
188
|
|
|
@@ -188,10 +190,10 @@ import type { Rule } from '@reformer/core/validation';
|
|
|
188
190
|
const validateAdultAge: Rule<string> = (value) => {
|
|
189
191
|
if (!value) return null;
|
|
190
192
|
const age = new Date().getFullYear() - new Date(value).getFullYear();
|
|
191
|
-
return age < 18 ? { code: 'tooYoung'
|
|
193
|
+
return age < 18 ? { code: 'tooYoung' } : null;
|
|
192
194
|
};
|
|
193
195
|
|
|
194
|
-
const schema =
|
|
196
|
+
const schema = defineValidation<MyForm>(({ model }) => {
|
|
195
197
|
validate(model.$.birthDate, [required(), validateAdultAge]);
|
|
196
198
|
});
|
|
197
199
|
```
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
### Imports rule (#1 cause of cascading errors — read first)
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
5
|
+
- Модель/форма/типы (`createForm`, `FormModel`, `FormSchemaNode`, `ValidationIssue`, `FormFlow`) — из `@reformer/core`.
|
|
6
|
+
- Хуки (`useFormBundle`, `useFormControl`, `useFormFlow`, `useIssueMessage`, …) и `I18nProvider` — из `@reformer/core/react`.
|
|
7
|
+
- Схема валидации (`defineValidation` + операторы `validate`/`validateAsync`/`validateWhen`/`apply`/`applyEach`) и раннер `validateModel` — из `@reformer/core/validation`. `cross` берётся из аргумента схемы.
|
|
7
8
|
- Чистые фабрики валидаторов (`required`/`min`/`email`/…) — из `@reformer/core/validators`.
|
|
9
|
+
- Словарь текстов замечаний (`defineIssueMessages`, `resolveIssueMessage`) — из `@reformer/core/i18n`.
|
|
8
10
|
- Декларативный DSL поведения (`defineFormBehavior` + операторы) — из `@reformer/core/behaviors`.
|
|
9
11
|
|
|
10
12
|
```typescript
|
|
@@ -12,40 +14,42 @@
|
|
|
12
14
|
import {
|
|
13
15
|
createModel,
|
|
14
16
|
createForm,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type FormProxy,
|
|
18
|
-
type FieldConfig,
|
|
17
|
+
type FormGroup,
|
|
18
|
+
type FormSchemaNode,
|
|
19
19
|
type FormModel,
|
|
20
|
-
type
|
|
20
|
+
type ValidationIssue,
|
|
21
21
|
} from '@reformer/core';
|
|
22
|
+
import { useFormBundle, useFormControl, useFormControlValue } from '@reformer/core/react';
|
|
23
|
+
import { defineIssueMessages } from '@reformer/core/i18n';
|
|
22
24
|
// схема валидации + раннер — отдельный слой:
|
|
23
25
|
import {
|
|
24
|
-
|
|
26
|
+
defineValidation,
|
|
25
27
|
validate,
|
|
26
28
|
validateAsync,
|
|
27
29
|
validateWhen,
|
|
28
|
-
cross,
|
|
29
|
-
each,
|
|
30
30
|
apply,
|
|
31
|
+
applyEach,
|
|
31
32
|
validateModel,
|
|
32
33
|
type Rule,
|
|
33
34
|
type AsyncRule,
|
|
34
|
-
type
|
|
35
|
+
type ValidationDefinition,
|
|
35
36
|
} from '@reformer/core/validation';
|
|
36
37
|
import { required, min, max, email } from '@reformer/core/validators';
|
|
37
|
-
//
|
|
38
|
-
import {
|
|
39
|
-
//
|
|
40
|
-
import {
|
|
38
|
+
// поведение формы — декларативный DSL:
|
|
39
|
+
import { defineFormBehavior, compute, enableWhen, copyFrom, onChange } from '@reformer/core/behaviors';
|
|
40
|
+
// примитивы над сигналами (вне формы, возвращают cleanup) — отдельный сабпат:
|
|
41
|
+
import { computeFrom, watchField } from '@reformer/core/model';
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
>
|
|
44
|
+
> **В корне `@reformer/core` операторов поведения нет.** `computeFrom`, `copyFrom`, `watchField`,
|
|
45
|
+
> `transformValue`, `resetWhen`, `syncFields`, `revalidateWhen` — примитивы сабпата
|
|
46
|
+
> `@reformer/core/model`; `enableWhen` / `disableWhen` — только операторы `@reformer/core/behaviors`.
|
|
44
47
|
> В DSL для реакции на изменения используется `onChange`.
|
|
45
48
|
|
|
46
49
|
### Значения — в модели, не в форме
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
Источник истины значений — модель. Форма (ноды) отражает их; `form.x.setValue(v)` пишет в ту же
|
|
52
|
+
модель.
|
|
49
53
|
|
|
50
54
|
```typescript
|
|
51
55
|
// ✅ читаем/пишем значение
|
|
@@ -68,7 +72,7 @@ const { value: loanType } = useFormControlValue(control.loanType);
|
|
|
68
72
|
const loanType = useFormControlValue(control.loanType);
|
|
69
73
|
|
|
70
74
|
// ALSO CORRECT - useFormControl returns object
|
|
71
|
-
const { value,
|
|
75
|
+
const { value, issues } = useFormControl(control.loanType);
|
|
72
76
|
```
|
|
73
77
|
|
|
74
78
|
### Behaviors принимают СИГНАЛЫ, а не пути/форму
|
|
@@ -83,33 +87,41 @@ enableWhen(model.$.city, () => Boolean(model.country), { resetOnDisable: true })
|
|
|
83
87
|
computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => price * qty);
|
|
84
88
|
```
|
|
85
89
|
|
|
86
|
-
### Валидаторы — оператор `validate`, а не
|
|
90
|
+
### Валидаторы — оператор `validate`, а не узел схемы
|
|
87
91
|
|
|
88
|
-
Валидация — **отдельный слой** (`@reformer/core/validation`), а не
|
|
89
|
-
(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
Валидация — **отдельный слой** (`@reformer/core/validation`), а не ключ узла. Схема формы
|
|
93
|
+
(билдер `createForm` / JSON-DSL) правил **не несёт**: они живут в `ValidationDefinition` и уходят в
|
|
94
|
+
сборку полем `validation` (шаги — `flow`). Запуск — уровнем формы:
|
|
95
|
+
`bundle.submission.submit(handler)`, `bundle.validate()`, `bundle.validateStep(id)`; без сборки —
|
|
96
|
+
`validateModel(model, schema)`. У нод методов `validate()` / `submit()` нет.
|
|
92
97
|
|
|
93
98
|
```typescript
|
|
94
|
-
// ❌ WRONG -
|
|
99
|
+
// ❌ WRONG - ключей value и validators у узла нет, как и записи «имя → узел»
|
|
95
100
|
const schema = {
|
|
96
101
|
email: { value: model.$.email, component: Input, validators: [required(), email()] },
|
|
97
102
|
};
|
|
98
103
|
validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
|
|
104
|
+
required({ message: 'Email is required' }); // опции message нет — текст живёт в словаре формы
|
|
99
105
|
|
|
100
|
-
// ✅ CORRECT - операторы validate/validateAsync внутри
|
|
101
|
-
const emailSchema =
|
|
102
|
-
validate(model.$.email, [required({
|
|
106
|
+
// ✅ CORRECT - операторы validate/validateAsync внутри defineValidation; у правила — код
|
|
107
|
+
const emailSchema = defineValidation<MyForm>(({ model }) => {
|
|
108
|
+
validate(model.$.email, [required({ code: 'email.required' }), email()]);
|
|
103
109
|
validateAsync(model.$.email, [
|
|
104
110
|
async (value, { signal }) => {
|
|
105
111
|
const res = await fetch(`/api/free?email=${value}`, { signal });
|
|
106
|
-
return (await res.json()).free ? null : { code: 'taken'
|
|
112
|
+
return (await res.json()).free ? null : { code: 'email.taken' };
|
|
107
113
|
},
|
|
108
114
|
]);
|
|
109
115
|
});
|
|
116
|
+
// тексты кодов — словарь формы
|
|
117
|
+
export const formMessages = defineIssueMessages({
|
|
118
|
+
'email.required': 'Укажите email',
|
|
119
|
+
'email.taken': 'Email уже занят',
|
|
120
|
+
});
|
|
110
121
|
|
|
111
|
-
// прогон
|
|
112
|
-
|
|
122
|
+
// прогон — уровнем формы: замечания сами доезжают до нод, warnings не блокируют.
|
|
123
|
+
// Отклонённое async-правило — сбой: статус `error`, на поле замечание `ruleFailed`.
|
|
124
|
+
const outcome = await bundle.submission.submit(api.send); // createForm({ …, validation: emailSchema })
|
|
113
125
|
```
|
|
114
126
|
|
|
115
127
|
### Cross-field — оператор `cross`, а не `ctx.form`
|
|
@@ -124,44 +136,57 @@ watchField(path.amount, (amount, ctx) => {
|
|
|
124
136
|
// ✅ CORRECT - compute (поведение) на сигналах; cross-field (валидация) через оператор cross
|
|
125
137
|
compute(model.$.total, () => (model.amount ?? 0) * (model.rate ?? 0));
|
|
126
138
|
|
|
127
|
-
// cross-field правило — обычная функция над
|
|
128
|
-
const amountVsMax = (
|
|
129
|
-
|
|
130
|
-
? { code: 'tooBig',
|
|
139
|
+
// cross-field правило — обычная функция над снимком модели; `cross` берётся из аргумента схемы:
|
|
140
|
+
const amountVsMax = (form: MyForm): ValidationIssue | null =>
|
|
141
|
+
form.amount != null && form.maxAmount != null && form.amount > form.maxAmount
|
|
142
|
+
? { code: 'amount.tooBig', params: { max: form.maxAmount } }
|
|
131
143
|
: null;
|
|
132
144
|
|
|
133
|
-
const amountSchema =
|
|
134
|
-
cross(model.$.amount, amountVsMax); //
|
|
145
|
+
const amountSchema = defineValidation<MyForm>(({ model, cross }) => {
|
|
146
|
+
cross(model.$.amount, amountVsMax); // check получает снимок модели, ошибка — на model.$.amount
|
|
135
147
|
});
|
|
136
148
|
```
|
|
137
149
|
|
|
138
|
-
### Form-shape types — `
|
|
150
|
+
### Form-shape types — `interface` или `type`
|
|
151
|
+
|
|
152
|
+
Форму данных можно объявлять и `interface`, и `type` — оба работают с `createModel<T>`,
|
|
153
|
+
`createForm<T>`, `FormGroup<T>` и `FormArray<T>`. Группу от поля `FormGroup` отличает по виду
|
|
154
|
+
значения (обычный объект, а не массив, дата, файл или функция), а не по index signature. Эталонная
|
|
155
|
+
форма (`CreditApplication`) объявлена через `interface`.
|
|
139
156
|
|
|
140
157
|
```typescript
|
|
141
|
-
//
|
|
158
|
+
// ✅ interface
|
|
142
159
|
export interface PropertyItem {
|
|
143
160
|
type: PropertyType;
|
|
144
161
|
description: string;
|
|
145
162
|
}
|
|
146
163
|
|
|
147
|
-
// ✅ type alias
|
|
164
|
+
// ✅ type alias — то же самое
|
|
148
165
|
export type PropertyItem = {
|
|
149
166
|
type: PropertyType;
|
|
150
167
|
description: string;
|
|
151
168
|
};
|
|
152
169
|
```
|
|
153
170
|
|
|
171
|
+
Единственная оговорка — свои обобщённые хелперы: `interface` не присваивается
|
|
172
|
+
`Record<string, unknown>` (у него нет неявной index signature). Ограничение пиши через
|
|
173
|
+
`T extends object`, а не `T extends Record<string, unknown>`.
|
|
174
|
+
|
|
154
175
|
`number | null` — конвенциональный тип для очищенного поля; встроенные валидаторы
|
|
155
176
|
(`min`, `max`, `minLength`, `maxLength`, `minDate`, `maxDate`, `minAge`, `maxAge`) пропускают
|
|
156
177
|
пустые значения внутри.
|
|
157
178
|
|
|
158
|
-
###
|
|
179
|
+
### Вид ноды — гардами, а не instanceof
|
|
180
|
+
|
|
181
|
+
`FormControl`, `FormGroup`, `FormArray` — интерфейсы: классов нод в публичном API нет, проверка
|
|
182
|
+
`instanceof` не скомпилируется. Вид ноды различают гарды по метке вида:
|
|
159
183
|
|
|
160
184
|
```typescript
|
|
161
|
-
// ❌
|
|
162
|
-
if (node instanceof
|
|
185
|
+
// ❌ FormControl — тип, а не класс
|
|
186
|
+
if (node instanceof FormControl) { ... }
|
|
163
187
|
|
|
164
188
|
// ✅ type guards
|
|
165
|
-
import {
|
|
166
|
-
if (
|
|
189
|
+
import { isFormNode, isFormControl, isFormGroup, isFormArray } from '@reformer/core';
|
|
190
|
+
if (isFormControl(node)) node.updateComponentProps({ placeholder: 'Email' });
|
|
191
|
+
if (isFormArray(node)) node.push();
|
|
167
192
|
```
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
## 5. TROUBLESHOOTING
|
|
2
2
|
|
|
3
|
-
| Error
|
|
4
|
-
|
|
|
5
|
-
| `'
|
|
6
|
-
| `
|
|
7
|
-
| `
|
|
8
|
-
| `
|
|
9
|
-
| `
|
|
10
|
-
|
|
|
11
|
-
|
|
|
3
|
+
| Error | Cause | Solution |
|
|
4
|
+
| -------------------------------------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
5
|
+
| `'message' does not exist in type 'RuleOptions'` | Текст передан в правило — у правил текста нет | `required({ code: 'phone.required' })` + запись `'phone.required'` в `defineIssueMessages`; см. 36-issue-messages.md |
|
|
6
|
+
| `'string' is not assignable to 'RuleOptions'` | Валидатору передали строку вместо options | `required({ code: 'field.required' })` |
|
|
7
|
+
| `Module has no exported member` | Неверный источник импорта | хуки и `I18nProvider` — из `@reformer/core/react`; `watchField`/примитивы — из `@reformer/core/model`; DSL — из `@reformer/core/behaviors`; фабрики — из `@reformer/core/validators`; удалённое — 17-nonexistent-api.md |
|
|
8
|
+
| `undefined` из `useFormControlValue` | Деструктурировали хук | `const v = useFormControlValue(...)` — без деструктуризации |
|
|
9
|
+
| `enableWhen`/`disableWhen` не срабатывает | Поле не материализовано в форме (элемент массива) | Убедись, что поле есть в схеме `createForm`; для per-item — `applyEach` |
|
|
10
|
+
| `Cycle detected` | Взаимные `compute`/`computeFrom` без стабилизации | Разорви цикл через условие `when` или `peek`; см. 22-cycle-detection.md |
|
|
11
|
+
| Значение поля не пишется | Пишем в форму вместо модели | Значения принадлежат модели: `model.field = ...` / `model.$.field.value = ...` / `form.field.setValue(...)` |
|
|
12
|
+
| Замечания не появляются после submit | Отправка в обход проверки (`api.send(model.get())`) | `await bundle.submission.submit(handler)` — один прогон правил, при отказе вся форма помечается тронутой; без сборки — `await validateModel(model, schema)` |
|
|
13
|
+
| На поле текст вида `loanAmount.required` | Свой код правила без записи в словаре формы | Добавь код в `defineIssueMessages` и подключи `ValidationMessagesProvider messages` (cdk) |
|
|
14
|
+
| `compileSchema: в области «#» селектор «X» повторяется` | Два узла с одним `selector` в одной области схемы | Разные селекторы; см. 37-compiled-schema.md |
|
|
15
|
+
| `createForm: шага «X» нет в потоке` / `FormFlow: шага «X» нет` | `validateStep(id)` / `flow.goTo(id)` с неизвестным шагом | `id` — из `flow.steps`; см. 38-form-flow.md |
|
|
16
|
+
| `flow.goTo(id)` вернул `reason: 'locked'` | Прыжок вперёд через непройденный шаг | Вперёд — `flow.next()`; `goTo` — назад и на открытые шаги |
|
|
@@ -1,63 +1,82 @@
|
|
|
1
1
|
## Import Patterns
|
|
2
2
|
|
|
3
3
|
```typescript
|
|
4
|
-
// Модель, форма,
|
|
4
|
+
// Модель, форма, уровень формы, типы — из @reformer/core. React здесь нет: корень работает и на сервере
|
|
5
5
|
import {
|
|
6
|
-
// фабрики
|
|
7
6
|
createModel,
|
|
8
7
|
createForm,
|
|
9
|
-
|
|
8
|
+
createFormFromModel,
|
|
9
|
+
disposeForm, // снять форму из createFormFromModel; сборку снимает bundle.dispose()
|
|
10
|
+
createSubmission, // отправка без сборки (визард в разметке без createForm)
|
|
11
|
+
arrayOf, // массив модели с шаблоном новой строки
|
|
12
|
+
compileSchema, // авторское дерево → CompiledSchema (обычно не нужен: createForm компилирует сам)
|
|
13
|
+
createFormFlow, // поток шагов без сборки
|
|
14
|
+
isFormControl,
|
|
15
|
+
isFormGroup,
|
|
16
|
+
isFormArray,
|
|
17
|
+
} from '@reformer/core';
|
|
18
|
+
|
|
19
|
+
// Привязка к React — хуки, контекст сборки, связка поля с контролом, провайдер локали — из /react
|
|
20
|
+
import {
|
|
21
|
+
useFormBundle,
|
|
10
22
|
useFormControl,
|
|
11
23
|
useFormControlValue,
|
|
12
24
|
useArrayLength,
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
transformValue,
|
|
20
|
-
resetWhen,
|
|
21
|
-
syncFields,
|
|
22
|
-
revalidateWhen,
|
|
23
|
-
} from '@reformer/core';
|
|
25
|
+
useFormFlow, // снимок потока шагов для рендера
|
|
26
|
+
useFormValidation, // стратегия без сборки: submit() — попытка отправки, validate() — проверка
|
|
27
|
+
useIssueMessage, // текст замечания: словарь формы → локаль → английский → код
|
|
28
|
+
I18nProvider,
|
|
29
|
+
} from '@reformer/core/react';
|
|
30
|
+
import type { FieldControlState } from '@reformer/core/react';
|
|
24
31
|
|
|
25
32
|
// Типы
|
|
26
33
|
import type {
|
|
27
|
-
FormModel,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
FormModel, // реактивная модель данных
|
|
35
|
+
FormBundle, // результат createForm: { model, form, render, flow, validate, validateStep, submission, dispose, … }
|
|
36
|
+
FormNode, // общий контракт ноды
|
|
37
|
+
FormControl, // нода одного поля
|
|
38
|
+
FormGroup, // нода группы — тип формы для props компонентов
|
|
39
|
+
FormArray, // нода массива под-форм
|
|
40
|
+
ModelArray, // реактивный массив модели (push/removeAt/at/map/length)
|
|
41
|
+
ModelSignals, // дерево ручек ($)
|
|
42
|
+
PathAwareSignal, // ручка, знающая свой путь
|
|
43
|
+
ValidationIssue, // { code, params?, severity? } — без текста
|
|
44
|
+
RuleOptions, // { code?, params? } — опции фабрик правил
|
|
45
|
+
FormSchemaNode, // узел схемы: поле | массив под-форм | подформа | контейнер
|
|
46
|
+
CompiledSchema, // скомпилированная схема: bundle.render.tree
|
|
47
|
+
FormValidation, // правила формы как данные: { rules?, extras?, strategy?, debounce? }
|
|
48
|
+
FormFlowConfig, // поток шагов в конфиге: { steps: [{ id, rules? }] }
|
|
49
|
+
FormFlow, // поток шагов сборки: current, next(), previous(), goTo(id)
|
|
50
|
+
FlowTransition, // итог перехода: moved | stayed
|
|
51
|
+
FormSubmission, // отправка сборки (bundle.submission): submit(handler), submitting
|
|
52
|
+
SubmitResult, // итог bundle.submission.submit: submitted | invalid | error | cancelled | failed
|
|
39
53
|
} from '@reformer/core';
|
|
40
54
|
|
|
41
55
|
// Схема валидации: операторы + раннер — из /validation
|
|
42
56
|
import {
|
|
43
|
-
|
|
57
|
+
defineValidation,
|
|
44
58
|
validate,
|
|
45
59
|
validateAsync,
|
|
46
60
|
validateWhen,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
apply, // правила подформы: apply(model.$.group, rules); композиция: apply(rulesA, rulesB)
|
|
62
|
+
applyEach, // правила строк массива: applyEach(model.$.items, rules)
|
|
63
|
+
validateModel, // раннер без сборки: сбор + разнос, Promise<boolean>
|
|
64
|
+
runValidation, // только сбор: Promise<ValidationResult>
|
|
65
|
+
applyValidationResult, // разнос собранного результата по нодам
|
|
51
66
|
} from '@reformer/core/validation';
|
|
52
67
|
import type {
|
|
53
|
-
Rule,
|
|
54
|
-
AsyncRule,
|
|
55
|
-
|
|
68
|
+
Rule, // (value) => ValidationIssue | null
|
|
69
|
+
AsyncRule, // (value, { signal }) => Promise<ValidationIssue | null>
|
|
70
|
+
ValidationDefinition, // ({ model, cross }) => void — cross берётся из аргумента схемы
|
|
71
|
+
ValidationResult, // { status, issues, failures }
|
|
56
72
|
} from '@reformer/core/validation';
|
|
57
73
|
|
|
58
74
|
// Валидаторы — чистые фабрики из /validators (кладутся в validate(sig, [...]))
|
|
59
75
|
import { required, min, max, email, minLength, pattern } from '@reformer/core/validators';
|
|
60
76
|
|
|
77
|
+
// Тексты замечаний и локаль без React — из /i18n
|
|
78
|
+
import { defineIssueMessages, resolveIssueMessage, createI18n } from '@reformer/core/i18n';
|
|
79
|
+
|
|
61
80
|
// Декларативный DSL behaviors — из /behaviors
|
|
62
81
|
import {
|
|
63
82
|
defineFormBehavior,
|
|
@@ -75,25 +94,37 @@ import {
|
|
|
75
94
|
applyEach,
|
|
76
95
|
aggregateInto,
|
|
77
96
|
exclusiveFlag,
|
|
97
|
+
hideWhen, // операторы узлов схемы — исполняет рендерер
|
|
98
|
+
renderEffect,
|
|
78
99
|
} from '@reformer/core/behaviors';
|
|
100
|
+
|
|
101
|
+
// Примитивы над сигналами (принимают сигналы, возвращают cleanup) — из /model, в корне их нет
|
|
102
|
+
import { computeFrom, copyFrom, watchField } from '@reformer/core/model';
|
|
103
|
+
// Утилиты значений модели — тоже из /model
|
|
104
|
+
import {
|
|
105
|
+
deepEqual, // сравнение значений модели (на нём isDirty)
|
|
106
|
+
deferredWritesSettled, // дождаться отложенных записей поведения тика; undefined — ждать нечего
|
|
107
|
+
} from '@reformer/core/model';
|
|
79
108
|
```
|
|
80
109
|
|
|
81
|
-
###
|
|
110
|
+
### Форма данных — `interface` или `type`
|
|
82
111
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
112
|
+
Оба варианта работают одинаково: `createModel<T>`, `createForm<T>`, `FormGroup<T>` и `FormArray<T>`
|
|
113
|
+
принимают и `interface`, и `type`-alias — для корня, вложенных групп и элементов массивов. Группу
|
|
114
|
+
от поля сборка отличает по виду значения (обычный объект — группа; массив объектов — массив
|
|
115
|
+
под-форм; примитив, массив примитивов, дата, файл — поле), а не по index signature.
|
|
86
116
|
|
|
87
117
|
```typescript
|
|
88
|
-
export
|
|
118
|
+
export interface AddressForm {
|
|
89
119
|
street: string;
|
|
90
120
|
city: string;
|
|
91
|
-
}
|
|
121
|
+
}
|
|
92
122
|
|
|
93
|
-
export
|
|
123
|
+
export interface CoBorrower {
|
|
94
124
|
fullName: string;
|
|
95
125
|
phone: string;
|
|
96
|
-
}
|
|
126
|
+
}
|
|
97
127
|
```
|
|
98
128
|
|
|
99
|
-
|
|
129
|
+
Оговорка для своих обобщённых хелперов: `interface` не присваивается `Record<string, unknown>`, поэтому
|
|
130
|
+
ограничение пиши `T extends object`. См. `30-type-safety-recipes.md`.
|