Add Effect v4 support, Fast Refresh tooling, and revamped docs #56
@@ -13,6 +13,7 @@ import { Route as IndexRouteImport } from './routes/index'
|
|||||||
import { Route as AsyncRouteImport } from './routes/async'
|
import { Route as AsyncRouteImport } from './routes/async'
|
||||||
import { Route as BlankRouteImport } from './routes/blank'
|
import { Route as BlankRouteImport } from './routes/blank'
|
||||||
import { Route as FormRouteImport } from './routes/form'
|
import { Route as FormRouteImport } from './routes/form'
|
||||||
|
import { Route as LensformRouteImport } from './routes/lensform'
|
||||||
import { Route as QueryRouteImport } from './routes/query'
|
import { Route as QueryRouteImport } from './routes/query'
|
||||||
import { Route as ResultRouteImport } from './routes/result'
|
import { Route as ResultRouteImport } from './routes/result'
|
||||||
|
|
||||||
@@ -36,6 +37,11 @@ const FormRoute = FormRouteImport.update({
|
|||||||
path: '/form',
|
path: '/form',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const LensformRoute = LensformRouteImport.update({
|
||||||
|
id: '/lensform',
|
||||||
|
path: '/lensform',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const QueryRoute = QueryRouteImport.update({
|
const QueryRoute = QueryRouteImport.update({
|
||||||
id: '/query',
|
id: '/query',
|
||||||
path: '/query',
|
path: '/query',
|
||||||
@@ -52,6 +58,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/async': typeof AsyncRoute
|
'/async': typeof AsyncRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/form': typeof FormRoute
|
'/form': typeof FormRoute
|
||||||
|
'/lensform': typeof LensformRoute
|
||||||
'/query': typeof QueryRoute
|
'/query': typeof QueryRoute
|
||||||
'/result': typeof ResultRoute
|
'/result': typeof ResultRoute
|
||||||
}
|
}
|
||||||
@@ -60,6 +67,7 @@ export interface FileRoutesByTo {
|
|||||||
'/async': typeof AsyncRoute
|
'/async': typeof AsyncRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/form': typeof FormRoute
|
'/form': typeof FormRoute
|
||||||
|
'/lensform': typeof LensformRoute
|
||||||
'/query': typeof QueryRoute
|
'/query': typeof QueryRoute
|
||||||
'/result': typeof ResultRoute
|
'/result': typeof ResultRoute
|
||||||
}
|
}
|
||||||
@@ -69,15 +77,25 @@ export interface FileRoutesById {
|
|||||||
'/async': typeof AsyncRoute
|
'/async': typeof AsyncRoute
|
||||||
'/blank': typeof BlankRoute
|
'/blank': typeof BlankRoute
|
||||||
'/form': typeof FormRoute
|
'/form': typeof FormRoute
|
||||||
|
'/lensform': typeof LensformRoute
|
||||||
'/query': typeof QueryRoute
|
'/query': typeof QueryRoute
|
||||||
'/result': typeof ResultRoute
|
'/result': typeof ResultRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/async' | '/blank' | '/form' | '/query' | '/result'
|
fullPaths:
|
||||||
|
'/' | '/async' | '/blank' | '/form' | '/lensform' | '/query' | '/result'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/async' | '/blank' | '/form' | '/query' | '/result'
|
to: '/' | '/async' | '/blank' | '/form' | '/lensform' | '/query' | '/result'
|
||||||
id: '__root__' | '/' | '/async' | '/blank' | '/form' | '/query' | '/result'
|
id:
|
||||||
|
| '__root__'
|
||||||
|
| '/'
|
||||||
|
| '/async'
|
||||||
|
| '/blank'
|
||||||
|
| '/form'
|
||||||
|
| '/lensform'
|
||||||
|
| '/query'
|
||||||
|
| '/result'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -85,6 +103,7 @@ export interface RootRouteChildren {
|
|||||||
AsyncRoute: typeof AsyncRoute
|
AsyncRoute: typeof AsyncRoute
|
||||||
BlankRoute: typeof BlankRoute
|
BlankRoute: typeof BlankRoute
|
||||||
FormRoute: typeof FormRoute
|
FormRoute: typeof FormRoute
|
||||||
|
LensformRoute: typeof LensformRoute
|
||||||
QueryRoute: typeof QueryRoute
|
QueryRoute: typeof QueryRoute
|
||||||
ResultRoute: typeof ResultRoute
|
ResultRoute: typeof ResultRoute
|
||||||
}
|
}
|
||||||
@@ -119,6 +138,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof FormRouteImport
|
preLoaderRoute: typeof FormRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/lensform': {
|
||||||
|
id: '/lensform'
|
||||||
|
path: '/lensform'
|
||||||
|
fullPath: '/lensform'
|
||||||
|
preLoaderRoute: typeof LensformRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/query': {
|
'/query': {
|
||||||
id: '/query'
|
id: '/query'
|
||||||
path: '/query'
|
path: '/query'
|
||||||
@@ -141,6 +167,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
AsyncRoute: AsyncRoute,
|
AsyncRoute: AsyncRoute,
|
||||||
BlankRoute: BlankRoute,
|
BlankRoute: BlankRoute,
|
||||||
FormRoute: FormRoute,
|
FormRoute: FormRoute,
|
||||||
|
LensformRoute: LensformRoute,
|
||||||
QueryRoute: QueryRoute,
|
QueryRoute: QueryRoute,
|
||||||
ResultRoute: ResultRoute,
|
ResultRoute: ResultRoute,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function Root() {
|
|||||||
<Link to="/query">Query</Link>
|
<Link to="/query">Query</Link>
|
||||||
<Link to="/result">Result</Link>
|
<Link to="/result">Result</Link>
|
||||||
<Link to="/form">Form</Link>
|
<Link to="/form">Form</Link>
|
||||||
|
<Link to="/lensform">LensForm</Link>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
||||||
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
|
import { Console, Context, Effect, Layer, Schema, Stream, SubscriptionRef } from "effect"
|
||||||
|
import { Component, Form, Lens, LensForm, MutationForm, View } from "effect-fc-next"
|
||||||
|
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
||||||
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
|
const UserProfileSchema = Schema.Struct({
|
||||||
|
email: Schema.String.check(
|
||||||
|
Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, {
|
||||||
|
message: "Enter a valid email address",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
password: Schema.String.check(
|
||||||
|
Schema.isMinLength(5, {
|
||||||
|
message: "Password must be at least 5 characters long",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const AppStateSchema = Schema.Struct({
|
||||||
|
currentUser: UserProfileSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
class AppState extends Context.Service<AppState, {
|
||||||
|
readonly lens: Lens.Lens<typeof AppStateSchema.Type>
|
||||||
|
}>()("AppState") {
|
||||||
|
static readonly layer = Layer.effect(
|
||||||
|
AppState,
|
||||||
|
Effect.gen(function*() {
|
||||||
|
return {
|
||||||
|
lens: Lens.fromSubscriptionRef(
|
||||||
|
yield* SubscriptionRef.make<typeof AppStateSchema.Type>({
|
||||||
|
currentUser: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const LensFormPageView = Component.make("LensFormPageView")(function*() {
|
||||||
|
yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
|
yield* Effect.addFinalizer(() => Console.log("LensForm route unmounted"))
|
||||||
|
yield* Console.log("LensForm route mounted")
|
||||||
|
}))
|
||||||
|
|
||||||
|
const context = yield* Component.useContextFromLayer(AppState.layer)
|
||||||
|
const UserProfileEditor = yield* Effect.provide(UserProfileEditorView.use, context)
|
||||||
|
|
||||||
|
return <UserProfileEditor />
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const UserProfileEditorView = Component.make("UserProfileEditorView")(function*() {
|
||||||
|
const appState = yield* AppState
|
||||||
|
|
||||||
|
const [
|
||||||
|
emailField,
|
||||||
|
passwordField,
|
||||||
|
] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
|
yield* Effect.forkScoped(
|
||||||
|
Stream.runForEach(Lens.changes(appState.lens), Console.log)
|
||||||
|
)
|
||||||
|
|
||||||
|
const form = yield* LensForm.service({
|
||||||
|
schema: UserProfileSchema,
|
||||||
|
target: Lens.focusObjectOn(appState.lens, "currentUser"),
|
||||||
|
initialEncodedValue: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emailField = Form.focusObjectOn(form, "email")
|
||||||
|
const passwordField = Form.focusObjectOn(form, "password")
|
||||||
|
|
||||||
|
return [emailField, passwordField] as const
|
||||||
|
}))
|
||||||
|
|
||||||
|
const TextFieldFormInput = yield* TextFieldFormInputView.use
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container width="300">
|
||||||
|
<form onSubmit={event => {
|
||||||
|
event.preventDefault()
|
||||||
|
}}>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<TextFieldFormInput
|
||||||
|
form={emailField}
|
||||||
|
placeholder="Email"
|
||||||
|
debounce="250 millis"
|
||||||
|
/>
|
||||||
|
<TextFieldFormInput
|
||||||
|
form={passwordField}
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
debounce="250 millis"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</form>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/lensform")({
|
||||||
|
component: Component.withContext(LensFormPageView, runtime.context),
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user