diff --git a/packages/effect-fc/src/Form.ts b/packages/effect-fc/src/Form.ts index c5a02d0..1b982e7 100644 --- a/packages/effect-fc/src/Form.ts +++ b/packages/effect-fc/src/Form.ts @@ -1,4 +1,4 @@ -import { Array, Duration, Effect, Equivalence, flow, identity, Option, ParseResult, Pipeable, Schema, Stream, Subscribable, SubscriptionRef } from "effect" +import { Array, Duration, Effect, Equivalence, flow, identity, Option, ParseResult, Pipeable, Schema, Scope, Stream, Subscribable, SubscriptionRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as React from "react" import { Hooks } from "./hooks/index.js" @@ -53,7 +53,7 @@ extends Pipeable.Class() implements Form { export namespace make { - export interface Options { + export interface Options { readonly schema: Schema.Schema readonly initialEncodedValue: NoInfer } @@ -70,6 +70,39 @@ export const make: { ) }) +const run = (self: Form) => Stream.runForEach( + self.encodedValueRef.changes, + flow( + Schema.decode(self.schema), + Effect.andThen(v => SubscriptionRef.set(self.valueRef, Option.some(v))), + Effect.andThen(SubscriptionRef.set(self.errorRef, Option.none())), + Effect.catchTag("ParseError", e => SubscriptionRef.set(self.errorRef, Option.some(e))) + ), +) + +export namespace service { + export interface Options extends make.Options {} +} + +export const service = ( + options: service.Options +): Effect.Effect, never, R | Scope.Scope> => Effect.tap( + make(options), + form => Effect.forkScoped(run(form)), +) + +export namespace useForm { + export interface Options extends make.Options {} +} + +export const useForm: { + (options: service.Options): Effect.Effect, never, R | Scope.Scope> +} = Effect.fnUntraced(function* (options: service.Options) { + const form = yield* Hooks.useOnce(() => make(options)) + yield* Hooks.useFork(() => run(form), [form]) + return form +}) + export namespace useInput { export interface Options { @@ -114,13 +147,7 @@ export const useInput: { internalValue => self.encodedValueRef.pipe( Effect.andThen(encodedValue => PropertyPath.immutableSet(encodedValue, path, internalValue)), - Effect.tap(encodedValue => SubscriptionRef.set(self.encodedValueRef, encodedValue)), - Effect.andThen(flow( - Schema.decode(self.schema), - Effect.andThen(v => SubscriptionRef.set(self.valueRef, Option.some(v))), - Effect.andThen(SubscriptionRef.set(self.errorRef, Option.none())), - Effect.catchTag("ParseError", e => SubscriptionRef.set(self.errorRef, Option.some(e))) - )), + Effect.andThen(encodedValue => SubscriptionRef.set(self.encodedValueRef, encodedValue)), ), ), [internalValueRef, self, ...path]) diff --git a/packages/example/src/routes/form.tsx b/packages/example/src/routes/form.tsx index 4e65251..c977569 100644 --- a/packages/example/src/routes/form.tsx +++ b/packages/example/src/routes/form.tsx @@ -12,7 +12,7 @@ const RegisterFormSchema = Schema.Struct({ }) class RegisterForm extends Effect.Service()("RegisterForm", { - scoped: Form.make({ + scoped: Form.service({ schema: RegisterFormSchema, initialEncodedValue: { email: "", password: "" }, }) @@ -64,7 +64,7 @@ class RegisterPage extends Component.makeUntraced(function* RegisterPage() { const RegisterRoute = Component.makeUntraced(function* RegisterRoute() { - const context = yield* useContext(RegisterForm.Default) + const context = yield* useContext(RegisterForm.Default, { finalizerExecutionMode: "fork" }) const RegisterRouteFC = yield* Effect.provide(RegisterPage, context) return