From 0f20587827ae56d4713a46bcab1dacbba78188eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 30 Mar 2026 16:25:16 +0200 Subject: [PATCH] Refactor Form --- packages/effect-fc/src/Form.ts | 9 +++- packages/effect-fc/tsconfig.json | 1 + .../src/lib/form/TextFieldFormInputView.tsx | 18 +++---- .../form/TextFieldOptionalFormInputView.tsx | 16 +++--- packages/example/src/routes/form.tsx | 52 +++++++++++-------- 5 files changed, 53 insertions(+), 43 deletions(-) diff --git a/packages/effect-fc/src/Form.ts b/packages/effect-fc/src/Form.ts index b05e80e..eda1864 100644 --- a/packages/effect-fc/src/Form.ts +++ b/packages/effect-fc/src/Form.ts @@ -21,6 +21,7 @@ extends Pipeable.Pipeable { readonly issues: Subscribable.Subscribable readonly isValidating: Subscribable.Subscribable readonly canSubmit: Subscribable.Subscribable + readonly isSubmitting: Subscribable.Subscribable } export class FormImpl @@ -34,6 +35,7 @@ extends Pipeable.Class() implements Form { readonly issues: Subscribable.Subscribable, readonly isValidating: Subscribable.Subscribable, readonly canSubmit: Subscribable.Subscribable, + readonly isSubmitting: Subscribable.Subscribable, ) { super() } @@ -80,7 +82,9 @@ extends Pipeable.Class() implements RootForm { readonly issues: Lens.Lens, readonly validationFiber: Lens.Lens>, never, never, never, never>, readonly isValidating: Subscribable.Subscribable, + readonly canSubmit: Subscribable.Subscribable, + readonly isSubmitting: Subscribable.Subscribable, readonly runSemaphore: Effect.Semaphore, ) { @@ -176,7 +180,6 @@ export declare namespace make { readonly schema: Schema.Schema readonly initialEncodedValue: NoInfer readonly autosubmit?: boolean - readonly debounce?: Duration.DurationInput } } @@ -203,6 +206,7 @@ export const make = Effect.fnUntraced(function* ( @@ -212,6 +216,7 @@ export const make = Effect.fnUntraced(function* Result.isRunning(result) || Result.hasRefreshingFlag(result)), yield* Effect.makeSemaphore(1), ) @@ -255,6 +260,7 @@ export const focusObjectField =

filterIssuesByPath(issues, path)), form.isValidating, form.canSubmit, + form.isSubmitting, ) } @@ -272,6 +278,7 @@ export const focusArrayAt =

