0.1.4 #5

Merged
Thilawyn merged 67 commits from next into master 2025-10-02 18:18:23 +02:00
5 changed files with 122 additions and 52 deletions
Showing only changes of commit 3456440e1e - Show all commits

View File

@@ -336,8 +336,8 @@ export const useInput: {
}) })
export namespace useOptionalInput { export namespace useOptionalInput {
export interface Options<I> extends useInput.Options { export interface Options<T> extends useInput.Options {
readonly defaultValue: I readonly defaultValue: T
} }
export interface Result<T> extends useInput.Result<T> { export interface Result<T> extends useInput.Result<T> {

View File

@@ -1,7 +1,6 @@
import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes" import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes"
import { Array, Option } from "effect" import { Array, Option } from "effect"
import { Component, Form } from "effect-fc" import { Component, Form, Hooks } from "effect-fc"
import { useSubscribables } from "effect-fc/Hooks"
export interface TextFieldFormInputProps export interface TextFieldFormInputProps
@@ -9,38 +8,42 @@ extends TextField.RootProps, Form.useInput.Options {
readonly field: Form.FormField<any, string> readonly field: Form.FormField<any, string>
} }
export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInput")(function*(props: TextFieldFormInputProps) { export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInput")(
const { value, setValue } = yield* Form.useInput(props.field, props) function*(props: TextFieldFormInputProps) {
const [issues, isValidating, isSubmitting] = yield* useSubscribables( const { value, setValue } = yield* Form.useInput(props.field, props)
props.field.issuesSubscribable, const [issues, isValidating, isSubmitting] = yield* Hooks.useSubscribables(
props.field.isValidatingSubscribable, props.field.issuesSubscribable,
props.field.isSubmittingSubscribable, props.field.isValidatingSubscribable,
) props.field.isSubmittingSubscribable,
)
return ( return (
<Flex direction="column" gap="1"> <Flex direction="column" gap="1">
<TextField.Root <TextField.Root
value={value} value={value}
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
disabled={isSubmitting} disabled={isSubmitting}
{...props} {...props}
> >
{isValidating && {isValidating &&
<TextField.Slot side="right"> <TextField.Slot side="right">
<Spinner /> <Spinner />
</TextField.Slot> </TextField.Slot>
} }
</TextField.Root>
{Option.match(Array.head(issues), { {props.children}
onSome: issue => ( </TextField.Root>
<Callout.Root>
<Callout.Text>{issue.message}</Callout.Text>
</Callout.Root>
),
onNone: () => <></>, {Option.match(Array.head(issues), {
})} onSome: issue => (
</Flex> <Callout.Root>
) <Callout.Text>{issue.message}</Callout.Text>
}) {} </Callout.Root>
),
onNone: () => <></>,
})}
</Flex>
)
}
) {}

View File

@@ -0,0 +1,57 @@
import { Callout, Flex, Spinner, Switch, TextField } from "@radix-ui/themes"
import { Array, Option } from "effect"
import { Component, Form, Hooks } from "effect-fc"
export interface TextFieldFormOptionalInputProps
extends Omit<TextField.RootProps, "defaultValue">, Form.useOptionalInput.Options<string> {
readonly field: Form.FormField<any, Option.Option<string>>
}
export class TextFieldFormOptionalInput extends Component.makeUntraced("TextFieldFormOptionalInput")(
function*(props: TextFieldFormOptionalInputProps) {
const { value, setValue, enabled, setEnabled } = yield* Form.useOptionalInput(props.field, props)
const [issues, isValidating, isSubmitting] = yield* Hooks.useSubscribables(
props.field.issuesSubscribable,
props.field.isValidatingSubscribable,
props.field.isSubmittingSubscribable,
)
return (
<Flex direction="column" gap="1">
<TextField.Root
value={value}
onChange={e => setValue(e.target.value)}
disabled={!enabled || isSubmitting}
{...props}
>
<TextField.Slot side="left">
<Switch
size="1"
checked={enabled}
onCheckedChange={setEnabled}
/>
</TextField.Slot>
{isValidating &&
<TextField.Slot side="right">
<Spinner />
</TextField.Slot>
}
{props.children}
</TextField.Root>
{Option.match(Array.head(issues), {
onSome: issue => (
<Callout.Root>
<Callout.Text>{issue.message}</Callout.Text>
</Callout.Root>
),
onNone: () => <></>,
})}
</Flex>
)
}
) {}

