effect-atom-svelte 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/Form.d.ts +605 -0
- package/dist/Form.js +67 -0
- package/dist/Hooks.svelte.d.ts +70 -10
- package/dist/Hooks.svelte.js +489 -163
- package/dist/HydrationBoundary.svelte +134 -17
- package/dist/HydrationBoundary.svelte.d.ts +13 -4
- package/dist/Inspector.d.ts +7 -3
- package/dist/Inspector.js +113 -29
- package/dist/RegistryContext.d.ts +2 -0
- package/dist/RegistryContext.js +13 -5
- package/dist/ScopedAtom.d.ts +5 -4
- package/dist/ScopedAtom.js +20 -11
- package/dist/SvelteKit.d.ts +71 -12
- package/dist/SvelteKit.js +133 -54
- package/dist/index.d.ts +10 -1
- package/dist/index.js +10 -1
- package/dist/internal/awaitWrite.d.ts +11 -0
- package/dist/internal/awaitWrite.js +27 -0
- package/dist/internal/boundaries.d.ts +9 -0
- package/dist/internal/boundaries.js +10 -0
- package/dist/internal/disposal.d.ts +13 -0
- package/dist/internal/disposal.js +43 -0
- package/dist/internal/form/array.d.ts +21 -0
- package/dist/internal/form/array.js +57 -0
- package/dist/internal/form/constraints.d.ts +13 -0
- package/dist/internal/form/constraints.js +166 -0
- package/dist/internal/form/engine.d.ts +56 -0
- package/dist/internal/form/engine.js +842 -0
- package/dist/internal/form/engineOf.d.ts +26 -0
- package/dist/internal/form/engineOf.js +43 -0
- package/dist/internal/form/field.d.ts +30 -0
- package/dist/internal/form/field.js +68 -0
- package/dist/internal/form/group.d.ts +13 -0
- package/dist/internal/form/group.js +20 -0
- package/dist/internal/form/instance.d.ts +41 -0
- package/dist/internal/form/instance.js +135 -0
- package/dist/internal/form/issues.d.ts +23 -0
- package/dist/internal/form/issues.js +144 -0
- package/dist/internal/form/paths.d.ts +47 -0
- package/dist/internal/form/paths.js +175 -0
- package/dist/internal/form/types.d.ts +80 -0
- package/dist/internal/form/types.js +1 -0
- package/dist/internal/form/validationError.d.ts +13 -0
- package/dist/internal/form/validationError.js +6 -0
- package/dist/internal/hydration.d.ts +11 -2
- package/dist/internal/hydration.js +26 -4
- package/dist/internal/nodeInternals.d.ts +6 -0
- package/dist/internal/nodeInternals.js +64 -7
- package/dist/internal/renderEnd.d.ts +13 -0
- package/dist/internal/renderEnd.js +34 -0
- package/dist/internal/teardown.svelte.d.ts +17 -0
- package/dist/internal/teardown.svelte.js +44 -0
- package/dist/internal/troubleshooting.d.ts +2 -0
- package/dist/internal/troubleshooting.js +3 -0
- package/package.json +12 -10
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ This is a community project by Jarred Norris. It is not part of Effect, and the
|
|
|
10
10
|
|
|
11
11
|
- `effect` 4.0.x. The peer range is `~4.0.0`, not `^4.0.0`: the bindings use parts of the atom registry that aren't public API, which a minor release of `effect` can change.
|
|
12
12
|
- Svelte 5.57.2 or later. `experimental.async` is needed for `useAtomSuspense`, `useAtomResult` and server rendering.
|
|
13
|
-
- SvelteKit is optional. The docs site and its tests run on SvelteKit 3. On SvelteKit 2, the error hooks in `effect-atom-svelte/sveltekit` work with less detail.
|
|
13
|
+
- SvelteKit is optional. The docs site and its tests run on SvelteKit 3. On SvelteKit 2, the error hooks in `effect-atom-svelte/sveltekit` work with less detail, and reach `<svelte:boundary>` only with `kit.experimental.handleRenderingErrors` turned on.
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
@@ -64,7 +64,9 @@ On the server, the provider creates a registry for each request, so visitors nev
|
|
|
64
64
|
const greeting = useAtomSuspense(greetingAtom);
|
|
65
65
|
</script>
|
|
66
66
|
|
|
67
|
-
<button onclick={() => (count.current += 1)}
|
|
67
|
+
<button onclick={() => (count.current += 1)}
|
|
68
|
+
>{count.current} × 2 = {doubled.current}</button
|
|
69
|
+
>
|
|
68
70
|
|
|
69
71
|
<svelte:boundary>
|
|
70
72
|
<p>{await greeting.current}</p>
|
|
@@ -80,6 +82,7 @@ Hooks return an object whose `current` you read, assign or `bind:` to. Don't des
|
|
|
80
82
|
- [Your first atom](https://atom.jarrednorris.dev/first-atom), [Reading and writing](https://atom.jarrednorris.dev/reading-and-writing) and [Async atoms](https://atom.jarrednorris.dev/async-atoms)
|
|
81
83
|
- [Server rendering](https://atom.jarrednorris.dev/server-rendering) and [Hydration](https://atom.jarrednorris.dev/hydration)
|
|
82
84
|
- [Effect RPC](https://atom.jarrednorris.dev/rpc) and [HttpApi](https://atom.jarrednorris.dev/http)
|
|
85
|
+
- [Forms](https://atom.jarrednorris.dev/forms), built from one Effect Schema
|
|
83
86
|
- [API reference](https://atom.jarrednorris.dev/reference) and [Troubleshooting](https://atom.jarrednorris.dev/troubleshooting)
|
|
84
87
|
- [Every export at a glance, and more recipes](https://github.com/jarrednorrisdev/effect-atom-svelte#api-at-a-glance), in the repository's README
|
|
85
88
|
|
package/dist/Form.d.ts
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forms driven by one Effect Schema. `Form.make` defines a form from a Schema whose encoded side is
|
|
3
|
+
* what inputs hold and whose decoded side is what a submit sends; `useForm` creates an instance in
|
|
4
|
+
* a component, and `Form.instance` creates one without Svelte. The module is headless: it gives
|
|
5
|
+
* values, errors and element attributes, and no components.
|
|
6
|
+
*
|
|
7
|
+
* @since 0.4.0
|
|
8
|
+
*/
|
|
9
|
+
import { Duration } from "effect";
|
|
10
|
+
import type { Exit, Option, Schema, SchemaIssue } from "effect";
|
|
11
|
+
import { AtomRegistry } from "effect/reactivity";
|
|
12
|
+
import type { AsyncResult, Atom } from "effect/reactivity";
|
|
13
|
+
import type { AtomInput } from "./Hooks.svelte.ts";
|
|
14
|
+
import type { Constraints as InternalConstraints } from "./internal/form/constraints.ts";
|
|
15
|
+
import type { Definition } from "./internal/form/types.ts";
|
|
16
|
+
import { FormValidationError } from "./internal/form/validationError.ts";
|
|
17
|
+
/**
|
|
18
|
+
* Marks a value as a form definition: every definition has this key, set to this same string.
|
|
19
|
+
*
|
|
20
|
+
* @stability unstable
|
|
21
|
+
* @since 0.4.0
|
|
22
|
+
* @category type ids
|
|
23
|
+
*/
|
|
24
|
+
export declare const TypeId: "~effect-atom-svelte/Form";
|
|
25
|
+
/**
|
|
26
|
+
* The type of `TypeId`.
|
|
27
|
+
*
|
|
28
|
+
* @stability unstable
|
|
29
|
+
* @since 0.4.0
|
|
30
|
+
* @category type ids
|
|
31
|
+
*/
|
|
32
|
+
export type TypeId = typeof TypeId;
|
|
33
|
+
/**
|
|
34
|
+
* When a field's error shows: as the user types (`"input"`), when they leave it (`"blur"`), or only
|
|
35
|
+
* when the form is submitted (`"submit"`). The Schema checks every change whatever the timing, so
|
|
36
|
+
* `valid` and `field.issue` are always current; the timing only decides when `field.error` shows.
|
|
37
|
+
*
|
|
38
|
+
* @stability unstable
|
|
39
|
+
* @since 0.4.0
|
|
40
|
+
* @category models
|
|
41
|
+
*/
|
|
42
|
+
export type Timing = "submit" | "blur" | "input";
|
|
43
|
+
/**
|
|
44
|
+
* A form definition: a Schema and the settings every instance shares. It holds no state, so it can
|
|
45
|
+
* live in a module beside the atoms.
|
|
46
|
+
*
|
|
47
|
+
* @stability unstable
|
|
48
|
+
* @since 0.4.0
|
|
49
|
+
* @category models
|
|
50
|
+
*/
|
|
51
|
+
export interface Form<S extends Schema.Top> {
|
|
52
|
+
readonly [TypeId]: TypeId;
|
|
53
|
+
readonly schema: S;
|
|
54
|
+
/** @internal */
|
|
55
|
+
readonly definition: Definition;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The values a form's inputs hold: its Schema's encoded side.
|
|
59
|
+
*
|
|
60
|
+
* @stability unstable
|
|
61
|
+
* @since 0.4.0
|
|
62
|
+
* @category models
|
|
63
|
+
*/
|
|
64
|
+
export type Encoded<F extends Form<Schema.Top>> = F["schema"]["Encoded"];
|
|
65
|
+
/**
|
|
66
|
+
* What a form's values decode to, and a submit sends: its Schema's decoded side.
|
|
67
|
+
*
|
|
68
|
+
* @stability unstable
|
|
69
|
+
* @since 0.4.0
|
|
70
|
+
* @category models
|
|
71
|
+
*/
|
|
72
|
+
export type Decoded<F extends Form<Schema.Top>> = F["schema"]["Type"];
|
|
73
|
+
/**
|
|
74
|
+
* Options for `Form.make`: the settings every instance of the form shares.
|
|
75
|
+
*
|
|
76
|
+
* @stability unstable
|
|
77
|
+
* @since 0.4.0
|
|
78
|
+
* @category models
|
|
79
|
+
*/
|
|
80
|
+
export interface MakeOptions {
|
|
81
|
+
/**
|
|
82
|
+
* A name for the form, used in its atoms' labels, such as `SignUp.values`, which the devtools
|
|
83
|
+
* show. The devtools' Vite plugin fills it in from the variable a form is declared as.
|
|
84
|
+
*/
|
|
85
|
+
readonly name?: string | undefined;
|
|
86
|
+
/** When a field's error first shows. Defaults to `"blur"`. */
|
|
87
|
+
readonly validateOn?: Timing | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* When it updates after the field has shown an error, or once the form has been submitted.
|
|
90
|
+
* Defaults to `"input"`.
|
|
91
|
+
*/
|
|
92
|
+
readonly revalidateOn?: Timing | undefined;
|
|
93
|
+
/** How long to wait after a change before validating. Defaults to none. */
|
|
94
|
+
readonly debounce?: Duration.Input | undefined;
|
|
95
|
+
/** A message for an issue that has no `message` annotation, or `undefined` for Effect's own. */
|
|
96
|
+
readonly leafHook?: ((issue: SchemaIssue.Leaf) => string | undefined) | undefined;
|
|
97
|
+
/** A message for a failed check that has no `message` annotation, or `undefined` for Effect's. */
|
|
98
|
+
readonly checkHook?: ((issue: SchemaIssue.Filter) => string | undefined) | undefined;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Options for `Form.make` with a runtime, for a Schema whose checks need services.
|
|
102
|
+
*
|
|
103
|
+
* @stability unstable
|
|
104
|
+
* @since 0.4.0
|
|
105
|
+
* @category models
|
|
106
|
+
*/
|
|
107
|
+
export interface MakeOptionsWithRuntime<R, ER> extends MakeOptions {
|
|
108
|
+
/** The runtime validation runs on, built with `Atom.runtime(layer)`. */
|
|
109
|
+
readonly runtime: Atom.AtomRuntime<R, ER>;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Defines a form from a Schema. Decoding always runs as an Effect, with every issue reported, so
|
|
113
|
+
* checks that call services work when a runtime provides them. A message comes from the Schema's
|
|
114
|
+
* `message` annotation first, then from `leafHook` or `checkHook`, then from Effect's default.
|
|
115
|
+
*
|
|
116
|
+
* **Example** (Defining a login form)
|
|
117
|
+
*
|
|
118
|
+
* ```ts
|
|
119
|
+
* import { Schema } from "effect";
|
|
120
|
+
* import { Form } from "effect-atom-svelte";
|
|
121
|
+
*
|
|
122
|
+
* export const Login = Form.make(
|
|
123
|
+
* Schema.Struct({
|
|
124
|
+
* email: Schema.String.check(
|
|
125
|
+
* Schema.isPattern(/^\S+@\S+\.\S+$/u, {
|
|
126
|
+
* message: "Enter an email address",
|
|
127
|
+
* })
|
|
128
|
+
* ),
|
|
129
|
+
* password: Schema.String.check(
|
|
130
|
+
* Schema.isMinLength(8, {
|
|
131
|
+
* message: "At least 8 characters",
|
|
132
|
+
* })
|
|
133
|
+
* ),
|
|
134
|
+
* })
|
|
135
|
+
* );
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* @see {@link https://atom.jarrednorris.dev/forms | Forms}
|
|
139
|
+
* @stability unstable
|
|
140
|
+
* @since 0.4.0
|
|
141
|
+
* @category constructors
|
|
142
|
+
*/
|
|
143
|
+
export declare function make<S extends Schema.Top & {
|
|
144
|
+
readonly DecodingServices: never;
|
|
145
|
+
}>(schema: S, options?: MakeOptions): Form<S>;
|
|
146
|
+
export declare function make<S extends Schema.Top, R, ER>(schema: S & ([S["DecodingServices"]] extends [R] ? unknown : never), options: MakeOptionsWithRuntime<R, ER>): Form<S>;
|
|
147
|
+
/**
|
|
148
|
+
* Whether a value is a form definition from `Form.make`.
|
|
149
|
+
*
|
|
150
|
+
* @stability unstable
|
|
151
|
+
* @since 0.4.0
|
|
152
|
+
* @category guards
|
|
153
|
+
*/
|
|
154
|
+
export declare const isForm: (u: unknown) => u is Form<Schema.Top>;
|
|
155
|
+
type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8];
|
|
156
|
+
type Scalar = string | number | boolean | bigint | symbol | null | undefined | Date | ((...args: readonly never[]) => unknown);
|
|
157
|
+
/**
|
|
158
|
+
* Every path into `T`, as a tuple of object keys and array indexes:
|
|
159
|
+
* `["email"]`, `["items", 0, "street"]`. For a union, the paths of every member.
|
|
160
|
+
*
|
|
161
|
+
* @stability unstable
|
|
162
|
+
* @since 0.4.0
|
|
163
|
+
* @category models
|
|
164
|
+
*/
|
|
165
|
+
export type Path<T, D extends number = 8> = [D] extends [never] ? never : T extends Scalar ? never : T extends readonly (infer Item)[] ? readonly [number] | readonly [number, ...Path<NonNullable<Item>, Depth[D]>] : T extends object ? {
|
|
166
|
+
[K in keyof T & string]-?: readonly [K] | readonly [K, ...Path<NonNullable<T[K]>, Depth[D]>];
|
|
167
|
+
}[keyof T & string] : never;
|
|
168
|
+
/**
|
|
169
|
+
* The value at path `P` in `T`. For a union, the value in each member that has the path. Below an
|
|
170
|
+
* optional parent it may also be `undefined`, as it is while the parent is.
|
|
171
|
+
*
|
|
172
|
+
* @stability unstable
|
|
173
|
+
* @since 0.4.0
|
|
174
|
+
* @category models
|
|
175
|
+
*/
|
|
176
|
+
export type At<T, P> = P extends readonly [] ? T : P extends readonly [infer Head, ...infer Rest] ? T extends unknown ? T extends readonly (infer Item)[] ? Head extends number ? At<Rest extends readonly [] ? Item : NonNullable<Item>, Rest> : never : Head extends keyof T ? Rest extends readonly [] ? T[Head] : At<NonNullable<T[Head]>, Rest> | (undefined extends T[Head] ? undefined : never) : never : never : never;
|
|
177
|
+
/**
|
|
178
|
+
* The top-level keys of `T`, of every member for a union: the short form of a path.
|
|
179
|
+
*
|
|
180
|
+
* @stability unstable
|
|
181
|
+
* @since 0.4.0
|
|
182
|
+
* @category models
|
|
183
|
+
*/
|
|
184
|
+
export type Key<T> = T extends unknown ? T extends readonly unknown[] | Scalar ? never : T extends object ? keyof T & string : never : never;
|
|
185
|
+
type ItemOf<V> = NonNullable<V> extends readonly (infer Item)[] ? Item : never;
|
|
186
|
+
/**
|
|
187
|
+
* A path as the form reports it, such as in `errors`: object keys and array indexes.
|
|
188
|
+
*
|
|
189
|
+
* @stability unstable
|
|
190
|
+
* @since 0.4.0
|
|
191
|
+
* @category models
|
|
192
|
+
*/
|
|
193
|
+
export type AnyPath = readonly (string | number)[];
|
|
194
|
+
/**
|
|
195
|
+
* HTML input constraints read from a field's Schema checks: `minlength` from `isMinLength` and
|
|
196
|
+
* `isNonEmpty`, `maxlength` from `isMaxLength`, both from `isBetweenLength`, `min` and `max` from
|
|
197
|
+
* `isGreaterThanOrEqualTo`, `isLessThanOrEqualTo` and `isBetween`, `pattern` from an `isPattern`
|
|
198
|
+
* anchored with `^` and `$`, with no flags but `u` or `v` (an HTML pattern must match the whole value, a
|
|
199
|
+
* Schema pattern anywhere in it), and `required`
|
|
200
|
+
* where the Schema rejects an empty string. Other checks set nothing.
|
|
201
|
+
*
|
|
202
|
+
* @stability unstable
|
|
203
|
+
* @since 0.4.0
|
|
204
|
+
* @category models
|
|
205
|
+
*/
|
|
206
|
+
export type Constraints = InternalConstraints;
|
|
207
|
+
/**
|
|
208
|
+
* The attributes `field.attrs` spreads on a native input.
|
|
209
|
+
*
|
|
210
|
+
* @stability unstable
|
|
211
|
+
* @since 0.4.0
|
|
212
|
+
* @category models
|
|
213
|
+
*/
|
|
214
|
+
export interface FieldAttrs {
|
|
215
|
+
readonly name: string;
|
|
216
|
+
readonly id: string;
|
|
217
|
+
readonly "aria-invalid": true | undefined;
|
|
218
|
+
readonly "aria-describedby": string | undefined;
|
|
219
|
+
readonly onblur: () => void;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* One field of a form instance. Every property is reactive in a component. `value` can be
|
|
223
|
+
* assigned, so `bind:value={field.value}` works; an assignment counts as input for validation
|
|
224
|
+
* timing, and a change clears a server or manual error on the field and the form-level error.
|
|
225
|
+
*
|
|
226
|
+
* @stability unstable
|
|
227
|
+
* @since 0.4.0
|
|
228
|
+
* @category models
|
|
229
|
+
*/
|
|
230
|
+
export interface Field<V> {
|
|
231
|
+
value: V;
|
|
232
|
+
/** The error to show: a schema error once the field's timing reveals it, or a server one. */
|
|
233
|
+
readonly error: string | undefined;
|
|
234
|
+
/**
|
|
235
|
+
* The latest check's message for the field, shown or not: the Schema checks every change, and
|
|
236
|
+
* `error` waits for the field's timing. For a live checklist, or to see what the timing hides.
|
|
237
|
+
*/
|
|
238
|
+
readonly issue: string | undefined;
|
|
239
|
+
/** Whether the user has left the field. `useForm`'s `attrs.onblur` sets it. */
|
|
240
|
+
touched: boolean;
|
|
241
|
+
/** Whether the value differs from the initial value. */
|
|
242
|
+
readonly dirty: boolean;
|
|
243
|
+
/**
|
|
244
|
+
* Whether the form's validation is running or waiting for its debounce: the whole form's flag,
|
|
245
|
+
* the same on every field, as one run checks every field.
|
|
246
|
+
*/
|
|
247
|
+
readonly validating: boolean;
|
|
248
|
+
/** The field's element id, from the instance's `id`. */
|
|
249
|
+
readonly id: string;
|
|
250
|
+
/** The id for the element that shows `error`, which `attrs` points `aria-describedby` at. */
|
|
251
|
+
readonly errorId: string;
|
|
252
|
+
/** The `name` attribute: the path joined with dots, such as `items.0.qty`. */
|
|
253
|
+
readonly name: string;
|
|
254
|
+
/** Where the field is now. An array item's field follows the item when it moves. */
|
|
255
|
+
readonly path: AnyPath;
|
|
256
|
+
/**
|
|
257
|
+
* Attributes to spread on a native input: `name`, `id`, `aria-invalid` while `error` is set,
|
|
258
|
+
* `aria-describedby` pointing at `errorId` while it is, and an `onblur` that marks the field
|
|
259
|
+
* touched.
|
|
260
|
+
*/
|
|
261
|
+
readonly attrs: FieldAttrs;
|
|
262
|
+
readonly constraints: Constraints;
|
|
263
|
+
/** What leaving the field does, for a custom control that has no blur event. */
|
|
264
|
+
readonly blur: () => void;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* One item of an array field, with a key that stays with it when items move.
|
|
268
|
+
*
|
|
269
|
+
* @stability unstable
|
|
270
|
+
* @since 0.4.0
|
|
271
|
+
* @category models
|
|
272
|
+
*/
|
|
273
|
+
export interface ArrayItem<I> {
|
|
274
|
+
/** Stays with the item: use it to key an `{#each}` block. */
|
|
275
|
+
readonly key: number;
|
|
276
|
+
readonly index: number;
|
|
277
|
+
/** The item's own fields, for an item that is an object. */
|
|
278
|
+
readonly fields: Group<I>;
|
|
279
|
+
/** The item itself as a field, for an item that is a single value. */
|
|
280
|
+
readonly field: Field<I>;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* An array in a form. Its operations move each item's errors and touched state with the item.
|
|
284
|
+
*
|
|
285
|
+
* @stability unstable
|
|
286
|
+
* @since 0.4.0
|
|
287
|
+
* @category models
|
|
288
|
+
*/
|
|
289
|
+
export interface ArrayField<I> {
|
|
290
|
+
readonly items: readonly ArrayItem<I>[];
|
|
291
|
+
/** An error on the array itself, such as from a check on its length. */
|
|
292
|
+
readonly error: string | undefined;
|
|
293
|
+
readonly path: AnyPath;
|
|
294
|
+
/** Adds an item at the end, with a new key and no errors or touched state. */
|
|
295
|
+
readonly append: (item: I) => void;
|
|
296
|
+
/** Removes the item at `index`, and its errors and touched state. Out of range does nothing. */
|
|
297
|
+
readonly remove: (index: number) => void;
|
|
298
|
+
/**
|
|
299
|
+
* Moves the item at index `from` to index `to`, shifting the items between; its errors and
|
|
300
|
+
* touched state go with it. Either index out of range does nothing.
|
|
301
|
+
*/
|
|
302
|
+
readonly move: (from: number, to: number) => void;
|
|
303
|
+
/** Puts a new item at `index`: the old item's errors and touched state go with it. */
|
|
304
|
+
readonly replace: (index: number, item: I) => void;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Fields under one path. A component that edits a part of a form takes a `Group` of that part's
|
|
308
|
+
* shape, and any form with that shape somewhere passes `form.at(path)`.
|
|
309
|
+
*
|
|
310
|
+
* @stability unstable
|
|
311
|
+
* @since 0.4.0
|
|
312
|
+
* @category models
|
|
313
|
+
*/
|
|
314
|
+
export interface Group<T> {
|
|
315
|
+
readonly path: AnyPath;
|
|
316
|
+
/** A field by top-level key, or by path: `field("email")`, `field(["items", 0, "qty"])`. */
|
|
317
|
+
readonly field: {
|
|
318
|
+
<K extends Key<T>>(key: K): Field<At<T, readonly [K]>>;
|
|
319
|
+
<const P extends Path<T>>(path: P): Field<At<T, P>>;
|
|
320
|
+
};
|
|
321
|
+
/** An array field, by top-level key or by path. */
|
|
322
|
+
readonly array: {
|
|
323
|
+
<K extends Key<T>>(key: K): ArrayField<ItemOf<At<T, readonly [K]>>>;
|
|
324
|
+
<const P extends Path<T>>(path: P): ArrayField<ItemOf<At<T, P>>>;
|
|
325
|
+
};
|
|
326
|
+
/** The fields under a key or path, as a group to hand to a component. */
|
|
327
|
+
readonly at: {
|
|
328
|
+
<K extends Key<T>>(key: K): Group<NonNullable<At<T, readonly [K]>>>;
|
|
329
|
+
<const P extends Path<T>>(path: P): Group<NonNullable<At<T, P>>>;
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Errors to put on a form, from `mapError` or `setErrors`: by top-level field, by path, and
|
|
334
|
+
* form-level.
|
|
335
|
+
*
|
|
336
|
+
* @stability unstable
|
|
337
|
+
* @since 0.4.0
|
|
338
|
+
* @category models
|
|
339
|
+
*/
|
|
340
|
+
export interface Errors<T> {
|
|
341
|
+
readonly fields?: Readonly<Partial<Record<Key<T>, string | undefined>>> | undefined;
|
|
342
|
+
readonly paths?: readonly (readonly [Path<T>, string])[] | undefined;
|
|
343
|
+
readonly form?: string | undefined;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* One error as `form.errors` lists it.
|
|
347
|
+
*
|
|
348
|
+
* @stability unstable
|
|
349
|
+
* @since 0.4.0
|
|
350
|
+
* @category models
|
|
351
|
+
*/
|
|
352
|
+
export interface ErrorEntry {
|
|
353
|
+
/** Empty for a form-level error. */
|
|
354
|
+
readonly path: AnyPath;
|
|
355
|
+
readonly message: string;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Why `submit` didn't call the submit atom: the values didn't decode. `errors` lists what showed.
|
|
359
|
+
* Its `_tag` is `"FormValidationError"`, for `Effect.catchTag`.
|
|
360
|
+
*
|
|
361
|
+
* @stability unstable
|
|
362
|
+
* @since 0.4.0
|
|
363
|
+
* @category errors
|
|
364
|
+
*/
|
|
365
|
+
export declare const ValidationError: typeof FormValidationError;
|
|
366
|
+
/**
|
|
367
|
+
* The type of `ValidationError`.
|
|
368
|
+
*
|
|
369
|
+
* @stability unstable
|
|
370
|
+
* @since 0.4.0
|
|
371
|
+
* @category errors
|
|
372
|
+
*/
|
|
373
|
+
export type ValidationError = FormValidationError;
|
|
374
|
+
/**
|
|
375
|
+
* The attributes `form.attrs` spreads on a `<form>`: `novalidate`, so the browser's own bubbles
|
|
376
|
+
* never compete with the form's errors, and an `onsubmit` that prevents the browser's submit,
|
|
377
|
+
* submits the form, and moves focus to the first field with an error when validation fails.
|
|
378
|
+
*
|
|
379
|
+
* @stability unstable
|
|
380
|
+
* @since 0.4.0
|
|
381
|
+
* @category models
|
|
382
|
+
*/
|
|
383
|
+
export interface FormAttrs {
|
|
384
|
+
readonly novalidate: true;
|
|
385
|
+
readonly onsubmit: (event: Event) => void;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* A form instance: its values, state and actions. Every property is reactive in a component.
|
|
389
|
+
* `submitting` and `submitResult` belong to this instance, not to the submit atom, which other
|
|
390
|
+
* instances may share.
|
|
391
|
+
*
|
|
392
|
+
* @stability unstable
|
|
393
|
+
* @since 0.4.0
|
|
394
|
+
* @category models
|
|
395
|
+
*/
|
|
396
|
+
export interface Instance<T, D, A, E, Args> extends Group<T> {
|
|
397
|
+
/** The current values, encoded, as the inputs hold them. */
|
|
398
|
+
readonly values: T;
|
|
399
|
+
/**
|
|
400
|
+
* What `dirty` compares the values with: `initialValues`, or the values of the last successful
|
|
401
|
+
* submit, which become the initial values.
|
|
402
|
+
*/
|
|
403
|
+
readonly initialValues: T;
|
|
404
|
+
/** Whether the values differ from `initialValues`. */
|
|
405
|
+
readonly dirty: boolean;
|
|
406
|
+
/**
|
|
407
|
+
* Whether the form can be submitted as it stands: the latest values have been checked and
|
|
408
|
+
* decode, and no external error (from `mapError` or `setErrors`) is set. It is false while the
|
|
409
|
+
* Schema fails, while a check is running or waiting for its debounce, and while any external
|
|
410
|
+
* error is set, whether or not an error shows yet.
|
|
411
|
+
*/
|
|
412
|
+
readonly valid: boolean;
|
|
413
|
+
/** Whether validation is running or waiting for its debounce. */
|
|
414
|
+
readonly validating: boolean;
|
|
415
|
+
/** Whether a submit is waiting for the submit atom's result. */
|
|
416
|
+
readonly submitting: boolean;
|
|
417
|
+
/**
|
|
418
|
+
* The last submit's result, holding only failures `mapError` didn't claim. A validation that
|
|
419
|
+
* couldn't run at all, such as when the form's runtime failed to build, ends here as a defect.
|
|
420
|
+
*/
|
|
421
|
+
readonly submitResult: AsyncResult.AsyncResult<A, E>;
|
|
422
|
+
/**
|
|
423
|
+
* A form-level error, on no field: `mapError`'s or `setErrors`' `form`, which shows at once and
|
|
424
|
+
* clears on any change to the values, at the next submit or on reset; or an error from a check on the whole Struct
|
|
425
|
+
* that gives no `path`, which shows only after `submit` or a `validate()` of the whole form.
|
|
426
|
+
*/
|
|
427
|
+
readonly rootError: string | undefined;
|
|
428
|
+
/**
|
|
429
|
+
* Every error on show, for an error summary: the form-level one first, then the fields' in the
|
|
430
|
+
* Schema's order, which may not be the page's.
|
|
431
|
+
*/
|
|
432
|
+
readonly errors: readonly ErrorEntry[];
|
|
433
|
+
/** How many times `submit` has been called since the last reset, whatever the outcome. */
|
|
434
|
+
readonly submitCount: number;
|
|
435
|
+
/** How many times `validate` has been called since the last reset. */
|
|
436
|
+
readonly validationCount: number;
|
|
437
|
+
/** The values of the last successful submit, encoded and decoded. */
|
|
438
|
+
readonly lastSubmitted: Option.Option<{
|
|
439
|
+
readonly encoded: T;
|
|
440
|
+
readonly decoded: D;
|
|
441
|
+
}>;
|
|
442
|
+
/** Whether the values differ from the last successful submit's. */
|
|
443
|
+
readonly dirtySinceSubmit: boolean;
|
|
444
|
+
/** Attributes to spread on the `<form>` element: see `FormAttrs`. */
|
|
445
|
+
readonly attrs: FormAttrs;
|
|
446
|
+
/**
|
|
447
|
+
* Decodes the values and, when they decode, writes the submit atom and waits for its result. A
|
|
448
|
+
* call while a submit runs gets that submit's result. A submit that settles after a reset doesn't
|
|
449
|
+
* touch the fresh form. Instances that submit to the same `Atom.fn` share its one result: if
|
|
450
|
+
* another instance writes it while this submit runs, this submit resolves with the other's
|
|
451
|
+
* result, so give each instance its own atom.
|
|
452
|
+
*/
|
|
453
|
+
readonly submit: (...args: [Args] extends [undefined] ? [] : [args: Args]) => Promise<Exit.Exit<A, E | ValidationError>>;
|
|
454
|
+
/**
|
|
455
|
+
* Shows the Schema's errors at and under these paths (every path by default) and resolves
|
|
456
|
+
* whether they passed: no Schema error and no external error (from `mapError` or `setErrors`) at
|
|
457
|
+
* or under any of them. A whole-form `validate()` also counts the form-level error; a call with
|
|
458
|
+
* paths doesn't.
|
|
459
|
+
*/
|
|
460
|
+
readonly validate: (paths?: readonly (Key<T> | Path<T>)[]) => Promise<boolean>;
|
|
461
|
+
/**
|
|
462
|
+
* Sets several values at once. Not counted as input on any field, so an untouched field's error
|
|
463
|
+
* waits for its own timing; a touched field whose check it breaks shows its error at once, as
|
|
464
|
+
* after another field's change. Like any change, it clears external errors where values changed.
|
|
465
|
+
*/
|
|
466
|
+
readonly setValues: (values: T | ((previous: T) => T)) => void;
|
|
467
|
+
/**
|
|
468
|
+
* Back to `initialValues`, or to `values`, which become the initial values. Clears
|
|
469
|
+
* `lastSubmitted`, `submitResult`, touched state, every error and both counts. After a successful
|
|
470
|
+
* submit, `initialValues` are the submitted values, unless `resetOnSuccess` is set.
|
|
471
|
+
*/
|
|
472
|
+
readonly reset: (values?: T) => void;
|
|
473
|
+
/**
|
|
474
|
+
* Puts the values back to the last successful submit's, or to `initialValues` when there has
|
|
475
|
+
* been none. Touched state stays. As with `setValues`, a touched field whose check it breaks shows
|
|
476
|
+
* its error at once, and it clears external errors where values change.
|
|
477
|
+
*/
|
|
478
|
+
readonly revertToLastSubmit: () => void;
|
|
479
|
+
/**
|
|
480
|
+
* Puts errors on fields or on the form; they show at once, and keep `valid` false while set. A
|
|
481
|
+
* field's error clears when the value at its path changes, including from a change to a parent or
|
|
482
|
+
* a child path. The form-level error (`form`) clears on any change to the values, and at the
|
|
483
|
+
* next submit. Both clear on reset; blur, `validate` and a write of the same values leave them.
|
|
484
|
+
* They don't stop a submit: it checks only the Schema, and the server checks the values again.
|
|
485
|
+
*/
|
|
486
|
+
readonly setErrors: (errors: Errors<T>) => void;
|
|
487
|
+
/**
|
|
488
|
+
* Focuses the field with an error that comes first on the page, finding each by the id in
|
|
489
|
+
* `field.attrs`. Fields whose element isn't on the page are skipped. `errors` keeps the Schema's
|
|
490
|
+
* order, which may differ from the page's.
|
|
491
|
+
*/
|
|
492
|
+
readonly focusFirstError: () => void;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Where `submit` sends the decoded values: a write atom from `runtime.fn`, an `AtomRpc` mutation
|
|
496
|
+
* or `Atom.fn`, or a getter that picks one. `payload` builds what to write from the decoded values
|
|
497
|
+
* and the arguments `submit` was called with; without it, the atom gets the decoded values.
|
|
498
|
+
*
|
|
499
|
+
* @stability unstable
|
|
500
|
+
* @since 0.4.0
|
|
501
|
+
* @category models
|
|
502
|
+
*/
|
|
503
|
+
export type SubmitOptions<D, A, E, W, Args> = {
|
|
504
|
+
readonly atom: AtomInput<Atom.Writable<AsyncResult.AsyncResult<A, E>, W>>;
|
|
505
|
+
readonly payload: (decoded: D, args: Args) => W;
|
|
506
|
+
} | {
|
|
507
|
+
readonly atom: AtomInput<Atom.Writable<AsyncResult.AsyncResult<A, E>, D>>;
|
|
508
|
+
readonly payload?: undefined;
|
|
509
|
+
};
|
|
510
|
+
/**
|
|
511
|
+
* Options for an instance, shared by `useForm` and `Form.instance`.
|
|
512
|
+
*
|
|
513
|
+
* @stability unstable
|
|
514
|
+
* @since 0.4.0
|
|
515
|
+
* @category models
|
|
516
|
+
*/
|
|
517
|
+
export interface BaseInstanceOptions<T> {
|
|
518
|
+
/**
|
|
519
|
+
* The base of every element id, which must match between server and browser: pass `$props.id()`.
|
|
520
|
+
*/
|
|
521
|
+
readonly id: string;
|
|
522
|
+
/**
|
|
523
|
+
* The values to start from, or a getter. `useForm` follows a getter: a clean form takes new
|
|
524
|
+
* values, a dirty one keeps its edits.
|
|
525
|
+
*/
|
|
526
|
+
readonly initialValues: T | (() => T);
|
|
527
|
+
/**
|
|
528
|
+
* Storage for the values that you own, such as an `Atom.kvs` draft that survives a reload. A
|
|
529
|
+
* write to it from code counts as a change made in code, not as typing. So does a field's write
|
|
530
|
+
* that the atom hands back changed (trimmed, say): the field loses its own timing for that change.
|
|
531
|
+
*/
|
|
532
|
+
readonly values?: Atom.Writable<T> | undefined;
|
|
533
|
+
/**
|
|
534
|
+
* After a successful submit, reset to `initialValues` rather than adopting the submitted values
|
|
535
|
+
* as the new initial values. The reset keeps `lastSubmitted` and `submitResult`.
|
|
536
|
+
*/
|
|
537
|
+
readonly resetOnSuccess?: boolean | undefined;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Options for an instance that submits to an atom.
|
|
541
|
+
*
|
|
542
|
+
* @stability unstable
|
|
543
|
+
* @since 0.4.0
|
|
544
|
+
* @category models
|
|
545
|
+
*/
|
|
546
|
+
export interface InstanceOptions<T, D, A, E, W, Args> extends BaseInstanceOptions<T> {
|
|
547
|
+
readonly submit: SubmitOptions<D, A, E, W, Args>;
|
|
548
|
+
/**
|
|
549
|
+
* Claims a typed failure: return errors to put on fields or the form, or `undefined` to leave
|
|
550
|
+
* the failure in `submitResult`. Defects and interruptions are never passed here. The errors
|
|
551
|
+
* show at once and keep `valid` false while set. A field's error clears when the value at its
|
|
552
|
+
* path changes; the form-level one (`form`) on any change to the values. All of them clear at the
|
|
553
|
+
* next submit and on reset.
|
|
554
|
+
*/
|
|
555
|
+
readonly mapError?: ((error: E) => Errors<T> | undefined) | undefined;
|
|
556
|
+
/** Submit after each change that validates. Only for a submit that takes no arguments. */
|
|
557
|
+
readonly autoSubmit?: ([Args] extends [undefined] ? boolean : never) | undefined;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Options for an instance with no submit atom: `submit` validates and resolves with the decoded
|
|
561
|
+
* values.
|
|
562
|
+
*
|
|
563
|
+
* @stability unstable
|
|
564
|
+
* @since 0.4.0
|
|
565
|
+
* @category models
|
|
566
|
+
*/
|
|
567
|
+
export interface ValidateOnlyOptions<T> extends BaseInstanceOptions<T> {
|
|
568
|
+
readonly submit?: undefined;
|
|
569
|
+
readonly autoSubmit?: boolean | undefined;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Creates a form instance without Svelte, for tests and code outside components. It lives in
|
|
573
|
+
* `registry` (a new one by default) until `dispose` is called; a submit in flight keeps it until
|
|
574
|
+
* the submit settles. A getter for `initialValues` is read once.
|
|
575
|
+
*
|
|
576
|
+
* **Example** (Submitting a form in a test)
|
|
577
|
+
*
|
|
578
|
+
* ```ts
|
|
579
|
+
* import { Form } from "effect-atom-svelte";
|
|
580
|
+
* import { Login } from "./forms.ts";
|
|
581
|
+
*
|
|
582
|
+
* const form = Form.instance(Login, {
|
|
583
|
+
* id: "test",
|
|
584
|
+
* initialValues: { email: "", password: "" },
|
|
585
|
+
* });
|
|
586
|
+
* form.field("email").value = "ada@example.com";
|
|
587
|
+
* const exit = await form.submit();
|
|
588
|
+
* form.dispose();
|
|
589
|
+
* ```
|
|
590
|
+
*
|
|
591
|
+
* @stability unstable
|
|
592
|
+
* @since 0.4.0
|
|
593
|
+
* @category constructors
|
|
594
|
+
*/
|
|
595
|
+
export declare function instance<F extends Form<Schema.Top>, A, E, W, Args = undefined>(form: F, options: InstanceOptions<Encoded<F>, Decoded<F>, A, E, W, Args> & {
|
|
596
|
+
readonly registry?: AtomRegistry.AtomRegistry | undefined;
|
|
597
|
+
}): Instance<Encoded<F>, Decoded<F>, A, E, Args> & {
|
|
598
|
+
readonly dispose: () => void;
|
|
599
|
+
};
|
|
600
|
+
export declare function instance<F extends Form<Schema.Top>>(form: F, options: ValidateOnlyOptions<Encoded<F>> & {
|
|
601
|
+
readonly registry?: AtomRegistry.AtomRegistry | undefined;
|
|
602
|
+
}): Instance<Encoded<F>, Decoded<F>, Decoded<F>, never, undefined> & {
|
|
603
|
+
readonly dispose: () => void;
|
|
604
|
+
};
|
|
605
|
+
export {};
|