0.2.1 (#26)
Co-authored-by: Julien Valverdé <julien.valverde@mailo.com> Co-authored-by: Renovate Bot <renovate-bot@valverde.cloud> Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Button, Container, Flex } from "@radix-ui/themes"
|
||||
import { Button, Container, Flex, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Console, Effect, Option, ParseResult, Schema } from "effect"
|
||||
import { Console, Effect, Match, Option, ParseResult, Schema } from "effect"
|
||||
import { Component, Form, Subscribable } from "effect-fc"
|
||||
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||
import { DateTimeUtcFromZonedInput } from "@/lib/schema"
|
||||
@@ -23,6 +23,21 @@ const RegisterFormSchema = Schema.Struct({
|
||||
birth: Schema.OptionFromSelf(DateTimeUtcFromZonedInput),
|
||||
})
|
||||
|
||||
const RegisterFormSubmitSchema = Schema.Struct({
|
||||
email: Schema.transformOrFail(
|
||||
Schema.String,
|
||||
Schema.String,
|
||||
{
|
||||
decode: (input, _options, ast) => input !== "admin@admin.com"
|
||||
? ParseResult.succeed(input)
|
||||
: ParseResult.fail(new ParseResult.Type(ast, input, "This email is already in use.")),
|
||||
encode: ParseResult.succeed,
|
||||
},
|
||||
),
|
||||
password: Schema.String,
|
||||
birth: Schema.OptionFromSelf(Schema.DateTimeUtcFromSelf),
|
||||
})
|
||||
|
||||
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||
scoped: Form.service({
|
||||
schema: RegisterFormSchema.pipe(
|
||||
@@ -39,19 +54,22 @@ class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||
),
|
||||
|
||||
initialEncodedValue: { email: "", password: "", birth: Option.none() },
|
||||
onSubmit: v => Effect.sleep("500 millis").pipe(
|
||||
Effect.andThen(Console.log(v)),
|
||||
Effect.andThen(Effect.sync(() => alert("Done!"))),
|
||||
),
|
||||
f: Effect.fnUntraced(function*([value]) {
|
||||
yield* Effect.sleep("500 millis")
|
||||
return yield* Schema.decode(RegisterFormSubmitSchema)(value)
|
||||
}),
|
||||
debounce: "500 millis",
|
||||
})
|
||||
}) {}
|
||||
|
||||
class RegisterFormView extends Component.makeUntraced("RegisterFormView")(function*() {
|
||||
const form = yield* RegisterForm
|
||||
const submit = yield* Form.useSubmit(form)
|
||||
const [canSubmit] = yield* Subscribable.useSubscribables(form.canSubmitSubscribable)
|
||||
const [canSubmit, submitResult] = yield* Subscribable.useSubscribables([
|
||||
form.canSubmit,
|
||||
form.mutation.result,
|
||||
])
|
||||
|
||||
const runPromise = yield* Component.useRunPromise()
|
||||
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||
|
||||
yield* Component.useOnMount(() => Effect.gen(function*() {
|
||||
@@ -64,27 +82,35 @@ class RegisterFormView extends Component.makeUntraced("RegisterFormView")(functi
|
||||
<Container width="300">
|
||||
<form onSubmit={e => {
|
||||
e.preventDefault()
|
||||
void submit()
|
||||
void runPromise(form.submit)
|
||||
}}>
|
||||
<Flex direction="column" gap="2">
|
||||
<TextFieldFormInputFC
|
||||
field={Form.useField(form, ["email"])}
|
||||
field={yield* form.field(["email"])}
|
||||
/>
|
||||
|
||||
<TextFieldFormInputFC
|
||||
field={Form.useField(form, ["password"])}
|
||||
field={yield* form.field(["password"])}
|
||||
/>
|
||||
|
||||
<TextFieldFormInputFC
|
||||
optional
|
||||
type="datetime-local"
|
||||
field={Form.useField(form, ["birth"])}
|
||||
field={yield* form.field(["birth"])}
|
||||
defaultValue=""
|
||||
/>
|
||||
|
||||
<Button disabled={!canSubmit}>Submit</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
|
||||
{Match.value(submitResult).pipe(
|
||||
Match.tag("Initial", () => <></>),
|
||||
Match.tag("Running", () => <Text>Submitting...</Text>),
|
||||
Match.tag("Success", () => <Text>Submitted successfully!</Text>),
|
||||
Match.tag("Failure", e => <Text>Error: {e.cause.toString()}</Text>),
|
||||
Match.exhaustive,
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}) {}
|
||||
|
||||
116
packages/example/src/routes/query.tsx
Normal file
116
packages/example/src/routes/query.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
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 { runtime } from "@/runtime"
|
||||
|
||||
|
||||
const Post = Schema.Struct({
|
||||
userId: Schema.Int,
|
||||
id: Schema.Int,
|
||||
title: Schema.String,
|
||||
body: Schema.String,
|
||||
})
|
||||
|
||||
const ResultView = Component.makeUntraced("Result")(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.zipLatest(Stream.make("posts" as const), idRef.changes)
|
||||
|
||||
const query = yield* Query.service({
|
||||
key,
|
||||
f: ([, id]) => HttpClient.HttpClient.pipe(
|
||||
Effect.tap(Effect.sleep("500 millis")),
|
||||
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||
Effect.andThen(response => response.json),
|
||||
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||
),
|
||||
})
|
||||
|
||||
const mutation = yield* Mutation.make({
|
||||
f: ([id]: readonly [id: number]) => HttpClient.HttpClient.pipe(
|
||||
Effect.tap(Effect.sleep("500 millis")),
|
||||
Effect.andThen(client => client.get(`https://jsonplaceholder.typicode.com/posts/${ id }`)),
|
||||
Effect.andThen(response => response.json),
|
||||
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||
),
|
||||
})
|
||||
|
||||
return [idRef, query, mutation] as const
|
||||
}))
|
||||
|
||||
const [id, setId] = yield* SubscriptionRef.useSubscriptionRefState(idRef)
|
||||
const [queryResult, mutationResult] = yield* Subscribable.useSubscribables([query.result, mutation.result])
|
||||
|
||||
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||
Effect.andThen(observer => observer.subscribe),
|
||||
Effect.andThen(Stream.fromQueue),
|
||||
Stream.unwrapScoped,
|
||||
Stream.runForEach(flow(
|
||||
Cause.failures,
|
||||
Chunk.findFirst(e => e._tag === "RequestError" || e._tag === "ResponseError"),
|
||||
Option.match({
|
||||
onSome: e => Console.log("ResultView HttpClient error", e),
|
||||
onNone: () => Effect.void,
|
||||
}),
|
||||
)),
|
||||
Effect.forkScoped,
|
||||
))
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Flex direction="column" align="center" gap="2">
|
||||
<Slider
|
||||
value={[id]}
|
||||
onValueChange={flow(Array.head, Option.getOrThrow, setId)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{Match.value(queryResult).pipe(
|
||||
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||
Match.tag("Success", result => <>
|
||||
<Heading>{result.value.title}</Heading>
|
||||
<Text>{result.value.body}</Text>
|
||||
{Result.isRefreshing(result) && <Text>Refreshing...</Text>}
|
||||
</>),
|
||||
Match.tag("Failure", result =>
|
||||
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||
),
|
||||
Match.orElse(() => <></>),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Flex direction="row" justify="center" align="center" gap="1">
|
||||
<Button onClick={() => runPromise(query.refresh)}>Refresh</Button>
|
||||
<Button onClick={() => runPromise(query.refetch)}>Refetch</Button>
|
||||
</Flex>
|
||||
|
||||
<div>
|
||||
{Match.value(mutationResult).pipe(
|
||||
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||
Match.tag("Success", result => <>
|
||||
<Heading>{result.value.title}</Heading>
|
||||
<Text>{result.value.body}</Text>
|
||||
{Result.isRefreshing(result) && <Text>Refreshing...</Text>}
|
||||
</>),
|
||||
Match.tag("Failure", result =>
|
||||
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||
),
|
||||
Match.orElse(() => <></>),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Flex direction="row" justify="center" align="center" gap="1">
|
||||
<Button onClick={() => runPromise(Effect.andThen(idRef, id => mutation.mutate([id])))}>Mutate</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
|
||||
export const Route = createFileRoute("/query")({
|
||||
component: Component.withRuntime(ResultView, runtime.context)
|
||||
})
|
||||
60
packages/example/src/routes/result.tsx
Normal file
60
packages/example/src/routes/result.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { HttpClient, type HttpClientError } from "@effect/platform"
|
||||
import { Container, Heading, Text } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Cause, Chunk, Console, Effect, flow, Match, Option, Schema, Stream } from "effect"
|
||||
import { Component, ErrorObserver, Result, Subscribable } from "effect-fc"
|
||||
import { runtime } from "@/runtime"
|
||||
|
||||
|
||||
const Post = Schema.Struct({
|
||||
userId: Schema.Int,
|
||||
id: Schema.Int,
|
||||
title: Schema.String,
|
||||
body: Schema.String,
|
||||
})
|
||||
|
||||
const ResultView = Component.makeUntraced("Result")(function*() {
|
||||
const [resultSubscribable] = yield* Component.useOnMount(() => HttpClient.HttpClient.pipe(
|
||||
Effect.andThen(client => client.get("https://jsonplaceholder.typicode.com/posts/1")),
|
||||
Effect.andThen(response => response.json),
|
||||
Effect.andThen(Schema.decodeUnknown(Post)),
|
||||
Effect.tap(Effect.sleep("250 millis")),
|
||||
Result.forkEffect,
|
||||
))
|
||||
const [result] = yield* Subscribable.useSubscribables([resultSubscribable])
|
||||
|
||||
yield* Component.useOnMount(() => ErrorObserver.ErrorObserver<HttpClientError.HttpClientError>().pipe(
|
||||
Effect.andThen(observer => observer.subscribe),
|
||||
Effect.andThen(Stream.fromQueue),
|
||||
Stream.unwrapScoped,
|
||||
Stream.runForEach(flow(
|
||||
Cause.failures,
|
||||
Chunk.findFirst(e => e._tag === "RequestError" || e._tag === "ResponseError"),
|
||||
Option.match({
|
||||
onSome: e => Console.log("ResultView HttpClient error", e),
|
||||
onNone: () => Effect.void,
|
||||
}),
|
||||
)),
|
||||
Effect.forkScoped,
|
||||
))
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{Match.value(result).pipe(
|
||||
Match.tag("Running", () => <Text>Loading...</Text>),
|
||||
Match.tag("Success", result => <>
|
||||
<Heading>{result.value.title}</Heading>
|
||||
<Text>{result.value.body}</Text>
|
||||
</>),
|
||||
Match.tag("Failure", result =>
|
||||
<Text>An error has occured: {result.cause.toString()}</Text>
|
||||
),
|
||||
Match.orElse(() => <></>),
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
|
||||
export const Route = createFileRoute("/result")({
|
||||
component: Component.withRuntime(ResultView, runtime.context)
|
||||
})
|
||||
Reference in New Issue
Block a user