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.
Files changed (56) hide show
  1. package/README.md +5 -2
  2. package/dist/Form.d.ts +605 -0
  3. package/dist/Form.js +67 -0
  4. package/dist/Hooks.svelte.d.ts +70 -10
  5. package/dist/Hooks.svelte.js +489 -163
  6. package/dist/HydrationBoundary.svelte +134 -17
  7. package/dist/HydrationBoundary.svelte.d.ts +13 -4
  8. package/dist/Inspector.d.ts +7 -3
  9. package/dist/Inspector.js +113 -29
  10. package/dist/RegistryContext.d.ts +2 -0
  11. package/dist/RegistryContext.js +13 -5
  12. package/dist/ScopedAtom.d.ts +5 -4
  13. package/dist/ScopedAtom.js +20 -11
  14. package/dist/SvelteKit.d.ts +71 -12
  15. package/dist/SvelteKit.js +133 -54
  16. package/dist/index.d.ts +10 -1
  17. package/dist/index.js +10 -1
  18. package/dist/internal/awaitWrite.d.ts +11 -0
  19. package/dist/internal/awaitWrite.js +27 -0
  20. package/dist/internal/boundaries.d.ts +9 -0
  21. package/dist/internal/boundaries.js +10 -0
  22. package/dist/internal/disposal.d.ts +13 -0
  23. package/dist/internal/disposal.js +43 -0
  24. package/dist/internal/form/array.d.ts +21 -0
  25. package/dist/internal/form/array.js +57 -0
  26. package/dist/internal/form/constraints.d.ts +13 -0
  27. package/dist/internal/form/constraints.js +166 -0
  28. package/dist/internal/form/engine.d.ts +56 -0
  29. package/dist/internal/form/engine.js +842 -0
  30. package/dist/internal/form/engineOf.d.ts +26 -0
  31. package/dist/internal/form/engineOf.js +43 -0
  32. package/dist/internal/form/field.d.ts +30 -0
  33. package/dist/internal/form/field.js +68 -0
  34. package/dist/internal/form/group.d.ts +13 -0
  35. package/dist/internal/form/group.js +20 -0
  36. package/dist/internal/form/instance.d.ts +41 -0
  37. package/dist/internal/form/instance.js +135 -0
  38. package/dist/internal/form/issues.d.ts +23 -0
  39. package/dist/internal/form/issues.js +144 -0
  40. package/dist/internal/form/paths.d.ts +47 -0
  41. package/dist/internal/form/paths.js +175 -0
  42. package/dist/internal/form/types.d.ts +80 -0
  43. package/dist/internal/form/types.js +1 -0
  44. package/dist/internal/form/validationError.d.ts +13 -0
  45. package/dist/internal/form/validationError.js +6 -0
  46. package/dist/internal/hydration.d.ts +11 -2
  47. package/dist/internal/hydration.js +26 -4
  48. package/dist/internal/nodeInternals.d.ts +6 -0
  49. package/dist/internal/nodeInternals.js +64 -7
  50. package/dist/internal/renderEnd.d.ts +13 -0
  51. package/dist/internal/renderEnd.js +34 -0
  52. package/dist/internal/teardown.svelte.d.ts +17 -0
  53. package/dist/internal/teardown.svelte.js +44 -0
  54. package/dist/internal/troubleshooting.d.ts +2 -0
  55. package/dist/internal/troubleshooting.js +3 -0
  56. package/package.json +12 -10
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
+ }
@@ -4,8 +4,10 @@
4
4
  * @since 0.1.0
5
5
  */
6
6
  import { Exit } from "effect";
7
+ import type { Schema } from "effect";
7
8
  import { AsyncResult, Atom } from "effect/reactivity";
8
9
  import type { AtomRef } from "effect/reactivity";
