This commit is contained in:
@@ -13,7 +13,7 @@ export class TextFieldFormInputView extends Component.make("TextFieldFormInputVi
|
|||||||
props: TextFieldFormInputView.Props
|
props: TextFieldFormInputView.Props
|
||||||
) {
|
) {
|
||||||
const input = yield* Form.useInput(props.form, 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.issues,
|
||||||
props.form.isValidating,
|
props.form.isValidating,
|
||||||
props.form.isCommitting,
|
props.form.isCommitting,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class TextFieldOptionalFormInputView extends Component.make("TextFieldOpt
|
|||||||
props: TextFieldOptionalFormInputView.Props
|
props: TextFieldOptionalFormInputView.Props
|
||||||
) {
|
) {
|
||||||
const input = yield* Form.useOptionalInput(props.form, 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.issues,
|
||||||
props.form.isValidating,
|
props.form.isValidating,
|
||||||
props.form.isCommitting,
|
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 { TextFieldFormInputView } from "@/lib/form/TextFieldFormInputView"
|
||||||
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
import { TextFieldOptionalFormInputView } from "@/lib/form/TextFieldOptionalFormInputView"
|
||||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||||
import { runtime } from "@/runtime"
|
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>(
|
const email = Schema.pattern<typeof Schema.String>(
|
||||||
@@ -41,7 +41,7 @@ const RegisterFormSubmitSchema = Schema.Struct({
|
|||||||
|
|
||||||
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
|
class RegisterFormService extends Effect.Service<RegisterFormService>()("RegisterFormService", {
|
||||||
scoped: Effect.gen(function*() {
|
scoped: Effect.gen(function*() {
|
||||||
const form = yield* Form.service({
|
const form = yield* SubmittableForm.service({
|
||||||
schema: RegisterFormSchema.pipe(
|
schema: RegisterFormSchema.pipe(
|
||||||
Schema.compose(
|
Schema.compose(
|
||||||
Schema.transformOrFail(
|
Schema.transformOrFail(
|
||||||
@@ -73,7 +73,7 @@ class RegisterFormService extends Effect.Service<RegisterFormService>()("Registe
|
|||||||
|
|
||||||
class RegisterFormView extends Component.make("RegisterFormView")(function*() {
|
class RegisterFormView extends Component.make("RegisterFormView")(function*() {
|
||||||
const form = yield* RegisterFormService
|
const form = yield* RegisterFormService
|
||||||
const [canCommit, submitResult] = yield* Subscribable.useSubscribables([
|
const [canCommit, submitResult] = yield* Subscribable.useAll([
|
||||||
form.form.canCommit,
|
form.form.canCommit,
|
||||||
form.form.mutation.result,
|
form.form.mutation.result,
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { HttpClient, type HttpClientError } from "@effect/platform"
|
import { HttpClient, type HttpClientError } from "@effect/platform"
|
||||||
import { Button, Container, Flex, Heading, Slider, Text } from "@radix-ui/themes"
|
import { Button, Container, Flex, Heading, Slider, Text } from "@radix-ui/themes"
|
||||||
import { createFileRoute } from "@tanstack/react-router"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Array, Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect"
|
import { Array, Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream, SubscriptionRef } from "effect"
|
||||||
import { Component, ErrorObserver, Mutation, Query, Result, Subscribable, SubscriptionRef } from "effect-fc"
|
import { Component, ErrorObserver, Lens, Mutation, Query, Result, Subscribable } from "effect-fc"
|
||||||
import { runtime } from "@/runtime"
|
import { runtime } from "@/runtime"
|
||||||
|
|
||||||
|
|
||||||
@@ -16,9 +16,9 @@ const Post = Schema.Struct({
|
|||||||
const ResultView = Component.make("ResultView")(function*() {
|
const ResultView = Component.make("ResultView")(function*() {
|
||||||
const runPromise = yield* Component.useRunPromise()
|
const runPromise = yield* Component.useRunPromise()
|
||||||
|
|
||||||
const [idRef, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
const [idLens, query, mutation] = yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||||
const idRef = yield* SubscriptionRef.make(1)
|
const idLens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(1))
|
||||||
const key = Stream.map(idRef.changes, id => [id] as const)
|
const key = Stream.map(idLens.changes, id => [id] as const)
|
||||||
|
|
||||||
const query = yield* Query.service({
|
const query = yield* Query.service({
|
||||||
key,
|
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 [id, setId] = yield* Lens.useState(idLens)
|
||||||
const [queryResult, mutationResult] = yield* Subscribable.useSubscribables([query.result, mutation.result])
|
const [queryResult, mutationResult] = yield* Subscribable.useAll([query.result, mutation.result])
|
||||||
|
|
||||||
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||||
Effect.andThen(observer => observer.subscribe),
|
Effect.andThen(observer => observer.subscribe),
|
||||||
@@ -105,7 +105,7 @@ const ResultView = Component.make("ResultView")(function*() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Flex direction="row" justify="center" align="center" gap="1">
|
<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>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const ResultView = Component.makeUntraced("Result")(function*() {
|
|||||||
Effect.tap(Effect.sleep("250 millis")),
|
Effect.tap(Effect.sleep("250 millis")),
|
||||||
Result.forkEffect,
|
Result.forkEffect,
|
||||||
))
|
))
|
||||||
const [result] = yield* Subscribable.useSubscribables([resultSubscribable])
|
const [result] = yield* Subscribable.useAll([resultSubscribable])
|
||||||
|
|
||||||
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||||
Effect.andThen(observer => observer.subscribe),
|
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 { KeyValueStore } from "@effect/platform"
|
||||||
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
import { BrowserKeyValueStore } from "@effect/platform-browser"
|
||||||
import { Chunk, Console, Effect, Option, Schema, Stream, SubscriptionRef } from "effect"
|
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"
|
import { Todo } from "@/domain"
|
||||||
|
|
||||||
|
|
||||||
@@ -30,27 +30,29 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
|||||||
: kv.remove(key)
|
: kv.remove(key)
|
||||||
)
|
)
|
||||||
|
|
||||||
const ref = yield* SubscriptionRef.make(yield* readFromLocalStorage)
|
const lens = Lens.fromSubscriptionRef(yield* SubscriptionRef.make(yield* readFromLocalStorage))
|
||||||
yield* Effect.forkScoped(ref.changes.pipe(
|
yield* Effect.forkScoped(lens.changes.pipe(
|
||||||
Stream.debounce("500 millis"),
|
Stream.debounce("500 millis"),
|
||||||
Stream.runForEach(saveToLocalStorage),
|
Stream.runForEach(saveToLocalStorage),
|
||||||
))
|
))
|
||||||
yield* Effect.addFinalizer(() => ref.pipe(
|
yield* Effect.addFinalizer(() => Lens.get(lens).pipe(
|
||||||
Effect.andThen(saveToLocalStorage),
|
Effect.andThen(saveToLocalStorage),
|
||||||
Effect.ignore,
|
Effect.ignore,
|
||||||
))
|
))
|
||||||
|
|
||||||
const sizeSubscribable = Subscribable.make({
|
const sizeSubscribable = Subscribable.map(lens, Chunk.size)
|
||||||
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 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("index", () => Chunk.findFirstIndex(todos, v => v.id === id)),
|
||||||
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
||||||
Effect.bind("previous", ({ index }) => Chunk.get(todos, index - 1)),
|
Effect.bind("previous", ({ index }) => Chunk.get(todos, index - 1)),
|
||||||
@@ -62,7 +64,7 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
|||||||
: todos
|
: 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("index", () => Chunk.findFirstIndex(todos, v => v.id === id)),
|
||||||
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
Effect.bind("todo", ({ index }) => Chunk.get(todos, index)),
|
||||||
Effect.bind("next", ({ index }) => Chunk.get(todos, index + 1)),
|
Effect.bind("next", ({ index }) => Chunk.get(todos, index + 1)),
|
||||||
@@ -74,15 +76,15 @@ export class TodosState extends Effect.Service<TodosState>()("TodosState", {
|
|||||||
: todos
|
: 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),
|
Chunk.findFirstIndex(todos, v => v.id === id),
|
||||||
index => Chunk.remove(todos, index),
|
index => Chunk.remove(todos, index),
|
||||||
))
|
))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ref,
|
lens,
|
||||||
sizeSubscribable,
|
sizeSubscribable,
|
||||||
getElementRef,
|
getElementLens,
|
||||||
getIndexSubscribable,
|
getIndexSubscribable,
|
||||||
moveLeft,
|
moveLeft,
|
||||||
moveRight,
|
moveRight,
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
import { Container, Flex, Heading } from "@radix-ui/themes"
|
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||||
import { Chunk, Console, Effect } from "effect"
|
import { Chunk, Console, Effect } from "effect"
|
||||||
import { Component, Subscribable } from "effect-fc"
|
import { Component, Subscribable } from "effect-fc"
|
||||||
|
import { EditTodoView } from "./EditTodoView"
|
||||||
|
import { NewTodoView } from "./NewTodoView"
|
||||||
import { TodosState } from "./TodosState"
|
import { TodosState } from "./TodosState"
|
||||||
import { TodoView } from "./TodoView"
|
|
||||||
|
|
||||||
|
|
||||||
export class TodosView extends Component.make("TodosView")(function*() {
|
export class TodosView extends Component.make("TodosView")(function*() {
|
||||||
const state = yield* TodosState
|
const state = yield* TodosState
|
||||||
const [todos] = yield* Subscribable.useSubscribables([state.ref])
|
const [todos] = yield* Subscribable.useAll([state.lens])
|
||||||
|
|
||||||
yield* Component.useOnMount(() => Effect.andThen(
|
yield* Component.useOnMount(() => Effect.andThen(
|
||||||
Console.log("Todos mounted"),
|
Console.log("Todos mounted"),
|
||||||
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
||||||
))
|
))
|
||||||
|
|
||||||
const Todo = yield* TodoView.use
|
const NewTodo = yield* NewTodoView.use
|
||||||
|
const EditTodo = yield* EditTodoView.use
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Heading align="center">Todos</Heading>
|
<Heading align="center">Todos</Heading>
|
||||||
|
|
||||||
<Flex direction="column" align="stretch" gap="2" mt="2">
|
<Flex direction="column" align="stretch" gap="2" mt="2">
|
||||||
<Todo _tag="new" />
|
<NewTodo />
|
||||||
|
|
||||||
{Chunk.map(todos, todo =>
|
{Chunk.map(todos, todo =>
|
||||||
<Todo key={todo.id} _tag="edit" id={todo.id} />
|
<EditTodo key={todo.id} id={todo.id} />
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
Reference in New Issue
Block a user