effect-atom-svelte 0.3.0 → 0.4.1

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