0.1.4 #5
@@ -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> {
|
||||||
|
|||||||
@@ -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,9 +8,10 @@ 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")(
|
||||||
|
function*(props: TextFieldFormInputProps) {
|
||||||
const { value, setValue } = yield* Form.useInput(props.field, props)
|
const { value, setValue } = yield* Form.useInput(props.field, props)
|
||||||
const [issues, isValidating, isSubmitting] = yield* useSubscribables(
|
const [issues, isValidating, isSubmitting] = yield* Hooks.useSubscribables(
|
||||||
props.field.issuesSubscribable,
|
props.field.issuesSubscribable,
|
||||||
props.field.isValidatingSubscribable,
|
props.field.isValidatingSubscribable,
|
||||||
props.field.isSubmittingSubscribable,
|
props.field.isSubmittingSubscribable,
|
||||||
@@ -30,6 +30,8 @@ export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInp
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
</TextField.Slot>
|
</TextField.Slot>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{props.children}
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
|
|
||||||
{Option.match(Array.head(issues), {
|
{Option.match(Array.head(issues), {
|
||||||
@@ -43,4 +45,5 @@ export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInp
|
|||||||
})}
|
})}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}) {}
|
}
|
||||||
|
) {}
|
||||||
|
|||||||
57
packages/example/src/lib/form/TextFieldFormOptionalInput.tsx
Normal file
57
packages/example/src/lib/form/TextFieldFormOptionalInput.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {}
|
||||||
@@ -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> }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.transformOrFail(
|
||||||
|
Schema.typeSchema(RegisterFormSchema),
|
||||||
Schema.typeSchema(RegisterFormSchema),
|
Schema.typeSchema(RegisterFormSchema),
|
||||||
{
|
{
|
||||||
decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
|
decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
|
||||||
encode: ParseResult.succeed,
|
encode: ParseResult.succeed,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
initialEncodedValue: { email: "", password: "" },
|
),
|
||||||
submit: () => Effect.andThen(
|
),
|
||||||
Effect.sleep("500 millis"),
|
|
||||||
Effect.sync(() => alert("Done!")),
|
initialEncodedValue: { email: "", password: "", iq: Option.none() },
|
||||||
|
submit: v => Effect.sleep("500 millis").pipe(
|
||||||
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user