10
+ import type * as Form from "./Form.ts";
9
11
  /**
10
12
  * An atom, or a getter so the hook follows the atom when reactive state picks a different one.
11
13
  *
@@ -122,12 +124,14 @@ export declare const useAtomMount: (input: AtomInput<Atom.Atom<unknown>>) => voi
122
124
  *
123
125
  * In `promise` and `promiseExit` modes the setter waits for the atom's next settled result. The
124
126
  * wait holds the atom, so a call still in flight keeps it running after the component is
125
- * destroyed, until it settles. The call's `signal` cancels only the wait: an `Atom.fn` already
127
+ * destroyed, until it settles, unless the `RegistryProvider` that created the registry is
128
+ * destroyed: that interrupts the call and settles it as interrupted. The call's `signal` cancels only the wait: an `Atom.fn` already
126
129
  * running keeps going (send `Atom.Interrupt` to stop it). A signal that is already aborted settles
127
130
  * the call as interrupted without writing, as `fetch` does. Calls on one `Atom.fn` share its single
128
131
  * result, so a new call supersedes one in flight and every pending call resolves with the latest
129
132
  * call's result. `Atom.Reset` has no result to wait for, so these modes leave it out of their
130
- * types and reject it; reset with a `value` mode setter.
133
+ * types and reject it; reset with a `value` mode setter. A reset written while a promise-mode call
134
+ * is pending settles that promise as interrupted. To cancel a call, send `Atom.Interrupt`.
131
135
  *
132
136
  * **Example** (Updating an atom from its current value)
133
137
  *
@@ -207,9 +211,10 @@ export declare const useAtomSubscribe: <A>(input: AtomInput<Atom.Atom<A>>, f: (v
207
211
  * an atom that can only compute in the browser can start from a value on the server.
208
212
  *
209
213
  * A value applies once while the atom is held, so a component mounted later, or a second
210
- * component with its own value, does not overwrite a value the atom has moved on from. Once nothing
211
- * holds the atom and the registry disposes it, the next component to start it applies its value
212
- * again. On the server a value lasts one render, so each request against a shared registry applies
214
+ * component with its own value, does not overwrite a value the atom has moved on from since the
215
+ * first applied one. An atom that is already in use without one, read by another component, is set
216
+ * to the value, as `@effect/atom-react` does. Once nothing holds the atom and the registry disposes
217
+ * it, the next component to start it applies its value again. On the server a value lasts one render, so each request against a shared registry applies
213
218
  * its own; requests rendering at the same time share the registry's atoms, and with them the value
214
219
  * the first one applied.
215
220
  *
@@ -232,8 +237,9 @@ export declare const useAtomSubscribe: <A>(input: AtomInput<Atom.Atom<A>>, f: (v
232
237
  */
233
238
  export declare const useAtomInitialValues: (initialValues: Iterable<readonly [Atom.Atom<unknown>, unknown]>) => void;
234
239
  /**
235
- * Reads an `AtomRef`, following it when the getter returns a different ref. For one property of a
236
- * ref, use `useAtomRefPropValue`.
240
+ * Reads an `AtomRef`, following it when the getter returns a different ref. For a writable ref,
241
+ * `current` can be assigned too, which sets the ref, so `bind:value={name.current}` works; a
242
+ * read-only ref, as from `map`, can't be. For one property of a ref, use `useAtomRefPropValue`.
237
243
  *
238
244
  * A ref has no registry. On the server, one created at module level is shared by every request,
239
245
  * so don't write a visitor's data to it there: see
@@ -253,7 +259,8 @@ export declare const useAtomInitialValues: (initialValues: Iterable<readonly [At
253
259
  * @since 0.1.0
254
260
  * @category hooks
255
261
  */
256
- export declare const useAtomRef: <A>(input: AtomInput<AtomRef.ReadonlyRef<A>>) => AtomValue<A>;
262
+ export declare function useAtomRef<A>(input: AtomInput<AtomRef.AtomRef<A>>): AtomState<A>;
263
+ export declare function useAtomRef<A>(input: AtomInput<AtomRef.ReadonlyRef<A>>): AtomValue<A>;
257
264
  /**
258
265
  * Returns `ref.prop(prop)`, the `AtomRef` for one property of an `AtomRef`. It takes the ref
259
266
  * itself, not a getter, and does not follow later changes to `ref` or `prop`; to read a property
@@ -276,7 +283,8 @@ export declare const useAtomRef: <A>(input: AtomInput<AtomRef.ReadonlyRef<A>>) =
276
283
  export declare const useAtomRefProp: <A, K extends keyof A>(ref: AtomRef.AtomRef<A>, prop: K) => AtomRef.AtomRef<A[K]>;
277
284
  /**
278
285
  * Reads one property of an `AtomRef`, following the getter to a different ref. `prop` is taken
279
- * once, when the component starts, and is not followed.
286
+ * once, when the component starts, and is not followed. Assigning `current` sets the property, so
287
+ * `bind:checked={done.current}` works.
280
288
  *
281
289
  * **Example** (Reading one property of a ref)
282
290
  *
@@ -292,7 +300,7 @@ export declare const useAtomRefProp: <A, K extends keyof A>(ref: AtomRef.AtomRef
292
300
  * @since 0.1.0
293
301
  * @category hooks
294
302
  */