filterIssuesByPath(issues, path)), form.isValidating, form.canSubmit, + form.isSubmitting, ) } diff --git a/packages/effect-fc/tsconfig.json b/packages/effect-fc/tsconfig.json index 9d120e6..6ad0810 100644 --- a/packages/effect-fc/tsconfig.json +++ b/packages/effect-fc/tsconfig.json @@ -25,6 +25,7 @@ "noPropertyAccessFromIndexSignature": false, // Build + "rootDir": "./src", "outDir": "./dist", "declaration": true, "sourceMap": true, diff --git a/packages/example/src/lib/form/TextFieldFormInputView.tsx b/packages/example/src/lib/form/TextFieldFormInputView.tsx index 53621d3..d7b462c 100644 --- a/packages/example/src/lib/form/TextFieldFormInputView.tsx +++ b/packages/example/src/lib/form/TextFieldFormInputView.tsx @@ -1,24 +1,22 @@ import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes" -import { Array, Option } from "effect" +import { Array, Option, Struct } from "effect" import { Component, Form, Subscribable } from "effect-fc" export declare namespace TextFieldFormInputView { - export interface Props - extends TextField.RootProps, Form.useInput.Options { - readonly field: Form.FormField + export interface Props extends Omit, Form.useInput.Options { + readonly form: Form.Form } } - export class TextFieldFormInputView extends Component.make("TextFieldFormInputView")(function*( props: TextFieldFormInputView.Props ) { - const input = yield* Form.useInput(props.field, props) + const input = yield* Form.useInput(props.form, props) const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([ - props.field.issues, - props.field.isValidating, - props.field.isSubmitting, + props.form.issues, + props.form.isValidating, + props.form.isSubmitting, ]) return ( @@ -27,7 +25,7 @@ export class TextFieldFormInputView extends Component.make("TextFieldFormInputVi value={input.value} onChange={e => input.setValue(e.target.value)} disabled={isSubmitting} - {...props} + {...Struct.omit(props, "form")} > {isValidating && diff --git a/packages/example/src/lib/form/TextFieldOptionalFormInputView.tsx b/packages/example/src/lib/form/TextFieldOptionalFormInputView.tsx index fa9e361..a3b5eda 100644 --- a/packages/example/src/lib/form/TextFieldOptionalFormInputView.tsx +++ b/packages/example/src/lib/form/TextFieldOptionalFormInputView.tsx @@ -4,21 +4,19 @@ import { Component, Form, Subscribable } from "effect-fc" export declare namespace TextFieldOptionalFormInputView { - export interface Props - extends Omit, Form.useOptionalInput.Options { - readonly field: Form.FormField> + export interface Props extends Omit, Form.useOptionalInput.Options { + readonly form: Form.Form> } } - export class TextFieldOptionalFormInputView extends Component.make("TextFieldOptionalFormInputView")(function*( props: TextFieldOptionalFormInputView.Props ) { - const input = yield* Form.useOptionalInput(props.field, props) + const input = yield* Form.useOptionalInput(props.form, props) const [issues, isValidating, isSubmitting] = yield* Subscribable.useSubscribables([ - props.field.issues, - props.field.isValidating, - props.field.isSubmitting, + props.form.issues, + props.form.isValidating, + props.form.isSubmitting, ]) return ( @@ -27,7 +25,7 @@ export class TextFieldOptionalFormInputView extends Component.make("TextFieldOpt value={input.value} onChange={e => input.setValue(e.target.value)} disabled={!input.enabled || isSubmitting} - {...Struct.omit(props, "defaultValue")} + {...Struct.omit(props, "form", "defaultValue")} > ()("RegisterFormService", { - scoped: Form.service({ - schema: RegisterFormSchema.pipe( - Schema.compose( - Schema.transformOrFail( - Schema.typeSchema(RegisterFormSchema), - Schema.typeSchema(RegisterFormSchema), - { - decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)), - encode: ParseResult.succeed, - }, + scoped: Effect.gen(function*() { + const form = yield* Form.service({ + schema: RegisterFormSchema.pipe( + Schema.compose( + Schema.transformOrFail( + Schema.typeSchema(RegisterFormSchema), + Schema.typeSchema(RegisterFormSchema), + { + decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)), + encode: ParseResult.succeed, + }, + ), ), ), - ), - initialEncodedValue: { email: "", password: "", birth: Option.none() }, - f: Effect.fnUntraced(function*([value]) { - yield* Effect.sleep("500 millis") - return yield* Schema.decode(RegisterFormSubmitSchema)(value) - }), - debounce: "500 millis", + initialEncodedValue: { email: "", password: "", birth: Option.none() }, + f: Effect.fnUntraced(function*([value]) { + yield* Effect.sleep("500 millis") + return yield* Schema.decode(RegisterFormSubmitSchema)(value) + }), + }) + + return { + form, + emailField: Form.focusObjectField(form, "email"), + passwordField: Form.focusObjectField(form, "password"), + birthField: Form.focusObjectField(form, "birth"), + } as const }) }) {} class RegisterFormView extends Component.make("RegisterFormView")(function*() { const form = yield* RegisterFormService const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([ - form.canSubmit, - form.mutation.result, + form.form.canSubmit, + form.form.mutation.result, ]) const runPromise = yield* Component.useRunPromise() @@ -84,12 +92,10 @@ class RegisterFormView extends Component.make("RegisterFormView")(function*() {

{ e.preventDefault() - void runPromise(form.submit) + void runPromise(form.form.submit) }}> - +