From 58bb84ac2b02cb0455758945a69e82a688f1e1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Tue, 30 Sep 2025 14:56:08 +0200 Subject: [PATCH] Form work --- packages/effect-fc/src/Form.ts | 50 +++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/effect-fc/src/Form.ts b/packages/effect-fc/src/Form.ts index 472a45b..9054291 100644 --- a/packages/effect-fc/src/Form.ts +++ b/packages/effect-fc/src/Form.ts @@ -1,5 +1,5 @@ import * as AsyncData from "@typed/async-data" -import { Array, Duration, Effect, Equal, Equivalence, Exit, flow, identity, Option, ParseResult, pipe, Pipeable, Ref, Schema, Scope, Stream, Subscribable, SubscriptionRef } from "effect" +import { Array, Duration, Effect, Equal, Exit, flow, identity, Option, ParseResult, pipe, Pipeable, Ref, Schema, Scope, Stream, Subscribable, SubscriptionRef } from "effect" import type { NoSuchElementException } from "effect/Cause" import * as React from "react" import { Hooks } from "./hooks/index.js" @@ -289,40 +289,45 @@ export const useInput: { Stream.runForEach( internalValueRef.changes.pipe( Stream.drop(1), - Stream.changesWith(Equivalence.strict()), + Stream.changesWith(Equal.equivalence()), options?.debounce ? Stream.debounce(options.debounce) : identity, ), internalValue => Ref.set(field.encodedValueRef, internalValue), ), - ], { concurrency: "unbounded" }), [field, internalValueRef]) + ], { concurrency: "unbounded" }), [field, internalValueRef, options?.debounce]) return { value, setValue, issues } }) export namespace useOptionalInput { - export interface Options> extends useInput.Options { - readonly defaultValue: Option.Option.Value + export interface Options extends useInput.Options { + readonly defaultValue: I } - export interface Result extends useInput.Result {} + export interface Result extends useInput.Result { + readonly enabled: boolean + readonly setEnabled: React.Dispatch> + } } export const useOptionalInput: { - >( - field: FormField, + ( + field: FormField>, options: useOptionalInput.Options, - ): Effect.Effect>, NoSuchElementException> -} = Effect.fnUntraced(function* >( - field: FormField, + ): Effect.Effect, NoSuchElementException> +} = Effect.fnUntraced(function* ( + field: FormField>, options: useOptionalInput.Options, ) { - const internalValueRef = yield* Hooks.useMemo(() => field.encodedValueRef.pipe( - Effect.map(Option.match({ - onSome: identity, - onNone: () => options.defaultValue, - })), - Effect.andThen(SubscriptionRef.make), + const [enabledRef, internalValueRef] = yield* Hooks.useMemo(() => Effect.andThen( + field.encodedValueRef, + Option.match({ + onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]), + onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]), + }), ), [field]) + + const [enabled, setEnabled] = yield* Hooks.useRefState(enabledRef) const [value, setValue] = yield* Hooks.useRefState(internalValueRef) const [issues] = yield* Hooks.useSubscribables(field.issuesSubscribable) @@ -336,14 +341,15 @@ export const useOptionalInput: { ), Stream.runForEach( - internalValueRef.changes.pipe( + enabledRef.changes.pipe( + Stream.zipLatest(internalValueRef.changes), Stream.drop(1), - Stream.changesWith(Equivalence.strict()), + Stream.changesWith(Equal.equivalence()), options?.debounce ? Stream.debounce(options.debounce) : identity, ), - internalValue => Ref.set(field.encodedValueRef, internalValue), + ([enabled, internalValue]) => Ref.set(field.encodedValueRef, enabled ? Option.some(internalValue) : Option.none()), ), - ], { concurrency: "unbounded" }), [field, internalValueRef]) + ], { concurrency: "unbounded" }), [field, enabledRef, internalValueRef, options.debounce]) - return { value, setValue, issues } + return { enabled, setEnabled, value, setValue, issues } })