295
- export declare const useAtomRefPropValue: <A, K extends keyof A>(input: AtomInput<AtomRef.AtomRef<A>>, prop: K) => AtomValue<A[K]>;
303
+ export declare const useAtomRefPropValue: <A, K extends keyof A>(input: AtomInput<AtomRef.AtomRef<A>>, prop: K) => AtomState<A[K]>;
296
304
  /**
297
305
  * Options for `useAtomResult`.
298
306
  *
@@ -394,3 +402,55 @@ export declare function useAtomSuspense<A, E>(input: AtomInput<Atom.Atom<AsyncRe
394
402
  export declare function useAtomSuspense<A, E>(input: AtomInput<Atom.Atom<AsyncResult.AsyncResult<A, E>>>, options?: SuspenseOptions & {
395
403
  readonly includeFailure?: boolean | undefined;
396
404
  }): AtomValue<Promise<A | AsyncResult.Success<A, E> | AsyncResult.Failure<A, E>>>;
405
+ /**
406
+ * Creates an instance of a form from `Form.make` in a component. Its state lives in the
407
+ * component's registry and is released when the component is destroyed; a submit in flight keeps
408
+ * it, and keeps its atom running, until the submit settles, so a save finishes after a dialog
409
+ * closes. Share an instance by passing it down, or create it in the parent whose lifetime it
410
+ * should have, such as a wizard around its steps.
411
+ *
412
+ * Pass `id` from `$props.id()`: field ids are built from it, and must match between the server and
413
+ * the browser. On the server the form renders from `initialValues` and doesn't validate, so there
414
+ * is nothing to hydrate. A getter for `initialValues` is followed: when it changes, a clean form
415
+ * takes the new values and a dirty one keeps its edits.
416
+ *
417
+ * **Example** (A login form)
418
+ *
419
+ * ```svelte
420
+ * <script lang="ts">
421
+ * import { useForm } from "effect-atom-svelte";
422
+ * import { Login, loginAtom } from "./forms.ts";
423
+ *
424
+ * const id = $props.id();
425
+ * const form = useForm(Login, {
426
+ * id,
427
+ * initialValues: { email: "", password: "" },
428
+ * submit: { atom: loginAtom },
429
+ * mapError: (error) =>
430
+ * error._tag === "WrongPassword"
431
+ * ? { form: "Wrong email or password" }
432
+ * : undefined,
433
+ * });
434
+ * const email = form.field("email");
435
+ * </script>
436
+ *
437
+ * <form {...form.attrs}>
438
+ * <label for={email.id}>Email</label>
439
+ * <input {...email.attrs} bind:value={email.value} />
440
+ * {#if email.error}
441
+ * <p id={email.errorId}>{email.error}</p>
442
+ * {/if}
443
+ * {#if form.rootError}
444
+ * <p role="alert">{form.rootError}</p>
445
+ * {/if}
446
+ * <button disabled={form.submitting}>Log in</button>
447
+ * </form>
448
+ * ```
449
+ *
450
+ * @see {@link https://atom.jarrednorris.dev/forms | Forms}
451
+ * @stability unstable
452
+ * @since 0.4.0
453
+ * @category hooks
454
+ */
455
+ export declare function useForm<F extends Form.Form<Schema.Top>, A, E, W, Args = undefined>(form: F, options: Form.InstanceOptions<Form.Encoded<F>, Form.Decoded<F>, A, E, W, Args>): Form.Instance<Form.Encoded<F>, Form.Decoded<F>, A, E, Args>;
456
+ export declare function useForm<F extends Form.Form<Schema.Top>>(form: F, options: Form.ValidateOnlyOptions<Form.Encoded<F>>): Form.Instance<Form.Encoded<F>, Form.Decoded<F>, Form.Decoded<F>, never, void>;