diff --git a/packages/effect-fc/src/Form.ts b/packages/effect-fc/src/Form.ts index 23731cd..0ccac23 100644 --- a/packages/effect-fc/src/Form.ts +++ b/packages/effect-fc/src/Form.ts @@ -18,6 +18,7 @@ extends Pipeable.Pipeable { readonly schema: Schema.Schema readonly onSubmit: (value: NoInfer) => Effect.Effect + readonly autosubmit: Option.Option readonly debounce: Option.Option readonly valueRef: SubscriptionRef.SubscriptionRef> @@ -36,6 +37,7 @@ extends Pipeable.Class() implements Form { constructor( readonly schema: Schema.Schema, readonly onSubmit: (value: NoInfer) => Effect.Effect, + readonly autosubmit: Option.Option, readonly debounce: Option.Option, readonly valueRef: SubscriptionRef.SubscriptionRef>, @@ -53,11 +55,15 @@ extends Pipeable.Class() implements Form { export const isForm = (u: unknown): u is Form => Predicate.hasProperty(u, FormTypeId) export namespace make { - export interface Options { + export interface Options { readonly schema: Schema.Schema readonly initialEncodedValue: NoInfer - readonly onSubmit: (value: NoInfer) => Effect.Effect, - readonly debounce?: Duration.DurationInput, + readonly onSubmit: ( + this: Form, NoInfer, NoInfer, unknown, unknown, unknown>, + value: NoInfer, + ) => Effect.Effect + readonly autosubmit?: boolean + readonly debounce?: Duration.DurationInput } } @@ -76,6 +82,7 @@ export const make: { return new FormImpl( options.schema, options.onSubmit, + Option.fromNullable(options.autosubmit), Option.fromNullable(options.debounce), valueRef, @@ -114,9 +121,9 @@ export const run = ( Effect.exit, Effect.andThen(flow( Exit.matchEffect({ - onSuccess: v => Effect.andThen( - SubscriptionRef.set(self.valueRef, Option.some(v)), - SubscriptionRef.set(self.errorRef, Option.none()), + onSuccess: v => SubscriptionRef.set(self.valueRef, Option.some(v)).pipe( + Effect.andThen(SubscriptionRef.set(self.errorRef, Option.none())), + Effect.as(Option.some(v)), ), onFailure: c => Option.match( Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), @@ -158,7 +165,7 @@ export const submit = ( ) export namespace service { - export interface Options + export interface Options extends make.Options {} } diff --git a/packages/example/src/todo/Todo.tsx b/packages/example/src/todo/Todo.tsx index 2fb685f..f01304b 100644 --- a/packages/example/src/todo/Todo.tsx +++ b/packages/example/src/todo/Todo.tsx @@ -5,10 +5,16 @@ import { Component, Form, Hooks, Memoized, Subscribable, SubscriptionSubRef } fr import { FaArrowDown, FaArrowUp } from "react-icons/fa" import { FaDeleteLeft } from "react-icons/fa6" import * as Domain from "@/domain" +import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput" import { DateTimeUtcFromZonedInput } from "@/lib/schema" import { TodosState } from "./TodosState.service" +const TodoFormSchema = Schema.compose(Schema.Struct({ + ...Domain.Todo.Todo.fields, + completedAt: Schema.OptionFromSelf(DateTimeUtcFromZonedInput), +}), Domain.Todo.Todo) + const makeTodo = makeUuid4.pipe( Effect.map(id => Domain.Todo.Todo.make({ id, @@ -28,51 +34,62 @@ export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoPr const runtime = yield* Effect.runtime() const state = yield* TodosState - const { ref, indexRef, contentRef, completedAtRef } = yield* Hooks.useMemo(() => Match.value(props).pipe( - Match.tag("new", () => Effect.Do.pipe( - Effect.bind("ref", () => Effect.andThen(makeTodo, SubscriptionRef.make)), - Effect.let("indexRef", () => Subscribable.make({ get: Effect.succeed(-1), changes: Stream.empty })), - )), - Match.tag("edit", ({ id }) => Effect.Do.pipe( - Effect.let("ref", () => state.getElementRef(id)), - Effect.let("indexRef", () => state.getIndexSubscribable(id)), - )), - Match.exhaustive, + const [indexRef, form, contentField, completedAtField] = yield* Component.useOnChange(() => Effect.gen(function*() { + const indexRef = Match.value(props).pipe( + Match.tag("new", () => Subscribable.make({ get: Effect.succeed(-1), changes: Stream.empty })), + Match.tag("edit", ({ id }) => state.getIndexSubscribable(id)), + Match.exhaustive, + ) - Effect.let("contentRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["content"])), - Effect.let("completedAtRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["completedAt"])), - ), [props._tag, props._tag === "edit" ? props.id : undefined]) - - const { form } = yield* Component.useOnChange(() => Effect.gen(function*() { const form = yield* Form.service({ - schema: Domain.Todo.TodoFromJson, - initialEncodedValue: yield* Schema.encode(Domain.Todo.TodoFromJson)( + schema: TodoFormSchema, + initialEncodedValue: yield* Schema.encode(TodoFormSchema)( yield* Match.value(props).pipe( Match.tag("new", () => makeTodo), Match.tag("edit", ({ id }) => state.getElementRef(id)), Match.exhaustive, ) ), - onSubmit: v => Effect.void, + onSubmit: function(todo) { + return Match.value(props).pipe( + Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe( + Effect.andThen(makeTodo), + Effect.andThen(Schema.encode(TodoFormSchema)), + Effect.andThen(v => Ref.set(this.encodedValueRef, v)), + )), + Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)), + Match.exhaustive, + ) + }, }) - return { form } + return [ + indexRef, + form, + Form.field(form, ["content"]), + Form.field(form, ["completedAt"]), + ] as const }), [props._tag, props._tag === "edit" ? props.id : undefined]) const [index, size] = yield* Hooks.useSubscribables(indexRef, state.sizeSubscribable) + const submit = yield* Form.useSubmit(form) + const TextFieldFormInputFC = yield* TextFieldFormInput return ( - + - DateTime.now)} + defaultValue="" /> {props._tag === "new" &&