View File

@@ -29,12 +29,10 @@ export const TextFieldInput = <A, R, O extends boolean = false>(options: {
) = options.optional ) = options.optional
? { ? {
optional: true, optional: true,
// eslint-disable-next-line react-hooks/rules-of-hooks
...yield* useOptionalInput({ ...options, ...props as TextFieldOptionalInputProps<A, R> }), ...yield* useOptionalInput({ ...options, ...props as TextFieldOptionalInputProps<A, R> }),
} }
: { : {
optional: false, optional: false,
// eslint-disable-next-line react-hooks/rules-of-hooks
...yield* useInput({ ...options, ...props as TextFieldInputProps<A, R> }), ...yield* useInput({ ...options, ...props as TextFieldInputProps<A, R> }),
} }

View File

@@ -1,9 +1,9 @@
import { Button, Container, Flex } from "@radix-ui/themes" import { Button, Container, Flex } from "@radix-ui/themes"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { Effect, ParseResult, Schema } from "effect" import { Console, Effect, Option, ParseResult, Schema } from "effect"
import { Component, Form, Hooks } from "effect-fc" import { Component, Form, Hooks } from "effect-fc"
import { useSubscribables } from "effect-fc/Hooks"
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput" import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
import { TextFieldFormOptionalInput } from "@/lib/form/TextFieldFormOptionalInput"
import { runtime } from "@/runtime" import { runtime } from "@/runtime"
@@ -20,22 +20,28 @@ const email = Schema.pattern<typeof Schema.String>(
const RegisterFormSchema = Schema.Struct({ const RegisterFormSchema = Schema.Struct({
email: Schema.String.pipe(email), email: Schema.String.pipe(email),
password: Schema.String.pipe(Schema.minLength(3)), password: Schema.String.pipe(Schema.minLength(3)),
iq: Schema.OptionFromSelf(Schema.NumberFromString),
}) })
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", { class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
scoped: Form.service({ scoped: Form.service({
schema: Schema.transformOrFail( schema: RegisterFormSchema.pipe(
Schema.encodedSchema(RegisterFormSchema), Schema.compose(
Schema.typeSchema(RegisterFormSchema), Schema.transformOrFail(
{ Schema.typeSchema(RegisterFormSchema),
decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)), Schema.typeSchema(RegisterFormSchema),
encode: ParseResult.succeed, {
}, decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
encode: ParseResult.succeed,
},
),
),
), ),
initialEncodedValue: { email: "", password: "" },
submit: () => Effect.andThen( initialEncodedValue: { email: "", password: "", iq: Option.none() },
Effect.sleep("500 millis"), submit: v => Effect.sleep("500 millis").pipe(
Effect.sync(() => alert("Done!")), Effect.andThen(Console.log(v)),
Effect.andThen(Effect.sync(() => alert("Done!"))),
), ),
}) })
}) {} }) {}
@@ -43,9 +49,10 @@ class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
class RegisterPage extends Component.makeUntraced("RegisterPage")(function*() { class RegisterPage extends Component.makeUntraced("RegisterPage")(function*() {
const form = yield* RegisterForm const form = yield* RegisterForm
const submit = yield* Form.useSubmit(form) const submit = yield* Form.useSubmit(form)
const [canSubmit] = yield* useSubscribables(form.canSubmitSubscribable) const [canSubmit] = yield* Hooks.useSubscribables(form.canSubmitSubscribable)
const TextFieldFormInputFC = yield* TextFieldFormInput const TextFieldFormInputFC = yield* TextFieldFormInput
const TextFieldFormOptionalInputFC = yield* TextFieldFormOptionalInput
return ( return (
<Container width="300"> <Container width="300">
@@ -64,6 +71,11 @@ class RegisterPage extends Component.makeUntraced("RegisterPage")(function*() {
debounce="200 millis" debounce="200 millis"
/> />
<TextFieldFormOptionalInputFC
field={Form.useField(form, ["iq"])}
defaultValue="100"
/>
<Button disabled={!canSubmit}>Submit</Button> <Button disabled={!canSubmit}>Submit</Button>
</Flex> </Flex>
</form> </form>