This commit is contained in:
@@ -13,7 +13,7 @@ export class TextFieldFormInputView extends Component.make("TextFieldFormInputVi
|
||||
props: TextFieldFormInputView.Props
|
||||
) {
|
||||
const input = yield* Form.useInput(props.form, props)
|
||||
const [issues, isValidating, isCommitting] = yield* Subscribable.useSubscribables([
|
||||
const [issues, isValidating, isCommitting] = yield* Subscribable.useAll([
|
||||
props.form.issues,
|
||||
props.form.isValidating,
|
||||
props.form.isCommitting,
|
||||
|
||||
@@ -13,7 +13,7 @@ export class TextFieldOptionalFormInputView extends Component.make("TextFieldOpt
|
||||
props: TextFieldOptionalFormInputView.Props
|
||||
) {
|
||||
const input = yield* Form.useOptionalInput(props.form, props)
|
||||
const [issues, isValidating, isCommitting] = yield* Subscribable.useSubscribables([
|
||||
const [issues, isValidating, isCommitting] = yield* Subscribable.useAll([
|
||||
props.form.issues,
|
||||
props.form.isValidating,
|
||||
props.form.isCommitting,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
||||
import { Component, Form, SubmittableForm, Subscribable } from "effect-fc"
|
||||
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
||||
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||
import { runtime } from "@/runtime"
|
||||
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
||||
import { Component, Form, Subscribable } from "effect-fc"
|
||||
|
||||
|
||||
const email = Schema.pattern<typeof Schema.String>(
|
||||
@@ -41,7 +41,7 @@ const RegisterFormSubmitSchema = Schema.Struct({
|
||||
|
||||
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
|
||||
scoped: Effect.gen(function*() {
|
||||
const form = yield* Form.service({
|
||||
const form = yield* SubmittableForm.service({
|
||||
schema: RegisterFormSchema.pipe(
|
||||
Schema.compose(
|
||||
Schema.transformOrFail(
|
||||
@@ -73,7 +73,7 @@ class RegisterFormService extends Effect.Service<RegisterFormService>()("Registe
|
||||
|
||||
class RegisterFormView extends Component.make("RegisterFormView")(function*() {
|
||||
const form = yield* RegisterFormService
|
||||
const [canCommit, submitResult] = yield* Subscribable.useSubscribables([
|
||||
const [canCommit, submitResult] = yield* Subscribable.useAll([
|
||||
form.form.canCommit,
|
||||
form.form.mutation.result,
|
||||
])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { HttpClient, type HttpClientError } from "@effect/platform"
|
||||
import { Button, Container, Flex, Heading, Slider, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Array, Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect"
|
||||
import { Component, ErrorObserver, Mutation, Query, Result, Subscribable, SubscriptionRef } from "effect-fc"
|
||||
import { Array, Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream, SubscriptionRef } from "effect"
|
||||
import { Component, ErrorObserver, Lens, Mutation, Query, Result, Subscribable } from "effect-fc"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ const Post = Schema.Struct({
|
||||
const ResultView = Component.make("ResultView")(function*() {
|
||||
const runPromise = yield* Component.useRunPromise()
|
||||
|
||||
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||
const idRef = yield* SubscriptionRef.make(1)
|
||||
const key = Stream.map(idRef.changes, id => [id] as const)
|
||||
const [idLens, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||
const idLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(1))
|
||||
const key = Stream.map(idLens.changes, id => [id] as const)
|
||||
|
||||
const query = yield* Query.service({
|
||||
key,
|
||||
@@ -40,11 +40,11 @@ const ResultView = Component.make("ResultView")(function*() {
|
||||
),
|
||||
})
|
||||
|
||||
return [idRef, query, mutation] as const
|
||||
return [idLens, query, mutation] as const
|
||||
}))
|
||||
|
||||
const [id, setId] = yield* SubscriptionRef.useSubscriptionRefState(idRef)
|
||||
const [queryResult, mutationResult] = yield* Subscribable.useSubscribables([query.result, mutation.result])
|
||||
const [id, setId] = yield* Lens.useState(idLens)
|
||||
const [queryResult, mutationResult] = yield* Subscribable.useAll([query.result, mutation.result])
|
||||
|
||||
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||
Effect.andThen(observer => observer.subscribe),
|
||||
@@ -105,7 +105,7 @@ const ResultView = Component.make("ResultView")(function*() {
|
||||
</div>
|
||||
|
||||
<Flex direction="row" justify="center" align="center" gap="1">
|
||||
<Button onClick={() => runPromise(Effect.andThen(idRef, id => mutation.mutate([id])))}>Mutate</Button>
|
||||
<Button onClick={() => runPromise(Effect.andThen(Lens.get(idLens), id => mutation.mutate([id])))}>Mutate</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Container>
|
||||
|
||||
@@ -21,7 +21,7 @@ const ResultView = Component.makeUntraced("Result")(function*() {
|
||||
Effect.tap(Effect.sleep("250 millis")),
|
||||
Result.forkEffect,
|
||||
))
|
||||
const [result] = yield* Subscribable.useSubscribables([resultSubscribable])
|
||||
const [result] = yield* Subscribable.useAll([resultSubscribable])
|
||||
|
||||
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||
Effect.andThen(observer => observer.subscribe),
|
||||
|
||||
88
packages/example/src/todo/EditTodoView.tsx
Normal file
88
packages/example/src/todo/EditTodoView.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Box, Flex, IconButton } from "@radix-ui/themes"
|
||||
import { Effect } from "effect"
|
||||
import { Component, Form, Subscribable, SynchronizedForm } from "effect-fc"
|
||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||
import { FaDeleteLeft } from "react-icons/fa6"
|
||||
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
||||
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
||||
import { TodoFormSchema } from "./TodoFormSchema"
|
||||
import { TodosState } from "./TodosState"
|
||||
|
||||
|
||||
export interface EditTodoViewProps {
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
export class EditTodoView extends Component.make("TodoView")(function*(props: EditTodoViewProps) {
|
||||
const state = yield* TodosState
|
||||
|
||||
const [
|
||||
indexSubscribable,
|
||||
contentField,
|
||||
completedAtField,
|
||||
] = yield* Component.useOnChange(() => Effect.gen(function*() {
|
||||
const indexSubscribable = state.getIndexSubscribable(props.id)
|
||||
|
||||
const form = yield* SynchronizedForm.service({
|
||||
schema: TodoFormSchema,
|
||||
target: state.getElementLens(props.id),
|
||||
})
|
||||
|
||||
return [
|
||||
indexSubscribable,
|
||||
Form.focusObjectField(form, "content"),
|
||||
Form.focusObjectField(form, "completedAt"),
|
||||
] as const
|
||||
}), [props.id])
|
||||
|
||||
const [index, size] = yield* Subscribable.useAll([
|
||||
indexSubscribable,
|
||||
state.sizeSubscribable,
|
||||
])
|
||||
|
||||
const runSync = yield* Component.useRunSync()
|
||||
const TextFieldFormInput = yield* TextFieldFormInputView.use
|
||||
const TextFieldOptionalFormInput = yield* TextFieldOptionalFormInputView.use
|
||||
|
||||
|
||||
return (
|
||||
<Flex direction="row" align="center" gap="2">
|
||||
<Box flexGrow="1">
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<TextFieldFormInput
|
||||
form={contentField}
|
||||
debounce="250 millis"
|
||||
/>
|
||||
|
||||
<Flex direction="row" justify="center" align="center" gap="2">
|
||||
<TextFieldOptionalFormInput
|
||||
form={completedAtField}
|
||||
type="datetime-local"
|
||||
defaultValue=""
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
<Flex direction="column" justify="center" align="center" gap="1">
|
||||
<IconButton
|
||||
disabled={index <= 0}
|
||||
onClick={() => runSync(state.moveLeft(props.id))}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={index >= size - 1}
|
||||
onClick={() => runSync(state.moveRight(props.id))}
|
||||
>
|
||||
<FaArrowDown />
|
||||
</IconButton>
|
||||
|
||||
<IconButton onClick={() => runSync(state.remove(props.id))}>
|
||||
<FaDeleteLeft />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}) {}
|
||||
78
packages/example/src/todo/NewTodoView.tsx
Normal file
78
packages/example/src/todo/NewTodoView.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Box, Button, Flex } from "@radix-ui/themes"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, type DateTime, Effect, Option, Schema } from "effect"
|
||||
import { Component, Form, Lens, SubmittableForm, Subscribable } from "effect-fc"
|
||||
import * as Domain from "@/domain"
|
||||
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
||||
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
||||
import { TodoFormSchema } from "./TodoFormSchema"
|
||||
import { TodosState } from "./TodosState"
|
||||
|
||||
|
||||
const makeTodo = makeUuid4.pipe(
|
||||
Effect.map(id => Domain.Todo.Todo.make({
|
||||
id,
|
||||
content: "",
|
||||
completedAt: Option.none(),
|
||||
})),
|
||||
Effect.provide(GetRandomValues.CryptoRandom),
|
||||
)
|
||||
|
||||
|
||||
export class NewTodoView extends Component.make("NewTodoView")(function*() {
|
||||
const state = yield* TodosState
|
||||
|
||||
const [
|
||||
form,
|
||||
contentField,
|
||||
completedAtField,
|
||||
] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||
const form = yield* SubmittableForm.service({
|
||||
schema: TodoFormSchema,
|
||||
initialEncodedValue: yield* Schema.encode(TodoFormSchema)(yield* makeTodo),
|
||||
f: ([todo, form]) => Lens.update(state.lens, Chunk.prepend(todo)).pipe(
|
||||
Effect.andThen(makeTodo),
|
||||
Effect.andThen(Schema.encode(TodoFormSchema)),
|
||||
Effect.andThen(v => Lens.set(form.encodedValue, v)),
|
||||
),
|
||||
})
|
||||
|
||||
return [
|
||||
form,
|
||||
Form.focusObjectField(form, "content"),
|
||||
Form.focusObjectField(form, "completedAt"),
|
||||
] as const
|
||||
}))
|
||||
|
||||
const [canCommit] = yield* Subscribable.useAll([form.canCommit])
|
||||
|
||||
const runPromise = yield* Component.useRunPromise<DateTime.CurrentTimeZone>()
|
||||
const TextFieldFormInput = yield* TextFieldFormInputView.use
|
||||
const TextFieldOptionalFormInput = yield* TextFieldOptionalFormInputView.use
|
||||
|
||||
|
||||
return (
|
||||
<Flex direction="row" align="center" gap="2">
|
||||
<Box flexGrow="1">
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<TextFieldFormInput
|
||||
form={contentField}
|
||||
debounce="250 millis"
|
||||
/>
|
||||
|
||||
<Flex direction="row" justify="center" align="center" gap="2">
|
||||
<TextFieldOptionalFormInput
|
||||
form={completedAtField}
|
||||
type="datetime-local"
|
||||
defaultValue=""
|
||||
/>
|
||||
|
||||
<Button disabled={!canCommit} onClick={() => void runPromise(form.submit)}>
|
||||
Add
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
)
|
||||
}) {}
|
||||
9
packages/example/src/todo/TodoFormSchema.ts
Normal file
9
packages/example/src/todo/TodoFormSchema.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Schema } from "effect"
|
||||
import * as Domain from "@/domain"
|
||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||
|
||||
|
||||
export const TodoFormSchema = Schema.compose(Schema.Struct({
|
||||
...Domain.Todo.Todo.fields,
|
||||
completedAt: Schema.OptionFromSelf(DateTimeUtcFromZonedInput),
|
||||
}), Domain.Todo.Todo)
|
||||
@@ -1,137 +0,0 @@
|
||||
import * as Domain from "@/domain"
|
||||
import { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
||||
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||
import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, type DateTime, Effect, Match, Option, Ref, Schema, Stream } from "effect"
|
||||
import { Component, Form, Lens, Subscribable } from "effect-fc"
|
||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||
import { FaDeleteLeft } from "react-icons/fa6"
|
||||
import { TodosState } from "./TodosState"
|
||||
|
||||
|
||||
const TodoFormSchema = Schema.compose(Schema.Struct({
|
||||
...Domain.Todo.Todo.fields,
|
||||
completedAt: Schema.OptionFromSelf(DateTimeUtcFromZonedInput),
|
||||
}), Domain.Todo.Todo)
|
||||
|
||||
const makeTodo = makeUuid4.pipe(
|
||||
Effect.map(id => Domain.Todo.Todo.make({
|
||||
id,
|
||||
content: "",
|
||||
completedAt: Option.none(),
|
||||
})),
|
||||
Effect.provide(GetRandomValues.CryptoRandom),
|
||||
)
|
||||
|
||||
|
||||
export type TodoProps = (
|
||||
| { readonly _tag: "new" }
|
||||
| { readonly _tag: "edit", readonly id: string }
|
||||
)
|
||||
|
||||
export class TodoView extends Component.make("TodoView")(function*(props: TodoProps) {
|
||||
const state = yield* TodosState
|
||||
|
||||
const [
|
||||
indexRef,
|
||||
form,
|
||||
contentField,
|
||||
completedAtField,
|
||||
] = yield* Component.useOnChange(() => Effect.gen(function*() {
|
||||
const indexRef = Match.value(props).pipe(
|
||||
Match.tag("new", () => Subscribable.make({ get: Effect.succeed(-1), changes: Stream.make(-1) })),
|
||||
Match.tag("edit", ({ id }) => state.getIndexSubscribable(id)),
|
||||
Match.exhaustive,
|
||||
)
|
||||
|
||||
const form = yield* Form.service({
|
||||
schema: TodoFormSchema,
|
||||
initialEncodedValue: yield* Schema.encode(TodoFormSchema)(
|
||||
yield* Match.value(props).pipe(
|
||||
Match.tag("new", () => makeTodo),
|
||||
Match.tag("edit", ({ id }) => state.getElementRef(id)),
|
||||
Match.exhaustive,
|
||||
)
|
||||
),
|
||||
f: ([todo, form]) => Match.value(props).pipe(
|
||||
Match.tag("new", () => Ref.update(state.ref, Chunk.prepend(todo)).pipe(
|
||||
Effect.andThen(makeTodo),
|
||||
Effect.andThen(Schema.encode(TodoFormSchema)),
|
||||
Effect.andThen(v => Lens.set(form.encodedValue, v)),
|
||||
)),
|
||||
Match.tag("edit", ({ id }) => Ref.set(state.getElementRef(id), todo)),
|
||||
Match.exhaustive,
|
||||
),
|
||||
})
|
||||
|
||||
return [
|
||||
indexRef,
|
||||
form,
|
||||
Form.focusObjectField(form, "content"),
|
||||
Form.focusObjectField(form, "completedAt"),
|
||||
] as const
|
||||
}), [props._tag, props._tag === "edit" ? props.id : undefined])
|
||||
|
||||
const [index, size, canCommit] = yield* Subscribable.useSubscribables([
|
||||
indexRef,
|
||||
state.sizeSubscribable,
|
||||
form.canCommit,
|
||||
])
|
||||
|
||||
const runSync = yield* Component.useRunSync()
|
||||
const runPromise = yield* Component.useRunPromise<DateTime.CurrentTimeZone>()
|
||||
const TextFieldFormInput = yield* TextFieldFormInputView.use
|
||||
const TextFieldOptionalFormInput = yield* TextFieldOptionalFormInputView.use
|
||||
|
||||
|
||||
return (
|
||||
<Flex direction="row" align="center" gap="2">
|
||||
<Box flexGrow="1">
|
||||
<Flex direction="column" align="stretch" gap="2">
|
||||
<TextFieldFormInput
|
||||
form={contentField}
|
||||
debounce="250 millis"
|
||||
/>
|
||||
|
||||
<Flex direction="row" justify="center" align="center" gap="2">
|
||||
<TextFieldOptionalFormInput
|
||||
form={completedAtField}
|
||||
type="datetime-local"
|
||||
defaultValue=""
|
||||
/>
|
||||
|
||||
{props._tag === "new" &&
|
||||
<Button disabled={!canCommit} onClick={() => void runPromise(form.submit)}>
|
||||
Add
|
||||
</Button>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{props._tag === "edit" &&
|
||||
<Flex direction="column" justify="center" align="center" gap="1">
|
||||
<IconButton
|
||||
disabled={index <= 0}
|
||||
onClick={() => runSync(state.moveLeft(props.id))}
|
||||
>
|
||||
<FaArrowUp />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
disabled={index >= size - 1}
|
||||
onClick={() => runSync(state.moveRight(props.id))}
|
||||
>
|
||||
<FaArrowDown />
|
||||
</IconButton>
|
||||
|
||||
<IconButton onClick={() => runSync(state.remove(props.id))}>
|
||||
<FaDeleteLeft />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
}) {}
|
||||
@@ -1,7 +1,7 @@
|
||||
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"
|
||||
import { Lens, Subscribable } from "effect-fc"
|
||||
import { Todo } from "@/domain"
|
||||
|
||||
|
||||
@@ -30,27 +30,29 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||
: kv.remove(key)
|
||||
)
|
||||
|
||||
const ref = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
||||
yield* Effect.forkScoped(ref.changes.pipe(
|
||||
const lens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(yield* readFromLocalStorage))
|
||||
yield* Effect.forkScoped(lens.changes.pipe(
|
||||
Stream.debounce("500 millis"),
|
||||
Stream.runForEach(saveToLocalStorage),
|
||||
))
|
||||
yield* Effect.addFinalizer(() => ref.pipe(
|
||||
yield* Effect.addFinalizer(() => Lens.get(lens).pipe(
|
||||
Effect.andThen(saveToLocalStorage),
|
||||
Effect.ignore,
|
||||
))
|
||||
|
||||
const sizeSubscribable = Subscribable.make({
|
||||
get: Effect.andThen(ref, Chunk.size),
|
||||
get changes() { return Stream.map(ref.changes, Chunk.size) },
|
||||
})
|
||||
const getElementRef = (id: string) => SubscriptionSubRef.makeFromChunkFindFirst(ref, v => v.id === id)
|
||||
const getIndexSubscribable = (id: string) => Subscribable.make({
|
||||
get: Effect.flatMap(ref, Chunk.findFirstIndex(v => v.id === id)),
|
||||
get changes() { return Stream.flatMap(ref.changes, Chunk.findFirstIndex(v => v.id === id)) },
|
||||
})
|
||||
const sizeSubscribable = Subscribable.map(lens, Chunk.size)
|
||||
|
||||
const moveLeft = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.Do.pipe(
|
||||
const getElementLens = (id: string) => Lens.mapEffect(
|
||||
lens,
|
||||
Chunk.findFirst(v => v.id === id),
|
||||
(a, b) => Effect.flatMap(
|
||||
Chunk.findFirstIndex(a, v => v.id === id),
|
||||
i => Chunk.replaceOption(a, i, b),
|
||||
)
|
||||
)
|
||||
const getIndexSubscribable = (id: string) => Subscribable.mapEffect(lens, Chunk.findFirstIndex(v => v.id === id))
|
||||
|
||||
const moveLeft = (id: string) => Lens.updateEffect(lens, todos => Effect.Do.pipe(
|
||||
Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === id)),
|
||||
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
||||
Effect.bind("previous", ({ index }) => Chunk.get(todos, index - 1)),
|
||||
@@ -62,7 +64,7 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||
: todos
|
||||
),
|
||||
))
|
||||
const moveRight = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.Do.pipe(
|
||||
const moveRight = (id: string) => Lens.updateEffect(lens, todos => Effect.Do.pipe(
|
||||
Effect.bind("index", () => Chunk.findFirstIndex(todos, v => v.id === id)),
|
||||
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
||||
Effect.bind("next", ({ index }) => Chunk.get(todos, index + 1)),
|
||||
@@ -74,15 +76,15 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
||||
: todos
|
||||
),
|
||||
))
|
||||
const remove = (id: string) => SubscriptionRef.updateEffect(ref, todos => Effect.andThen(
|
||||
const remove = (id: string) => Lens.updateEffect(lens, todos => Effect.andThen(
|
||||
Chunk.findFirstIndex(todos, v => v.id === id),
|
||||
index => Chunk.remove(todos, index),
|
||||
))
|
||||
|
||||
return {
|
||||
ref,
|
||||
lens,
|
||||
sizeSubscribable,
|
||||
getElementRef,
|
||||
getElementLens,
|
||||
getIndexSubscribable,
|
||||
moveLeft,
|
||||
moveRight,
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||
import { Chunk, Console, Effect } from "effect"
|
||||
import { Component, Subscribable } from "effect-fc"
|
||||
import { EditTodoView } from "./EditTodoView"
|
||||
import { NewTodoView } from "./NewTodoView"
|
||||
import { TodosState } from "./TodosState"
|
||||
import { TodoView } from "./TodoView"
|
||||
|
||||
|
||||
export class TodosView extends Component.make("TodosView")(function*() {
|
||||
const state = yield* TodosState
|
||||
const [todos] = yield* Subscribable.useSubscribables([state.ref])
|
||||
const [todos] = yield* Subscribable.useAll([state.lens])
|
||||
|
||||
yield* Component.useOnMount(() => Effect.andThen(
|
||||
Console.log("Todos mounted"),
|
||||
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||
))
|
||||
|
||||
const Todo = yield* TodoView.use
|
||||
const NewTodo = yield* NewTodoView.use
|
||||
const EditTodo = yield* EditTodoView.use
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Heading align="center">Todos</Heading>
|
||||
|
||||
<Flex direction="column" align="stretch" gap="2" mt="2">
|
||||
<Todo _tag="new" />
|
||||
<NewTodo />
|
||||
|
||||
{Chunk.map(todos, todo =>
|
||||
<Todo key={todo.id} _tag="edit" id={todo.id} />
|
||||
<EditTodo key={todo.id} id={todo.id} />
|
||||
)}
|
||||
</Flex>
|
||||
</Container>
|
||||
|
||||
Reference in New Issue
Block a user