0.1.4 (#5)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
8
packages/example/biome.json
Normal file
8
packages/example/biome.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
||||
"root": false,
|
||||
"extends": "//",
|
||||
"files": {
|
||||
"includes": ["./src/**", "!src/routeTree.gen.ts"]
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -5,43 +5,40 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint:tsc": "tsc --noEmit",
|
||||
"lint:eslint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"lint:biome": "biome lint",
|
||||
"preview": "vite preview",
|
||||
"clean:cache": "rm -rf .turbo node_modules/.tmp node_modules/.vite* .tanstack",
|
||||
"clean:dist": "rm -rf dist",
|
||||
"clean:modules": "rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/language-service": "^0.35.2",
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@tanstack/react-router": "^1.131.27",
|
||||
"@tanstack/react-router-devtools": "^1.131.27",
|
||||
"@tanstack/router-plugin": "^1.131.27",
|
||||
"@types/react": "^19.1.11",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react": "^5.0.1",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"vite": "^7.1.3"
|
||||
"@tanstack/react-router": "^1.132.31",
|
||||
"@tanstack/react-router-devtools": "^1.132.31",
|
||||
"@tanstack/router-plugin": "^1.132.31",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"globals": "^16.4.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"type-fest": "^5.0.1",
|
||||
"vite": "^7.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@effect/platform": "^0.90.6",
|
||||
"@effect/platform-browser": "^0.70.0",
|
||||
"@effect/platform": "^0.92.1",
|
||||
"@effect/platform-browser": "^0.72.0",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@typed/async-data": "^0.13.1",
|
||||
"@typed/id": "^0.17.2",
|
||||
"@typed/lazy-ref": "^0.3.3",
|
||||
"effect": "^3.17.9",
|
||||
"effect": "^3.18.1",
|
||||
"effect-fc": "workspace:*",
|
||||
"react-icons": "^5.5.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "^19.1.11",
|
||||
"effect": "^3.17.9",
|
||||
"react": "^19.1.1"
|
||||
"@types/react": "^19.2.0",
|
||||
"effect": "^3.18.1",
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
77
packages/example/src/lib/form/TextFieldFormInput.tsx
Normal file
77
packages/example/src/lib/form/TextFieldFormInput.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Callout, Flex, Spinner, Switch, TextField } from "@radix-ui/themes"
|
||||
import { Array, Option } from "effect"
|
||||
import { Component, Form, Hooks } from "effect-fc"
|
||||
|
||||
|
||||
interface Props
|
||||
extends TextField.RootProps, Form.useInput.Options {
|
||||
readonly optional?: false
|
||||
readonly field: Form.FormField<any, string>
|
||||
}
|
||||
|
||||
interface OptionalProps
|
||||
extends Omit<TextField.RootProps, "defaultValue">, Form.useOptionalInput.Options<string> {
|
||||
readonly optional: true
|
||||
readonly field: Form.FormField<any, Option.Option<string>>
|
||||
}
|
||||
|
||||
export type TextFieldFormInputProps = Props | OptionalProps
|
||||
|
||||
|
||||
export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInput")(
|
||||
function*(props: TextFieldFormInputProps) {
|
||||
const input: (
|
||||
| { readonly optional: true } & Form.useOptionalInput.Result<string>
|
||||
| { readonly optional: false } & Form.useInput.Result<string>
|
||||
) = props.optional
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported
|
||||
? { optional: true, ...yield* Form.useOptionalInput(props.field, props) }
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: "optional" reactivity not supported
|
||||
: { optional: false, ...yield* Form.useInput(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={input.value}
|
||||
onChange={e => input.setValue(e.target.value)}
|
||||
disabled={(input.optional && !input.enabled) || isSubmitting}
|
||||
{...props}
|
||||
>
|
||||
{input.optional &&
|
||||
<TextField.Slot side="left">
|
||||
<Switch
|
||||
size="1"
|
||||
checked={input.enabled}
|
||||
onCheckedChange={input.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>
|
||||
)
|
||||
}
|
||||
) {}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Callout, Flex, TextArea, TextAreaProps } from "@radix-ui/themes"
|
||||
import { Array, Equivalence, Option, ParseResult, Schema, Struct } from "effect"
|
||||
/** biome-ignore-all lint/correctness/useHookAtTopLevel: effect-fc HOC */
|
||||
import { Callout, Flex, TextArea, type TextAreaProps } from "@radix-ui/themes"
|
||||
import { Array, type Equivalence, Option, ParseResult, type Schema, Struct } from "effect"
|
||||
import { Component } from "effect-fc"
|
||||
import { useInput } from "effect-fc/hooks"
|
||||
import { useInput } from "effect-fc/Hooks"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
@@ -15,7 +16,7 @@ export const TextAreaInput = <A, R>(options: {
|
||||
React.JSX.Element,
|
||||
ParseResult.ParseError,
|
||||
R
|
||||
> => Component.makeUntraced(function* TextFieldInput(props) {
|
||||
> => Component.makeUntraced("TextFieldInput")(function*(props) {
|
||||
const input = yield* useInput({ ...options, ...props })
|
||||
const issue = React.useMemo(() => input.error.pipe(
|
||||
Option.map(ParseResult.ArrayFormatter.formatErrorSync),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/** biome-ignore-all lint/correctness/useHookAtTopLevel: effect-fc HOC */
|
||||
import { Callout, Checkbox, Flex, TextField } from "@radix-ui/themes"
|
||||
import { Array, Equivalence, Option, ParseResult, Schema, Struct } from "effect"
|
||||
import { Component, Memo } from "effect-fc"
|
||||
import { useInput, useOptionalInput } from "effect-fc/hooks"
|
||||
import { Array, type Equivalence, Option, ParseResult, type Schema, Struct } from "effect"
|
||||
import { Component } from "effect-fc"
|
||||
import { useInput, useOptionalInput } from "effect-fc/Hooks"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
@@ -18,7 +19,7 @@ export const TextFieldInput = <A, R, O extends boolean = false>(options: {
|
||||
readonly optional?: O
|
||||
readonly schema: Schema.Schema<A, string, R>
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
}) => Component.makeUntraced(function* TextFieldInput(props: O extends true
|
||||
}) => Component.makeUntraced("TextFieldInput")(function*(props: O extends true
|
||||
? TextFieldOptionalInputProps<A, R>
|
||||
: TextFieldInputProps<A, R>
|
||||
) {
|
||||
@@ -28,12 +29,10 @@ export const TextFieldInput = <A, R, O extends boolean = false>(options: {
|
||||
) = options.optional
|
||||
? {
|
||||
optional: true,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
...yield* useOptionalInput({ ...options, ...props as TextFieldOptionalInputProps<A, R> }),
|
||||
}
|
||||
: {
|
||||
optional: false,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
...yield* useInput({ ...options, ...props as TextFieldInputProps<A, R> }),
|
||||
}
|
||||
|
||||
@@ -67,4 +66,4 @@ export const TextFieldInput = <A, R, O extends boolean = false>(options: {
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
}).pipe(Memo.memo)
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ declare module "@tanstack/react-router" {
|
||||
}
|
||||
}
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: React entrypoint
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ReactRuntime.Provider runtime={runtime}>
|
||||
|
||||
@@ -9,12 +9,18 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as FormRouteImport } from './routes/form'
|
||||
import { Route as BlankRouteImport } from './routes/blank'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DevMemoRouteImport } from './routes/dev/memo'
|
||||
import { Route as DevInputRouteImport } from './routes/dev/input'
|
||||
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
|
||||
|
||||
const FormRoute = FormRouteImport.update({
|
||||
id: '/form',
|
||||
path: '/form',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const BlankRoute = BlankRouteImport.update({
|
||||
id: '/blank',
|
||||
path: '/blank',
|
||||
@@ -44,6 +50,7 @@ const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/input': typeof DevInputRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
@@ -51,6 +58,7 @@ export interface FileRoutesByFullPath {
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/input': typeof DevInputRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
@@ -59,6 +67,7 @@ export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/input': typeof DevInputRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
@@ -68,15 +77,23 @@ export interface FileRouteTypes {
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/input'
|
||||
| '/dev/memo'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/blank' | '/dev/async-rendering' | '/dev/input' | '/dev/memo'
|
||||
to:
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/input'
|
||||
| '/dev/memo'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/input'
|
||||
| '/dev/memo'
|
||||
@@ -85,6 +102,7 @@ export interface FileRouteTypes {
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
BlankRoute: typeof BlankRoute
|
||||
FormRoute: typeof FormRoute
|
||||
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
|
||||
DevInputRoute: typeof DevInputRoute
|
||||
DevMemoRoute: typeof DevMemoRoute
|
||||
@@ -92,6 +110,13 @@ export interface RootRouteChildren {
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/form': {
|
||||
id: '/form'
|
||||
path: '/form'
|
||||
fullPath: '/form'
|
||||
preLoaderRoute: typeof FormRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/blank': {
|
||||
id: '/blank'
|
||||
path: '/blank'
|
||||
@@ -133,6 +158,7 @@ declare module '@tanstack/react-router' {
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
BlankRoute: BlankRoute,
|
||||
FormRoute: FormRoute,
|
||||
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
|
||||
DevInputRoute: DevInputRoute,
|
||||
DevMemoRoute: DevMemoRoute,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { runtime } from "@/runtime"
|
||||
import { Flex, Text, TextField } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Effect } from "effect"
|
||||
import { Component, Memo, Suspense } from "effect-fc"
|
||||
import { Hooks } from "effect-fc/hooks"
|
||||
import { Async, Component, Hooks, Memoized } from "effect-fc"
|
||||
import * as React from "react"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
// Generator version
|
||||
@@ -51,7 +50,7 @@ const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
||||
// )
|
||||
|
||||
|
||||
class AsyncComponent extends Component.makeUntraced(function* AsyncComponent() {
|
||||
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
|
||||
const SubComponentFC = yield* SubComponent
|
||||
|
||||
yield* Effect.sleep("500 millis") // Async operation
|
||||
@@ -64,12 +63,12 @@ class AsyncComponent extends Component.makeUntraced(function* AsyncComponent() {
|
||||
</Flex>
|
||||
)
|
||||
}).pipe(
|
||||
Suspense.suspense,
|
||||
Suspense.withOptions({ defaultFallback: <p>Loading...</p> }),
|
||||
Async.async,
|
||||
Async.withOptions({ defaultFallback: <p>Loading...</p> }),
|
||||
) {}
|
||||
class MemoizedAsyncComponent extends Memo.memo(AsyncComponent) {}
|
||||
class MemoizedAsyncComponent extends Memoized.memoized(AsyncComponent) {}
|
||||
|
||||
class SubComponent extends Component.makeUntraced(function* SubComponent() {
|
||||
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
||||
const [state] = React.useState(yield* Hooks.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
||||
return <Text>{state}</Text>
|
||||
}) {}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { TextFieldInput } from "@/lib/input/TextFieldInput"
|
||||
import { runtime } from "@/runtime"
|
||||
import { Container } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Schema, SubscriptionRef } from "effect"
|
||||
import { Component, Memo } from "effect-fc"
|
||||
import { useInput, useOnce, useRefState } from "effect-fc/hooks"
|
||||
import { Component, Hooks, Memoized } from "effect-fc"
|
||||
import { TextFieldInput } from "@/lib/input/TextFieldInput"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
const IntFromString = Schema.NumberFromString.pipe(Schema.int())
|
||||
@@ -12,18 +11,18 @@ const IntFromString = Schema.NumberFromString.pipe(Schema.int())
|
||||
const IntTextFieldInput = TextFieldInput({ schema: IntFromString })
|
||||
const StringTextFieldInput = TextFieldInput({ schema: Schema.String })
|
||||
|
||||
const Input = Component.makeUntraced(function* Input() {
|
||||
const Input = Component.makeUntraced("Input")(function*() {
|
||||
const IntTextFieldInputFC = yield* IntTextFieldInput
|
||||
const StringTextFieldInputFC = yield* StringTextFieldInput
|
||||
|
||||
const intRef1 = yield* useOnce(() => SubscriptionRef.make(0))
|
||||
const intRef2 = yield* useOnce(() => SubscriptionRef.make(0))
|
||||
const stringRef = yield* useOnce(() => SubscriptionRef.make(""))
|
||||
const intRef1 = yield* Hooks.useOnce(() => SubscriptionRef.make(0))
|
||||
// const intRef2 = yield* useOnce(() => SubscriptionRef.make(0))
|
||||
const stringRef = yield* Hooks.useOnce(() => SubscriptionRef.make(""))
|
||||
// yield* useFork(() => Stream.runForEach(intRef1.changes, Console.log), [intRef1])
|
||||
|
||||
const input2 = yield* useInput({ schema: IntFromString, ref: intRef2 })
|
||||
// const input2 = yield* useInput({ schema: IntFromString, ref: intRef2 })
|
||||
|
||||
const [str, setStr] = yield* useRefState(stringRef)
|
||||
// const [str, setStr] = yield* useRefState(stringRef)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -33,7 +32,7 @@ const Input = Component.makeUntraced(function* Input() {
|
||||
</Container>
|
||||
)
|
||||
}).pipe(
|
||||
Memo.memo,
|
||||
Memoized.memoized,
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { runtime } from "@/runtime"
|
||||
import { Flex, Text, TextField } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Effect } from "effect"
|
||||
import { Component, Memo } from "effect-fc"
|
||||
import { Component, Memoized } from "effect-fc"
|
||||
import * as React from "react"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
const RouteComponent = Component.makeUntraced(function* RouteComponent() {
|
||||
const RouteComponent = Component.makeUntraced("RouteComponent")(function*() {
|
||||
const [value, setValue] = React.useState("")
|
||||
|
||||
return (
|
||||
@@ -25,12 +25,12 @@ const RouteComponent = Component.makeUntraced(function* RouteComponent() {
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
class SubComponent extends Component.makeUntraced(function* SubComponent() {
|
||||
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
||||
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
|
||||
return <Text>{id}</Text>
|
||||
}) {}
|
||||
|
||||
class MemoizedSubComponent extends Memo.memo(SubComponent) {}
|
||||
class MemoizedSubComponent extends Memoized.memoized(SubComponent) {}
|
||||
|
||||
export const Route = createFileRoute("/dev/memo")({
|
||||
component: RouteComponent,
|
||||
|
||||
99
packages/example/src/routes/form.tsx
Normal file
99
packages/example/src/routes/form.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Button, Container, Flex } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Option, ParseResult, Schema } from "effect"
|
||||
import { Component, Form, Hooks } from "effect-fc"
|
||||
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
const email = Schema.pattern<typeof Schema.String>(
|
||||
/^(?!\.)(?!.*\.\.)([A-Z0-9_+-.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i,
|
||||
|
||||
{
|
||||
identifier: "email",
|
||||
title: "email",
|
||||
message: () => "Not an email address",
|
||||
},
|
||||
)
|
||||
|
||||
const RegisterFormSchema = Schema.Struct({
|
||||
email: Schema.String.pipe(email),
|
||||
password: Schema.String.pipe(Schema.minLength(3)),
|
||||
birth: Schema.OptionFromSelf(DateTimeUtcFromZonedInput),
|
||||
})
|
||||
|
||||
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||
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,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
initialEncodedValue: { email: "", password: "", birth: Option.none() },
|
||||
submit: v => Effect.sleep("500 millis").pipe(
|
||||
Effect.andThen(Console.log(v)),
|
||||
Effect.andThen(Effect.sync(() => alert("Done!"))),
|
||||
),
|
||||
debounce: "500 millis",
|
||||
})
|
||||
}) {}
|
||||
|
||||
class RegisterPage extends Component.makeUntraced("RegisterPage")(function*() {
|
||||
const form = yield* RegisterForm
|
||||
const submit = yield* Form.useSubmit(form)
|
||||
const [canSubmit] = yield* Hooks.useSubscribables(form.canSubmitSubscribable)
|
||||
|
||||
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||
|
||||
|
||||
return (
|
||||
<Container width="300">
|
||||
<form onSubmit={e => {
|
||||
e.preventDefault()
|
||||
void submit()
|
||||
}}>
|
||||
<Flex direction="column" gap="2">
|
||||
<TextFieldFormInputFC
|
||||
field={Form.useField(form, ["email"])}
|
||||
/>
|
||||
|
||||
<TextFieldFormInputFC
|
||||
field={Form.useField(form, ["password"])}
|
||||
/>
|
||||
|
||||
<TextFieldFormInputFC
|
||||
optional
|
||||
type="datetime-local"
|
||||
field={Form.useField(form, ["birth"])}
|
||||
defaultValue=""
|
||||
/>
|
||||
|
||||
<Button disabled={!canSubmit}>Submit</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
}) {}
|
||||
|
||||
|
||||
export const Route = createFileRoute("/form")({
|
||||
component: Component.makeUntraced("RegisterRoute")(function*() {
|
||||
const RegisterRouteFC = yield* Effect.provide(
|
||||
RegisterPage,
|
||||
yield* Hooks.useContext(RegisterForm.Default, { finalizerExecutionMode: "fork" }),
|
||||
)
|
||||
|
||||
return <RegisterRouteFC />
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
})
|
||||
@@ -1,17 +1,18 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Effect } from "effect"
|
||||
import { Component, Hooks } from "effect-fc"
|
||||
import { runtime } from "@/runtime"
|
||||
import { Todos } from "@/todo/Todos"
|
||||
import { TodosState } from "@/todo/TodosState.service"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Effect } from "effect"
|
||||
import { Component } from "effect-fc"
|
||||
import { useContext } from "effect-fc/hooks"
|
||||
|
||||
|
||||
const TodosStateLive = TodosState.Default("todos")
|
||||
|
||||
const Index = Component.makeUntraced(function* Index() {
|
||||
const context = yield* useContext(TodosStateLive, { finalizerExecutionMode: "fork" })
|
||||
const TodosFC = yield* Effect.provide(Todos, context)
|
||||
const Index = Component.makeUntraced("Index")(function*() {
|
||||
const TodosFC = yield* Effect.provide(
|
||||
Todos,
|
||||
yield* Hooks.useContext(TodosStateLive, { finalizerExecutionMode: "fork" }),
|
||||
)
|
||||
|
||||
return <TodosFC />
|
||||
}).pipe(
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, DateTime, Effect, Match, Option, Ref, Runtime, Schema, Stream, SubscriptionRef } from "effect"
|
||||
import { Component, Hooks, Memoized, Subscribable, SubscriptionSubRef } from "effect-fc"
|
||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||
import { FaDeleteLeft } from "react-icons/fa6"
|
||||
import * as Domain from "@/domain"
|
||||
import { TextAreaInput } from "@/lib/input/TextAreaInput"
|
||||
import { TextFieldInput } from "@/lib/input/TextFieldInput"
|
||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, DateTime, Effect, Match, Option, Ref, Runtime, Schema, Stream, SubscriptionRef } from "effect"
|
||||
import { Component, Memo } from "effect-fc"
|
||||
import { useMemo, useOnce, useSubscribe } from "effect-fc/hooks"
|
||||
import { Subscribable, SubscriptionSubRef } from "effect-fc/types"
|
||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||
import { FaDeleteLeft } from "react-icons/fa6"
|
||||
import { TodosState } from "./TodosState.service"
|
||||
|
||||
|
||||
@@ -31,11 +29,11 @@ export type TodoProps = (
|
||||
| { readonly _tag: "edit", readonly id: string }
|
||||
)
|
||||
|
||||
export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps) {
|
||||
export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoProps) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
const state = yield* TodosState
|
||||
|
||||
const { ref, indexRef, contentRef, completedAtRef } = yield* useMemo(() => Match.value(props).pipe(
|
||||
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 })),
|
||||
@@ -48,10 +46,9 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps
|
||||
|
||||
Effect.let("contentRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["content"])),
|
||||
Effect.let("completedAtRef", ({ ref }) => SubscriptionSubRef.makeFromPath(ref, ["completedAt"])),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
), [props._tag, props._tag === "edit" ? props.id : undefined])
|
||||
|
||||
const [index, size] = yield* useSubscribe(indexRef, state.sizeSubscribable)
|
||||
const [index, size] = yield* Hooks.useSubscribables(indexRef, state.sizeSubscribable)
|
||||
|
||||
const StringTextAreaInputFC = yield* StringTextAreaInput
|
||||
const OptionalDateTimeInputFC = yield* OptionalDateTimeInput
|
||||
@@ -67,7 +64,7 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps
|
||||
<OptionalDateTimeInputFC
|
||||
type="datetime-local"
|
||||
ref={completedAtRef}
|
||||
defaultValue={yield* useOnce(() => DateTime.now)}
|
||||
defaultValue={yield* Hooks.useOnce(() => DateTime.now)}
|
||||
/>
|
||||
|
||||
{props._tag === "new" &&
|
||||
@@ -109,4 +106,6 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
}).pipe(Memo.memo) {}
|
||||
}).pipe(
|
||||
Memoized.memoized
|
||||
) {}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||
import { Chunk, Console, Effect } from "effect"
|
||||
import { Component } from "effect-fc"
|
||||
import { useOnce, useSubscribe } from "effect-fc/hooks"
|
||||
import { Component, Hooks } from "effect-fc"
|
||||
import { Todo } from "./Todo"
|
||||
import { TodosState } from "./TodosState.service"
|
||||
|
||||
|
||||
export class Todos extends Component.makeUntraced(function* Todos() {
|
||||
export class Todos extends Component.makeUntraced("Todos")(function*() {
|
||||
const state = yield* TodosState
|
||||
const [todos] = yield* useSubscribe(state.ref)
|
||||
const [todos] = yield* Hooks.useSubscribables(state.ref)
|
||||
|
||||
yield* useOnce(() => Effect.andThen(
|
||||
yield* Hooks.useOnce(() => Effect.andThen(
|
||||
Console.log("Todos mounted"),
|
||||
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||
))
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Todo } from "@/domain"
|
||||
import { KeyValueStore } from "@effect/platform"
|
||||
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||
import { Chunk, Console, Effect, Option, Schema, Stream, SubscriptionRef } from "effect"
|
||||
import { Subscribable, SubscriptionSubRef } from "effect-fc/types"
|
||||
import { Subscribable, SubscriptionSubRef } from "effect-fc"
|
||||
import { Todo } from "@/domain"
|
||||
|
||||
|
||||
export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||
|
||||
@@ -22,15 +22,15 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
"plugins": [
|
||||
{
|
||||
"name": "@effect/language-service"
|
||||
}
|
||||
{ "name": "@effect/language-service" }
|
||||
]
|
||||
},
|
||||
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user