@reformer/core 1.0.0-beta.3
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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/behaviors.d.ts +2 -0
- package/dist/behaviors.js +230 -0
- package/dist/core/behavior/behavior-applicator.d.ts +71 -0
- package/dist/core/behavior/behavior-applicator.js +92 -0
- package/dist/core/behavior/behavior-context.d.ts +29 -0
- package/dist/core/behavior/behavior-context.js +38 -0
- package/dist/core/behavior/behavior-registry.d.ts +97 -0
- package/dist/core/behavior/behavior-registry.js +198 -0
- package/dist/core/behavior/behaviors/compute-from.d.ts +41 -0
- package/dist/core/behavior/behaviors/compute-from.js +84 -0
- package/dist/core/behavior/behaviors/copy-from.d.ts +31 -0
- package/dist/core/behavior/behaviors/copy-from.js +64 -0
- package/dist/core/behavior/behaviors/enable-when.d.ts +49 -0
- package/dist/core/behavior/behaviors/enable-when.js +81 -0
- package/dist/core/behavior/behaviors/index.d.ts +11 -0
- package/dist/core/behavior/behaviors/index.js +11 -0
- package/dist/core/behavior/behaviors/reset-when.d.ts +51 -0
- package/dist/core/behavior/behaviors/reset-when.js +63 -0
- package/dist/core/behavior/behaviors/revalidate-when.d.ts +30 -0
- package/dist/core/behavior/behaviors/revalidate-when.js +51 -0
- package/dist/core/behavior/behaviors/sync-fields.d.ts +28 -0
- package/dist/core/behavior/behaviors/sync-fields.js +66 -0
- package/dist/core/behavior/behaviors/transform-value.d.ts +120 -0
- package/dist/core/behavior/behaviors/transform-value.js +110 -0
- package/dist/core/behavior/behaviors/watch-field.d.ts +35 -0
- package/dist/core/behavior/behaviors/watch-field.js +56 -0
- package/dist/core/behavior/compose-behavior.d.ts +106 -0
- package/dist/core/behavior/compose-behavior.js +166 -0
- package/dist/core/behavior/create-field-path.d.ts +20 -0
- package/dist/core/behavior/create-field-path.js +69 -0
- package/dist/core/behavior/index.d.ts +12 -0
- package/dist/core/behavior/index.js +17 -0
- package/dist/core/behavior/types.d.ts +152 -0
- package/dist/core/behavior/types.js +7 -0
- package/dist/core/context/form-context-impl.d.ts +29 -0
- package/dist/core/context/form-context-impl.js +37 -0
- package/dist/core/factories/index.d.ts +6 -0
- package/dist/core/factories/index.js +6 -0
- package/dist/core/factories/node-factory.d.ts +209 -0
- package/dist/core/factories/node-factory.js +281 -0
- package/dist/core/nodes/array-node.d.ts +308 -0
- package/dist/core/nodes/array-node.js +534 -0
- package/dist/core/nodes/field-node.d.ts +269 -0
- package/dist/core/nodes/field-node.js +510 -0
- package/dist/core/nodes/form-node.d.ts +342 -0
- package/dist/core/nodes/form-node.js +343 -0
- package/dist/core/nodes/group-node/field-registry.d.ts +191 -0
- package/dist/core/nodes/group-node/field-registry.js +215 -0
- package/dist/core/nodes/group-node/index.d.ts +11 -0
- package/dist/core/nodes/group-node/index.js +11 -0
- package/dist/core/nodes/group-node/proxy-builder.d.ts +71 -0
- package/dist/core/nodes/group-node/proxy-builder.js +161 -0
- package/dist/core/nodes/group-node/state-manager.d.ts +184 -0
- package/dist/core/nodes/group-node/state-manager.js +265 -0
- package/dist/core/nodes/group-node.d.ts +494 -0
- package/dist/core/nodes/group-node.js +770 -0
- package/dist/core/types/deep-schema.d.ts +78 -0
- package/dist/core/types/deep-schema.js +11 -0
- package/dist/core/types/field-path.d.ts +42 -0
- package/dist/core/types/field-path.js +4 -0
- package/dist/core/types/form-context.d.ts +83 -0
- package/dist/core/types/form-context.js +25 -0
- package/dist/core/types/group-node-proxy.d.ts +135 -0
- package/dist/core/types/group-node-proxy.js +31 -0
- package/dist/core/types/index.d.ts +163 -0
- package/dist/core/types/index.js +4 -0
- package/dist/core/types/validation-schema.d.ts +104 -0
- package/dist/core/types/validation-schema.js +10 -0
- package/dist/core/utils/create-form.d.ts +61 -0
- package/dist/core/utils/create-form.js +24 -0
- package/dist/core/utils/debounce.d.ts +160 -0
- package/dist/core/utils/debounce.js +197 -0
- package/dist/core/utils/error-handler.d.ts +180 -0
- package/dist/core/utils/error-handler.js +226 -0
- package/dist/core/utils/field-path-navigator.d.ts +240 -0
- package/dist/core/utils/field-path-navigator.js +374 -0
- package/dist/core/utils/index.d.ts +14 -0
- package/dist/core/utils/index.js +14 -0
- package/dist/core/utils/registry-helpers.d.ts +50 -0
- package/dist/core/utils/registry-helpers.js +79 -0
- package/dist/core/utils/registry-stack.d.ts +69 -0
- package/dist/core/utils/registry-stack.js +86 -0
- package/dist/core/utils/resources.d.ts +41 -0
- package/dist/core/utils/resources.js +69 -0
- package/dist/core/utils/subscription-manager.d.ts +180 -0
- package/dist/core/utils/subscription-manager.js +214 -0
- package/dist/core/utils/type-guards.d.ts +116 -0
- package/dist/core/utils/type-guards.js +169 -0
- package/dist/core/validation/core/apply-when.d.ts +28 -0
- package/dist/core/validation/core/apply-when.js +41 -0
- package/dist/core/validation/core/apply.d.ts +63 -0
- package/dist/core/validation/core/apply.js +38 -0
- package/dist/core/validation/core/index.d.ts +8 -0
- package/dist/core/validation/core/index.js +8 -0
- package/dist/core/validation/core/validate-async.d.ts +42 -0
- package/dist/core/validation/core/validate-async.js +45 -0
- package/dist/core/validation/core/validate-tree.d.ts +35 -0
- package/dist/core/validation/core/validate-tree.js +37 -0
- package/dist/core/validation/core/validate.d.ts +32 -0
- package/dist/core/validation/core/validate.js +38 -0
- package/dist/core/validation/field-path.d.ts +43 -0
- package/dist/core/validation/field-path.js +147 -0
- package/dist/core/validation/index.d.ts +21 -0
- package/dist/core/validation/index.js +33 -0
- package/dist/core/validation/validate-form.d.ts +85 -0
- package/dist/core/validation/validate-form.js +152 -0
- package/dist/core/validation/validation-applicator.d.ts +89 -0
- package/dist/core/validation/validation-applicator.js +217 -0
- package/dist/core/validation/validation-context.d.ts +47 -0
- package/dist/core/validation/validation-context.js +75 -0
- package/dist/core/validation/validation-registry.d.ts +156 -0
- package/dist/core/validation/validation-registry.js +298 -0
- package/dist/core/validation/validators/array-validators.d.ts +63 -0
- package/dist/core/validation/validators/array-validators.js +86 -0
- package/dist/core/validation/validators/date.d.ts +38 -0
- package/dist/core/validation/validators/date.js +117 -0
- package/dist/core/validation/validators/email.d.ts +44 -0
- package/dist/core/validation/validators/email.js +60 -0
- package/dist/core/validation/validators/index.d.ts +14 -0
- package/dist/core/validation/validators/index.js +14 -0
- package/dist/core/validation/validators/max-length.d.ts +45 -0
- package/dist/core/validation/validators/max-length.js +60 -0
- package/dist/core/validation/validators/max.d.ts +45 -0
- package/dist/core/validation/validators/max.js +60 -0
- package/dist/core/validation/validators/min-length.d.ts +45 -0
- package/dist/core/validation/validators/min-length.js +60 -0
- package/dist/core/validation/validators/min.d.ts +45 -0
- package/dist/core/validation/validators/min.js +60 -0
- package/dist/core/validation/validators/number.d.ts +38 -0
- package/dist/core/validation/validators/number.js +90 -0
- package/dist/core/validation/validators/pattern.d.ts +47 -0
- package/dist/core/validation/validators/pattern.js +62 -0
- package/dist/core/validation/validators/phone.d.ts +34 -0
- package/dist/core/validation/validators/phone.js +58 -0
- package/dist/core/validation/validators/required.d.ts +48 -0
- package/dist/core/validation/validators/required.js +69 -0
- package/dist/core/validation/validators/url.d.ts +29 -0
- package/dist/core/validation/validators/url.js +55 -0
- package/dist/create-field-path-CdPF3lIK.js +704 -0
- package/dist/hooks/useFormControl.d.ts +48 -0
- package/dist/hooks/useFormControl.js +298 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/node-factory-D7DOnSSN.js +3200 -0
- package/dist/validators.d.ts +2 -0
- package/dist/validators.js +298 -0
- package/llms.txt +847 -0
- package/package.json +86 -0
package/llms.txt
ADDED
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
# ReFormer
|
|
2
|
+
|
|
3
|
+
> Signals-based reactive form state management library for React.
|
|
4
|
+
> Built on Preact Signals Core for fine-grained reactivity with full TypeScript support.
|
|
5
|
+
> Key features: type-safe schemas, declarative validation, reactive behaviors, nested forms, dynamic arrays.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install reformer
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dependencies:
|
|
14
|
+
- React 18+ or React 19+
|
|
15
|
+
- @preact/signals-core ^1.8.0
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { createForm } from 'reformer';
|
|
21
|
+
import { required, email, minLength } from 'reformer/validators';
|
|
22
|
+
import { useFormControl } from 'reformer';
|
|
23
|
+
|
|
24
|
+
// 1. Define your form type
|
|
25
|
+
type ContactForm = {
|
|
26
|
+
name: string;
|
|
27
|
+
email: string;
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 2. Create form with schema and validation
|
|
32
|
+
const form = createForm<ContactForm>({
|
|
33
|
+
form: {
|
|
34
|
+
name: { value: '', component: Input, componentProps: { label: 'Name' } },
|
|
35
|
+
email: { value: '', component: Input, componentProps: { label: 'Email' } },
|
|
36
|
+
message: { value: '', component: Textarea, componentProps: { label: 'Message' } },
|
|
37
|
+
},
|
|
38
|
+
validation: (path) => {
|
|
39
|
+
required(path.name);
|
|
40
|
+
minLength(path.name, 2);
|
|
41
|
+
required(path.email);
|
|
42
|
+
email(path.email);
|
|
43
|
+
required(path.message);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 3. Use in React component
|
|
48
|
+
function ContactForm() {
|
|
49
|
+
const { value, errors, shouldShowError } = useFormControl(form.name);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<input
|
|
53
|
+
value={value}
|
|
54
|
+
onChange={(e) => form.name.setValue(e.target.value)}
|
|
55
|
+
onBlur={() => form.name.markAsTouched()}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Architecture
|
|
62
|
+
|
|
63
|
+
### Node Hierarchy
|
|
64
|
+
|
|
65
|
+
ReFormer uses a tree-based node architecture:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
GroupNode (Form)
|
|
69
|
+
├── FieldNode (single values: string, number, boolean)
|
|
70
|
+
├── GroupNode (nested objects)
|
|
71
|
+
└── ArrayNode (dynamic arrays)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
All nodes inherit from abstract `FormNode` base class.
|
|
75
|
+
|
|
76
|
+
### Key Concepts
|
|
77
|
+
|
|
78
|
+
1. **Form Schema** - Defines structure, components, and initial values
|
|
79
|
+
2. **Validation Schema** - Declares validation rules (separate from structure)
|
|
80
|
+
3. **Behavior Schema** - Reactive logic (computed fields, conditional visibility, sync)
|
|
81
|
+
|
|
82
|
+
### Signals-based Reactivity
|
|
83
|
+
|
|
84
|
+
- Uses @preact/signals-core for fine-grained reactivity
|
|
85
|
+
- Only affected components re-render when values change
|
|
86
|
+
- React integration via useSyncExternalStore
|
|
87
|
+
|
|
88
|
+
## Form Schema
|
|
89
|
+
|
|
90
|
+
### FieldConfig<T>
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
interface FieldConfig<T> {
|
|
94
|
+
value: T | null; // Initial value
|
|
95
|
+
component: ComponentType<any>; // React component to render
|
|
96
|
+
componentProps?: Record<string, any>; // Props passed to component
|
|
97
|
+
validators?: ValidatorFn<T>[]; // Sync validators
|
|
98
|
+
asyncValidators?: AsyncValidatorFn<T>[]; // Async validators
|
|
99
|
+
disabled?: boolean; // Initially disabled
|
|
100
|
+
updateOn?: 'change' | 'blur' | 'submit'; // When to validate (default: 'change')
|
|
101
|
+
debounce?: number; // Debounce validation in ms
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### ArrayConfig<T>
|
|
106
|
+
|
|
107
|
+
Arrays use single-element tuple syntax in schema:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
interface FormType {
|
|
111
|
+
phones: { type: string; number: string }[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const schema: FormSchema<FormType> = {
|
|
115
|
+
phones: [{
|
|
116
|
+
type: { value: 'mobile', component: Select },
|
|
117
|
+
number: { value: '', component: Input },
|
|
118
|
+
}],
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Complete Schema Example
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { createForm } from 'reformer';
|
|
126
|
+
|
|
127
|
+
type UserForm = {
|
|
128
|
+
name: string;
|
|
129
|
+
email: string;
|
|
130
|
+
age: number;
|
|
131
|
+
address: {
|
|
132
|
+
street: string;
|
|
133
|
+
city: string;
|
|
134
|
+
};
|
|
135
|
+
phones: { type: string; number: string }[];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const form = createForm<UserForm>({
|
|
139
|
+
form: {
|
|
140
|
+
name: { value: '', component: Input },
|
|
141
|
+
email: { value: '', component: Input },
|
|
142
|
+
age: { value: 0, component: Input, componentProps: { type: 'number' } },
|
|
143
|
+
address: {
|
|
144
|
+
street: { value: '', component: Input },
|
|
145
|
+
city: { value: '', component: Input },
|
|
146
|
+
},
|
|
147
|
+
phones: [{
|
|
148
|
+
type: { value: 'mobile', component: Select },
|
|
149
|
+
number: { value: '', component: Input },
|
|
150
|
+
}],
|
|
151
|
+
},
|
|
152
|
+
validation: (path) => { /* validators */ },
|
|
153
|
+
behavior: (path) => { /* behaviors */ },
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Node Types
|
|
158
|
+
|
|
159
|
+
### FieldNode<T>
|
|
160
|
+
|
|
161
|
+
Represents a single form field value.
|
|
162
|
+
|
|
163
|
+
**Properties (all are Signals):**
|
|
164
|
+
- `value` - Current value
|
|
165
|
+
- `valid` / `invalid` - Validation state
|
|
166
|
+
- `touched` / `untouched` - User interaction state
|
|
167
|
+
- `dirty` / `pristine` - Value changed from initial
|
|
168
|
+
- `errors` - Array of ValidationError objects
|
|
169
|
+
- `shouldShowError` - true when invalid AND (touched OR dirty)
|
|
170
|
+
- `disabled` - Is field disabled
|
|
171
|
+
- `pending` - Async validation in progress
|
|
172
|
+
- `status` - 'valid' | 'invalid' | 'pending' | 'disabled'
|
|
173
|
+
- `componentProps` - Props for component
|
|
174
|
+
|
|
175
|
+
**Methods:**
|
|
176
|
+
- `setValue(value, options?)` - Set new value
|
|
177
|
+
- `reset()` - Reset to initial value
|
|
178
|
+
- `markAsTouched()` - Mark as touched
|
|
179
|
+
- `markAsDirty()` - Mark as dirty
|
|
180
|
+
- `disable()` / `enable()` - Toggle disabled state
|
|
181
|
+
- `validate()` - Run validation
|
|
182
|
+
- `getErrors(filter?)` - Get filtered errors
|
|
183
|
+
|
|
184
|
+
### GroupNode<T>
|
|
185
|
+
|
|
186
|
+
Groups multiple fields into an object.
|
|
187
|
+
|
|
188
|
+
**Properties:**
|
|
189
|
+
- `controls` - Dictionary of child nodes
|
|
190
|
+
- All FormNode properties (computed from children)
|
|
191
|
+
|
|
192
|
+
**Methods:**
|
|
193
|
+
- `getFieldByPath(path: string)` - Get field by dot-notation path
|
|
194
|
+
- `patchValue(partial)` - Update subset of fields
|
|
195
|
+
- `resetAll()` - Reset all children
|
|
196
|
+
- All FormNode methods
|
|
197
|
+
|
|
198
|
+
**Proxy Access:**
|
|
199
|
+
```typescript
|
|
200
|
+
// Type-safe field access via proxy
|
|
201
|
+
form.name // FieldNode<string>
|
|
202
|
+
form.address.city // FieldNode<string>
|
|
203
|
+
form.phones // ArrayNode
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### ArrayNode<T>
|
|
207
|
+
|
|
208
|
+
Manages dynamic arrays.
|
|
209
|
+
|
|
210
|
+
**Properties:**
|
|
211
|
+
- `controls` - Array of GroupNode items
|
|
212
|
+
- `length` - Number of items
|
|
213
|
+
|
|
214
|
+
**Methods:**
|
|
215
|
+
- `push(value)` - Add item to end
|
|
216
|
+
- `insert(index, value)` - Insert at position
|
|
217
|
+
- `removeAt(index)` - Remove at position
|
|
218
|
+
- `move(from, to)` - Move item
|
|
219
|
+
- `clear()` - Remove all items
|
|
220
|
+
- `at(index)` - Get item at index
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// Usage
|
|
224
|
+
form.phones.push({ type: 'work', number: '' });
|
|
225
|
+
form.phones.removeAt(0);
|
|
226
|
+
form.phones.at(0).controls.number.setValue('123-456');
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Validation
|
|
230
|
+
|
|
231
|
+
### ValidationSchemaFn
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { required, email, minLength, validate, validateAsync } from 'reformer/validators';
|
|
235
|
+
|
|
236
|
+
const form = createForm<FormType>({
|
|
237
|
+
form: { /* schema */ },
|
|
238
|
+
validation: (path) => {
|
|
239
|
+
// Built-in validators
|
|
240
|
+
required(path.name);
|
|
241
|
+
email(path.email);
|
|
242
|
+
minLength(path.password, 8);
|
|
243
|
+
|
|
244
|
+
// Custom validator
|
|
245
|
+
validate(path.age, (value) => {
|
|
246
|
+
if (value < 18) return { code: 'tooYoung', message: 'Must be 18+' };
|
|
247
|
+
return null;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Async validator
|
|
251
|
+
validateAsync(path.username, async (value) => {
|
|
252
|
+
const available = await checkUsername(value);
|
|
253
|
+
if (!available) return { code: 'taken', message: 'Username taken' };
|
|
254
|
+
return null;
|
|
255
|
+
}, { debounce: 500 });
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Built-in Validators
|
|
261
|
+
|
|
262
|
+
All imported from `reformer/validators`:
|
|
263
|
+
|
|
264
|
+
| Validator | Usage | Description |
|
|
265
|
+
|-----------|-------|-------------|
|
|
266
|
+
| `required(path)` | `required(path.name)` | Non-empty value |
|
|
267
|
+
| `email(path)` | `email(path.email)` | Valid email format |
|
|
268
|
+
| `minLength(path, n)` | `minLength(path.name, 2)` | Minimum string length |
|
|
269
|
+
| `maxLength(path, n)` | `maxLength(path.bio, 500)` | Maximum string length |
|
|
270
|
+
| `min(path, n)` | `min(path.age, 18)` | Minimum number value |
|
|
271
|
+
| `max(path, n)` | `max(path.qty, 100)` | Maximum number value |
|
|
272
|
+
| `pattern(path, regex)` | `pattern(path.code, /^[A-Z]+$/)` | Match regex |
|
|
273
|
+
| `url(path)` | `url(path.website)` | Valid URL |
|
|
274
|
+
| `phone(path)` | `phone(path.phone)` | Valid phone |
|
|
275
|
+
| `number(path)` | `number(path.amount)` | Must be number |
|
|
276
|
+
| `date(path)` | `date(path.birthDate)` | Valid date |
|
|
277
|
+
|
|
278
|
+
### Custom Validator Example
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// validators/password.ts
|
|
282
|
+
export function strongPassword() {
|
|
283
|
+
return (value: string) => {
|
|
284
|
+
if (!value) return null; // Skip empty (use required() separately)
|
|
285
|
+
|
|
286
|
+
const errors: string[] = [];
|
|
287
|
+
if (!/[A-Z]/.test(value)) errors.push('uppercase');
|
|
288
|
+
if (!/[a-z]/.test(value)) errors.push('lowercase');
|
|
289
|
+
if (!/[0-9]/.test(value)) errors.push('number');
|
|
290
|
+
if (value.length < 8) errors.push('length');
|
|
291
|
+
|
|
292
|
+
if (errors.length) {
|
|
293
|
+
return { code: 'weakPassword', message: 'Password too weak', params: { missing: errors } };
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Usage
|
|
300
|
+
validation: (path) => {
|
|
301
|
+
required(path.password);
|
|
302
|
+
validate(path.password, strongPassword());
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Async Validation Example
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
// Check username availability on server
|
|
310
|
+
validation: (path) => {
|
|
311
|
+
required(path.username);
|
|
312
|
+
|
|
313
|
+
validateAsync(path.username, async (value, ctx) => {
|
|
314
|
+
if (!value || value.length < 3) return null;
|
|
315
|
+
|
|
316
|
+
const response = await fetch(`/api/check-username?u=${value}`);
|
|
317
|
+
const { available } = await response.json();
|
|
318
|
+
|
|
319
|
+
if (!available) {
|
|
320
|
+
return { code: 'usernameTaken', message: 'Username is already taken' };
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}, { debounce: 500 });
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Cross-field Validation
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { validateTree } from 'reformer/validators';
|
|
331
|
+
|
|
332
|
+
validation: (path) => {
|
|
333
|
+
required(path.password);
|
|
334
|
+
required(path.confirmPassword);
|
|
335
|
+
|
|
336
|
+
// Cross-field validation
|
|
337
|
+
validateTree((ctx) => {
|
|
338
|
+
const password = ctx.form.password.value.value;
|
|
339
|
+
const confirm = ctx.form.confirmPassword.value.value;
|
|
340
|
+
|
|
341
|
+
if (password && confirm && password !== confirm) {
|
|
342
|
+
return {
|
|
343
|
+
code: 'passwordMismatch',
|
|
344
|
+
message: 'Passwords do not match',
|
|
345
|
+
path: 'confirmPassword',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return null;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Behaviors
|
|
354
|
+
|
|
355
|
+
Behaviors add reactive logic to forms. All imported from `reformer/behaviors`.
|
|
356
|
+
|
|
357
|
+
### computeFrom
|
|
358
|
+
|
|
359
|
+
Calculate field value from other fields:
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
import { computeFrom } from 'reformer/behaviors';
|
|
363
|
+
|
|
364
|
+
behavior: (path) => {
|
|
365
|
+
// total = price * quantity
|
|
366
|
+
computeFrom(
|
|
367
|
+
[path.price, path.quantity], // Watch these fields
|
|
368
|
+
path.total, // Update this field
|
|
369
|
+
({ price, quantity }) => price * quantity // Compute function
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### enableWhen / disableWhen
|
|
375
|
+
|
|
376
|
+
Conditional field enable/disable:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { enableWhen, disableWhen } from 'reformer/behaviors';
|
|
380
|
+
|
|
381
|
+
behavior: (path) => {
|
|
382
|
+
// Enable discount field only when total > 500
|
|
383
|
+
enableWhen(path.discount, (form) => form.total > 500);
|
|
384
|
+
|
|
385
|
+
// Disable shipping when pickup is selected
|
|
386
|
+
disableWhen(path.shippingAddress, (form) => form.deliveryMethod === 'pickup');
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### watchField
|
|
391
|
+
|
|
392
|
+
React to field changes with custom logic:
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
import { watchField } from 'reformer/behaviors';
|
|
396
|
+
|
|
397
|
+
behavior: (path) => {
|
|
398
|
+
// Load cities when country changes
|
|
399
|
+
watchField(path.country, async (value, ctx) => {
|
|
400
|
+
const cities = await fetchCities(value);
|
|
401
|
+
ctx.form.city.updateComponentProps({ options: cities });
|
|
402
|
+
ctx.form.city.setValue(''); // Reset city selection
|
|
403
|
+
}, { debounce: 300 });
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### copyFrom
|
|
408
|
+
|
|
409
|
+
Copy values from one field/group to another:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { copyFrom } from 'reformer/behaviors';
|
|
413
|
+
|
|
414
|
+
behavior: (path) => {
|
|
415
|
+
// Copy billing address to shipping when checkbox is checked
|
|
416
|
+
copyFrom(path.billingAddress, path.shippingAddress, {
|
|
417
|
+
when: (form) => form.sameAsShipping === true,
|
|
418
|
+
fields: 'all', // or ['street', 'city', 'zip']
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### syncFields
|
|
424
|
+
|
|
425
|
+
Two-way field synchronization:
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import { syncFields } from 'reformer/behaviors';
|
|
429
|
+
|
|
430
|
+
behavior: (path) => {
|
|
431
|
+
syncFields(path.field1, path.field2);
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### resetWhen
|
|
436
|
+
|
|
437
|
+
Reset field when condition is met:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
import { resetWhen } from 'reformer/behaviors';
|
|
441
|
+
|
|
442
|
+
behavior: (path) => {
|
|
443
|
+
// Reset city when country changes
|
|
444
|
+
resetWhen(path.city, [path.country]);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### revalidateWhen
|
|
449
|
+
|
|
450
|
+
Trigger revalidation when another field changes:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
import { revalidateWhen } from 'reformer/behaviors';
|
|
454
|
+
|
|
455
|
+
behavior: (path) => {
|
|
456
|
+
// Revalidate confirmPassword when password changes
|
|
457
|
+
revalidateWhen(path.confirmPassword, [path.password]);
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Custom Behavior
|
|
462
|
+
|
|
463
|
+
Create reusable custom behaviors:
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
// behaviors/auto-save.ts
|
|
467
|
+
import { Behavior } from 'reformer';
|
|
468
|
+
|
|
469
|
+
interface AutoSaveOptions {
|
|
470
|
+
debounce?: number;
|
|
471
|
+
onSave: (data: any) => Promise<void>;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function autoSave<T>(options: AutoSaveOptions): Behavior<T> {
|
|
475
|
+
const { debounce = 1000, onSave } = options;
|
|
476
|
+
let timeoutId: NodeJS.Timeout;
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
key: 'autoSave',
|
|
480
|
+
paths: [], // Empty = listen to all fields
|
|
481
|
+
run: (values, ctx) => {
|
|
482
|
+
clearTimeout(timeoutId);
|
|
483
|
+
timeoutId = setTimeout(async () => {
|
|
484
|
+
await onSave(ctx.form.getValue());
|
|
485
|
+
}, debounce);
|
|
486
|
+
},
|
|
487
|
+
cleanup: () => clearTimeout(timeoutId),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Usage
|
|
492
|
+
behaviors: (path, { use }) => [
|
|
493
|
+
use(autoSave({
|
|
494
|
+
debounce: 2000,
|
|
495
|
+
onSave: async (data) => {
|
|
496
|
+
await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
|
|
497
|
+
},
|
|
498
|
+
})),
|
|
499
|
+
];
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Recommended Project Structure
|
|
503
|
+
|
|
504
|
+
### Form Organization (Colocation)
|
|
505
|
+
|
|
506
|
+
```
|
|
507
|
+
src/forms/
|
|
508
|
+
├── user-profile/
|
|
509
|
+
│ ├── UserProfileForm.tsx # React component
|
|
510
|
+
│ ├── type.ts # TypeScript interfaces
|
|
511
|
+
│ ├── schema.ts # Form schema (createForm)
|
|
512
|
+
│ ├── validators.ts # Validation rules
|
|
513
|
+
│ ├── behaviors.ts # Reactive behaviors
|
|
514
|
+
│ └── sub-forms/ # Reusable nested schemas
|
|
515
|
+
│ ├── address/
|
|
516
|
+
│ │ ├── type.ts
|
|
517
|
+
│ │ ├── schema.ts
|
|
518
|
+
│ │ ├── validators.ts
|
|
519
|
+
│ │ └── AddressForm.tsx
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Schema File Pattern
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
// forms/user-profile/schema.ts
|
|
526
|
+
import { createForm } from 'reformer';
|
|
527
|
+
import { validation } from './validators';
|
|
528
|
+
import { behavior } from './behaviors';
|
|
529
|
+
import type { UserProfile } from './type';
|
|
530
|
+
|
|
531
|
+
export const createUserProfileForm = (initial?: Partial<UserProfile>) =>
|
|
532
|
+
createForm<UserProfile>({
|
|
533
|
+
form: {
|
|
534
|
+
name: { value: initial?.name ?? '', component: Input },
|
|
535
|
+
email: { value: initial?.email ?? '', component: Input },
|
|
536
|
+
// ...
|
|
537
|
+
},
|
|
538
|
+
validation,
|
|
539
|
+
behavior,
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Multi-step Form Structure
|
|
544
|
+
|
|
545
|
+
```
|
|
546
|
+
src/forms/checkout/
|
|
547
|
+
├── CheckoutForm.tsx # Main form component
|
|
548
|
+
├── type.ts # Combined type
|
|
549
|
+
├── schema.ts # Combined schema
|
|
550
|
+
├── validators.ts # Combined + cross-step validators
|
|
551
|
+
├── behaviors.ts # Combined + cross-step behaviors
|
|
552
|
+
├── steps/
|
|
553
|
+
│ ├── shipping/
|
|
554
|
+
│ │ ├── type.ts
|
|
555
|
+
│ │ ├── schema.ts
|
|
556
|
+
│ │ ├── validators.ts
|
|
557
|
+
│ │ └── ShippingStep.tsx
|
|
558
|
+
│ ├── payment/
|
|
559
|
+
│ └── confirmation/
|
|
560
|
+
└── hooks/
|
|
561
|
+
└── useCheckoutNavigation.ts
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
## React Integration
|
|
565
|
+
|
|
566
|
+
### useFormControl<T>
|
|
567
|
+
|
|
568
|
+
Subscribe to all field state changes:
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
import { useFormControl } from 'reformer';
|
|
572
|
+
|
|
573
|
+
function TextField({ field }: { field: FieldNode<string> }) {
|
|
574
|
+
const {
|
|
575
|
+
value, // Current value
|
|
576
|
+
valid, // Is valid
|
|
577
|
+
invalid, // Has errors
|
|
578
|
+
errors, // ValidationError[]
|
|
579
|
+
touched, // User interacted
|
|
580
|
+
disabled, // Is disabled
|
|
581
|
+
pending, // Async validation running
|
|
582
|
+
shouldShowError, // Show error (touched && invalid)
|
|
583
|
+
componentProps, // Custom props from schema
|
|
584
|
+
} = useFormControl(field);
|
|
585
|
+
|
|
586
|
+
return (
|
|
587
|
+
<div>
|
|
588
|
+
<input
|
|
589
|
+
value={value}
|
|
590
|
+
onChange={(e) => field.setValue(e.target.value)}
|
|
591
|
+
onBlur={() => field.markAsTouched()}
|
|
592
|
+
disabled={disabled}
|
|
593
|
+
/>
|
|
594
|
+
{shouldShowError && errors[0] && (
|
|
595
|
+
<span className="error">{errors[0].message}</span>
|
|
596
|
+
)}
|
|
597
|
+
</div>
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### useFormControlValue<T>
|
|
603
|
+
|
|
604
|
+
Lightweight hook - returns only value (better performance):
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
import { useFormControlValue } from 'reformer';
|
|
608
|
+
|
|
609
|
+
function ConditionalField({ trigger, field }) {
|
|
610
|
+
// Re-renders only when trigger value changes
|
|
611
|
+
const showField = useFormControlValue(trigger);
|
|
612
|
+
|
|
613
|
+
if (showField !== 'yes') return null;
|
|
614
|
+
return <TextField field={field} />;
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### Performance Notes
|
|
619
|
+
|
|
620
|
+
- Uses `useSyncExternalStore` for React 18+ integration
|
|
621
|
+
- Fine-grained updates - only affected components re-render
|
|
622
|
+
- Memoized state objects prevent unnecessary re-renders
|
|
623
|
+
- Use `useFormControlValue` when you only need the value
|
|
624
|
+
|
|
625
|
+
## API Reference
|
|
626
|
+
|
|
627
|
+
### createForm<T>(config)
|
|
628
|
+
|
|
629
|
+
Creates a new form instance with type-safe proxy access.
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
function createForm<T>(config: GroupNodeConfig<T>): GroupNodeWithControls<T>
|
|
633
|
+
|
|
634
|
+
interface GroupNodeConfig<T> {
|
|
635
|
+
form: FormSchema<T>;
|
|
636
|
+
validation?: ValidationSchemaFn<T>;
|
|
637
|
+
behavior?: BehaviorSchemaFn<T>;
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Node Common Properties
|
|
642
|
+
|
|
643
|
+
All nodes have these Signal properties:
|
|
644
|
+
- `value` - Current value
|
|
645
|
+
- `valid` / `invalid` - Validation state
|
|
646
|
+
- `touched` / `untouched` - Interaction state
|
|
647
|
+
- `dirty` / `pristine` - Changed state
|
|
648
|
+
- `status` - 'valid' | 'invalid' | 'pending' | 'disabled'
|
|
649
|
+
- `disabled` - Is disabled
|
|
650
|
+
- `pending` - Async validation in progress
|
|
651
|
+
|
|
652
|
+
### Node Common Methods
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
setValue(value: T, options?: SetValueOptions): void
|
|
656
|
+
reset(): void
|
|
657
|
+
markAsTouched(): void
|
|
658
|
+
markAsDirty(): void
|
|
659
|
+
disable(): void
|
|
660
|
+
enable(): void
|
|
661
|
+
validate(): Promise<void>
|
|
662
|
+
getErrors(filter?: (error: ValidationError) => boolean): ValidationError[]
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### SetValueOptions
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
interface SetValueOptions {
|
|
669
|
+
emitEvent?: boolean; // Trigger change events (default: true)
|
|
670
|
+
onlySelf?: boolean; // Don't propagate to parent (default: false)
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### ValidationError
|
|
675
|
+
|
|
676
|
+
```typescript
|
|
677
|
+
interface ValidationError {
|
|
678
|
+
code: string; // Error identifier
|
|
679
|
+
message: string; // Human-readable message
|
|
680
|
+
params?: Record<string, any>; // Additional error data
|
|
681
|
+
severity?: 'error' | 'warning'; // Severity level
|
|
682
|
+
path?: string; // Field path (for cross-field)
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### FieldStatus
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
type FieldStatus = 'valid' | 'invalid' | 'pending' | 'disabled';
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Type Guards
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
import { isFieldNode, isGroupNode, isArrayNode, getNodeType } from 'reformer';
|
|
696
|
+
|
|
697
|
+
if (isFieldNode(node)) { /* node is FieldNode */ }
|
|
698
|
+
if (isGroupNode(node)) { /* node is GroupNode */ }
|
|
699
|
+
if (isArrayNode(node)) { /* node is ArrayNode */ }
|
|
700
|
+
const type = getNodeType(node); // 'field' | 'group' | 'array'
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
## Common Patterns
|
|
704
|
+
|
|
705
|
+
### Multi-step Form
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
function MultiStepForm() {
|
|
709
|
+
const form = useMemo(() => createCheckoutForm(), []);
|
|
710
|
+
const [step, setStep] = useState(0);
|
|
711
|
+
|
|
712
|
+
const validateStep = async () => {
|
|
713
|
+
const stepFields = getStepFields(step);
|
|
714
|
+
stepFields.forEach(f => f.markAsTouched());
|
|
715
|
+
await form.validate();
|
|
716
|
+
return stepFields.every(f => f.valid.value);
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const handleNext = async () => {
|
|
720
|
+
if (await validateStep()) {
|
|
721
|
+
setStep(s => s + 1);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
return (
|
|
726
|
+
<div>
|
|
727
|
+
{step === 0 && <ShippingStep form={form} />}
|
|
728
|
+
{step === 1 && <PaymentStep form={form} />}
|
|
729
|
+
{step === 2 && <ConfirmationStep form={form} />}
|
|
730
|
+
|
|
731
|
+
<button onClick={() => setStep(s => s - 1)} disabled={step === 0}>
|
|
732
|
+
Back
|
|
733
|
+
</button>
|
|
734
|
+
<button onClick={handleNext}>
|
|
735
|
+
{step === 2 ? 'Submit' : 'Next'}
|
|
736
|
+
</button>
|
|
737
|
+
</div>
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### Nested Form with Reusable Schema
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
// sub-forms/address/schema.ts
|
|
746
|
+
export const addressSchema = {
|
|
747
|
+
street: { value: '', component: Input, componentProps: { label: 'Street' } },
|
|
748
|
+
city: { value: '', component: Input, componentProps: { label: 'City' } },
|
|
749
|
+
zip: { value: '', component: Input, componentProps: { label: 'ZIP' } },
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
// main form
|
|
753
|
+
const form = createForm<OrderForm>({
|
|
754
|
+
form: {
|
|
755
|
+
billingAddress: addressSchema,
|
|
756
|
+
shippingAddress: addressSchema,
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Dynamic Array (Add/Remove Items)
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
function PhoneList({ array }: { array: ArrayNode<Phone> }) {
|
|
765
|
+
const { length } = useFormControl(array);
|
|
766
|
+
|
|
767
|
+
return (
|
|
768
|
+
<div>
|
|
769
|
+
{array.controls.map((phone, index) => (
|
|
770
|
+
<div key={phone.id}>
|
|
771
|
+
<FormField field={phone.controls.type} />
|
|
772
|
+
<FormField field={phone.controls.number} />
|
|
773
|
+
<button onClick={() => array.removeAt(index)}>Remove</button>
|
|
774
|
+
</div>
|
|
775
|
+
))}
|
|
776
|
+
|
|
777
|
+
<button onClick={() => array.push({ type: 'mobile', number: '' })}>
|
|
778
|
+
Add Phone
|
|
779
|
+
</button>
|
|
780
|
+
</div>
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Conditional Fields
|
|
786
|
+
|
|
787
|
+
```typescript
|
|
788
|
+
behavior: (path) => {
|
|
789
|
+
// Show company fields only for business accounts
|
|
790
|
+
enableWhen(path.companyName, (form) => form.accountType === 'business');
|
|
791
|
+
enableWhen(path.taxId, (form) => form.accountType === 'business');
|
|
792
|
+
|
|
793
|
+
// Reset company fields when switching to personal
|
|
794
|
+
resetWhen(path.companyName, [path.accountType]);
|
|
795
|
+
resetWhen(path.taxId, [path.accountType]);
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
## Troubleshooting / FAQ
|
|
800
|
+
|
|
801
|
+
### Q: Field not updating in React?
|
|
802
|
+
A: Ensure you're using `useFormControl()` hook to subscribe to changes. Direct signal access (`.value.value`) won't trigger re-renders.
|
|
803
|
+
|
|
804
|
+
### Q: Validation not triggering?
|
|
805
|
+
A: Check `updateOn` option in field config. Default is 'change'. For blur-triggered validation use `updateOn: 'blur'`.
|
|
806
|
+
|
|
807
|
+
### Q: How to access nested field by path string?
|
|
808
|
+
A: Use `form.getFieldByPath('address.city')` for dynamic string-based access. For type-safe access use proxy: `form.address.city`.
|
|
809
|
+
|
|
810
|
+
### Q: TypeScript errors with schema?
|
|
811
|
+
A: Ensure your type interface matches the schema structure exactly. Use `createForm<YourType>()` for proper type inference.
|
|
812
|
+
|
|
813
|
+
### Q: How to reset form to initial values?
|
|
814
|
+
A: Call `form.reset()` for single field or `form.resetAll()` for GroupNode to reset all children.
|
|
815
|
+
|
|
816
|
+
### Q: How to get all form values?
|
|
817
|
+
A: Access `form.value.value` (it's a Signal) or use `form.getValue()` method.
|
|
818
|
+
|
|
819
|
+
### Q: How to programmatically set multiple values?
|
|
820
|
+
A: Use `form.patchValue({ field1: 'value1', field2: 'value2' })` to update multiple fields at once.
|
|
821
|
+
|
|
822
|
+
### Q: Form instance recreated on every render?
|
|
823
|
+
A: Wrap `createForm()` in `useMemo()`:
|
|
824
|
+
```typescript
|
|
825
|
+
const form = useMemo(() => createForm<MyForm>({ form: schema }), []);
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### Q: How to handle form submission?
|
|
829
|
+
A:
|
|
830
|
+
```typescript
|
|
831
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
832
|
+
e.preventDefault();
|
|
833
|
+
form.markAsTouched(); // Show all errors
|
|
834
|
+
await form.validate(); // Run all validators
|
|
835
|
+
|
|
836
|
+
if (form.valid.value) {
|
|
837
|
+
const data = form.value.value;
|
|
838
|
+
await submitToServer(data);
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
## Links
|
|
844
|
+
|
|
845
|
+
- Repository: https://github.com/AlexandrBukhtatyy/ReFormer
|
|
846
|
+
- Documentation: https://alexandrbukhtatyy.github.io/ReFormer/
|
|
847
|
+
- Issues: https://github.com/AlexandrBukhtatyy/ReFormer/issues
|