From a7471c0d49dc282d05641c411bdce2d22b5b1c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Thu, 25 Sep 2025 02:31:46 +0200 Subject: [PATCH] Form work --- packages/effect-fc/src/Form.ts | 22 +++++++++++++++++++--- packages/example/src/routes/form.tsx | 11 ++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/effect-fc/src/Form.ts b/packages/effect-fc/src/Form.ts index 5ccf511..94391b8 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, Scope, Stream, Subscribable, SubscriptionRef } from "effect" +import { Array, Duration, Effect, Equal, Equivalence, flow, identity, Option, ParseResult, pipe, 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" @@ -16,6 +16,8 @@ extends Pipeable.Pipeable { readonly encodedValueRef: SubscriptionRef.SubscriptionRef, readonly errorRef: SubscriptionRef.SubscriptionRef>, + readonly canSubmitSubscribable: Subscribable.Subscribable + makeFieldIssuesSubscribable>( path: P ): Subscribable.Subscribable @@ -25,6 +27,7 @@ extends Pipeable.Pipeable { class FormImpl extends Pipeable.Class() implements Form { readonly [TypeId]: TypeId = TypeId + readonly canSubmitSubscribable: Subscribable.Subscribable constructor( readonly schema: Schema.Schema, @@ -33,6 +36,18 @@ extends Pipeable.Class() implements Form { readonly errorRef: SubscriptionRef.SubscriptionRef>, ) { super() + + this.canSubmitSubscribable = pipe( + ([value, error]: readonly [ + Option.Option, + Option.Option, + ]) => Option.isSome(value) && Option.isNone(error), + + filter => SubscribableInternal.make({ + get: Effect.map(Effect.all([valueRef, errorRef]), filter), + get changes() { return Stream.map(Stream.zipLatestAll(valueRef.changes, errorRef.changes), filter)}, + }), + ) } makeFieldIssuesSubscribable>(path: P) { @@ -146,8 +161,9 @@ export const useInput: { Stream.drop(1), ), - upstreamEncodedValue => internalValueRef.pipe( - + upstreamEncodedValue => Effect.whenEffect( + SubscriptionRef.set(internalValueRef, upstreamEncodedValue), + Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)), ), ), diff --git a/packages/example/src/routes/form.tsx b/packages/example/src/routes/form.tsx index 850f56d..569d26c 100644 --- a/packages/example/src/routes/form.tsx +++ b/packages/example/src/routes/form.tsx @@ -1,9 +1,9 @@ import { runtime } from "@/runtime" -import { Callout, Container, Flex, TextField } from "@radix-ui/themes" +import { Button, Callout, Container, Flex, TextField } from "@radix-ui/themes" import { createFileRoute } from "@tanstack/react-router" -import { Array, Console, Effect, Option, Schema, Stream } from "effect" +import { Array, Effect, Option, Schema } from "effect" import { Component, Form } from "effect-fc" -import { useContext, useFork } from "effect-fc/hooks" +import { useContext, useSubscribables } from "effect-fc/hooks" const email = Schema.pattern( @@ -33,8 +33,7 @@ class RegisterPage extends Component.makeUntraced("RegisterPage")(function*() { const emailInput = yield* Form.useInput(form, ["email"], { debounce: "200 millis" }) const passwordInput = yield* Form.useInput(form, ["password"], { debounce: "200 millis" }) - yield* useFork(() => Stream.runForEach(form.valueRef.changes, Console.log), []) - // yield* useFork(() => Stream.runForEach(form.errorRef.changes, Console.log), []) + const [canSubmit] = yield* useSubscribables(form.canSubmitSubscribable) return ( @@ -68,6 +67,8 @@ class RegisterPage extends Component.makeUntraced("RegisterPage")(function*() { onNone: () => <>, })} + + )