From a47ae73618116793b20f0d88c536c91f62a0ef87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Valverd=C3=A9?= Date: Mon, 20 Jul 2026 03:10:27 +0200 Subject: [PATCH] Add a working LensForm example --- packages/example-next/src/routeTree.gen.ts | 33 ++++- packages/example-next/src/routes/__root.tsx | 1 + packages/example-next/src/routes/lensform.tsx | 115 ++++++++++++++++++ 3 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 packages/example-next/src/routes/lensform.tsx diff --git a/packages/example-next/src/routeTree.gen.ts b/packages/example-next/src/routeTree.gen.ts index 5c09612..5904b93 100644 --- a/packages/example-next/src/routeTree.gen.ts +++ b/packages/example-next/src/routeTree.gen.ts @@ -13,6 +13,7 @@ import { Route as IndexRouteImport } from './routes/index' import { Route as AsyncRouteImport } from './routes/async' import { Route as BlankRouteImport } from './routes/blank' import { Route as FormRouteImport } from './routes/form' +import { Route as LensformRouteImport } from './routes/lensform' import { Route as QueryRouteImport } from './routes/query' import { Route as ResultRouteImport } from './routes/result' @@ -36,6 +37,11 @@ const FormRoute = FormRouteImport.update({ path: '/form', getParentRoute: () => rootRouteImport, } as any) +const LensformRoute = LensformRouteImport.update({ + id: '/lensform', + path: '/lensform', + getParentRoute: () => rootRouteImport, +} as any) const QueryRoute = QueryRouteImport.update({ id: '/query', path: '/query', @@ -52,6 +58,7 @@ export interface FileRoutesByFullPath { '/async': typeof AsyncRoute '/blank': typeof BlankRoute '/form': typeof FormRoute + '/lensform': typeof LensformRoute '/query': typeof QueryRoute '/result': typeof ResultRoute } @@ -60,6 +67,7 @@ export interface FileRoutesByTo { '/async': typeof AsyncRoute '/blank': typeof BlankRoute '/form': typeof FormRoute + '/lensform': typeof LensformRoute '/query': typeof QueryRoute '/result': typeof ResultRoute } @@ -69,15 +77,25 @@ export interface FileRoutesById { '/async': typeof AsyncRoute '/blank': typeof BlankRoute '/form': typeof FormRoute + '/lensform': typeof LensformRoute '/query': typeof QueryRoute '/result': typeof ResultRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/async' | '/blank' | '/form' | '/query' | '/result' + fullPaths: + '/' | '/async' | '/blank' | '/form' | '/lensform' | '/query' | '/result' fileRoutesByTo: FileRoutesByTo - to: '/' | '/async' | '/blank' | '/form' | '/query' | '/result' - id: '__root__' | '/' | '/async' | '/blank' | '/form' | '/query' | '/result' + to: '/' | '/async' | '/blank' | '/form' | '/lensform' | '/query' | '/result' + id: + | '__root__' + | '/' + | '/async' + | '/blank' + | '/form' + | '/lensform' + | '/query' + | '/result' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -85,6 +103,7 @@ export interface RootRouteChildren { AsyncRoute: typeof AsyncRoute BlankRoute: typeof BlankRoute FormRoute: typeof FormRoute + LensformRoute: typeof LensformRoute QueryRoute: typeof QueryRoute ResultRoute: typeof ResultRoute } @@ -119,6 +138,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof FormRouteImport parentRoute: typeof rootRouteImport } + '/lensform': { + id: '/lensform' + path: '/lensform' + fullPath: '/lensform' + preLoaderRoute: typeof LensformRouteImport + parentRoute: typeof rootRouteImport + } '/query': { id: '/query' path: '/query' @@ -141,6 +167,7 @@ const rootRouteChildren: RootRouteChildren = { AsyncRoute: AsyncRoute, BlankRoute: BlankRoute, FormRoute: FormRoute, + LensformRoute: LensformRoute, QueryRoute: QueryRoute, ResultRoute: ResultRoute, } diff --git a/packages/example-next/src/routes/__root.tsx b/packages/example-next/src/routes/__root.tsx index c9826b6..783b032 100644 --- a/packages/example-next/src/routes/__root.tsx +++ b/packages/example-next/src/routes/__root.tsx @@ -21,6 +21,7 @@ function Root() { Query Result Form + LensForm diff --git a/packages/example-next/src/routes/lensform.tsx b/packages/example-next/src/routes/lensform.tsx new file mode 100644 index 0000000..0de701f --- /dev/null +++ b/packages/example-next/src/routes/lensform.tsx @@ -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") { + static readonly layer = Layer.effect( + AppState, + Effect.gen(function*() { + return { + lens: Lens.fromSubscriptionRef( + yield* SubscriptionRef.make({ + 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 +}) + + +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 ( + +
{ + event.preventDefault() + }}> + + + + +
+
+ ) +}) + + +export const Route = createFileRoute("/lensform")({ + component: Component.withContext(LensFormPageView, runtime.context), +})