Compare commits
46 Commits
renovate/b
...
ef137d07f7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef137d07f7 | ||
|
|
917a9590de | ||
|
|
a1dc98aa04 | ||
|
|
9d978e709f | ||
|
|
040e671fd3 | ||
|
|
47e21f6340 | ||
|
|
58bb84ac2b | ||
|
|
74a8714acb | ||
|
|
833cfa503a | ||
|
|
73134478ed | ||
|
|
71d3c77e1a | ||
|
|
b9210885a7 | ||
|
|
ef042ed4b1 | ||
|
|
3eaa250c28 | ||
|
|
70555943c1 | ||
|
|
7e57cadd9c | ||
|
|
5c748d7dac | ||
|
|
b3d6cc6764 | ||
|
|
5f531c9b2e | ||
|
|
a7471c0d49 | ||
|
|
2df12d7f40 | ||
|
|
bfd4b7f073 | ||
|
|
8c1fed7800 | ||
|
|
e2cd7bb671 | ||
|
|
9033fcbb32 | ||
|
|
ed1efb41cd | ||
|
|
46ef35040a | ||
|
|
7545b4bb30 | ||
|
|
8b5074bd56 | ||
|
|
450d11cc3e | ||
|
|
f6e69a05fd | ||
|
|
a11180d03e | ||
|
|
8ba0189472 | ||
|
|
64114f0208 | ||
|
|
ecc37515b8 | ||
|
|
483e90dc67 | ||
|
|
fa6dc2a921 | ||
|
|
e61d8ecae1 | ||
|
|
7c5a118717 | ||
|
|
edb4b7ccd8 | ||
|
|
30f3ef2353 | ||
|
|
ae3aa00a06 | ||
|
|
4845e34b0a | ||
|
|
3613d421f3 | ||
|
|
993e2c1038 | ||
|
|
8057163ad0 |
8
bun.lock
8
bun.lock
@@ -12,7 +12,10 @@
|
||||
},
|
||||
"packages/effect-fc": {
|
||||
"name": "effect-fc",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"dependencies": {
|
||||
"@typed/async-data": "^0.13.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/language-service": "^0.35.2",
|
||||
},
|
||||
@@ -51,6 +54,7 @@
|
||||
"globals": "^16.3.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"type-fest": "^4.41.0",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"vite": "^7.1.3",
|
||||
},
|
||||
@@ -799,6 +803,8 @@
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.40.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "@typescript-eslint/typescript-estree": "8.40.0", "@typescript-eslint/utils": "8.40.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q=="],
|
||||
|
||||
@@ -45,5 +45,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@effect/language-service": "^0.35.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typed/async-data": "^0.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, String, Tracer, type Types, type Utils } from "effect"
|
||||
import { Context, Effect, Effectable, ExecutionStrategy, Function, Predicate, Runtime, Scope, Tracer, type Types, type Utils } from "effect"
|
||||
import * as React from "react"
|
||||
import { Hooks } from "./hooks/index.js"
|
||||
import * as Memo from "./Memo.js"
|
||||
@@ -331,13 +331,9 @@ export const make: (
|
||||
) => make.Gen & make.NonGen)
|
||||
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => {
|
||||
if (typeof spanNameOrBody !== "string") {
|
||||
const displayName = displayNameFromBody(spanNameOrBody)
|
||||
return Object.setPrototypeOf(
|
||||
Object.assign(function() {}, defaultOptions, {
|
||||
body: displayName
|
||||
? Effect.fn(displayName)(spanNameOrBody as any, ...pipeables as [])
|
||||
: Effect.fn(spanNameOrBody as any, ...pipeables),
|
||||
displayName,
|
||||
body: Effect.fn(spanNameOrBody as any, ...pipeables),
|
||||
}),
|
||||
ComponentProto,
|
||||
)
|
||||
@@ -347,26 +343,34 @@ export const make: (
|
||||
return (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
||||
Object.assign(function() {}, defaultOptions, {
|
||||
body: Effect.fn(spanNameOrBody, spanOptions)(body, ...pipeables as []),
|
||||
displayName: displayNameFromBody(body) ?? spanNameOrBody,
|
||||
displayName: spanNameOrBody,
|
||||
}),
|
||||
ComponentProto,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const makeUntraced: make.Gen & make.NonGen = (
|
||||
body: Function,
|
||||
...pipeables: any[]
|
||||
) => Object.setPrototypeOf(
|
||||
export const makeUntraced: (
|
||||
& make.Gen
|
||||
& make.NonGen
|
||||
& ((name: string) => make.Gen & make.NonGen)
|
||||
) = (spanNameOrBody: Function | string, ...pipeables: any[]): any => (
|
||||
typeof spanNameOrBody !== "string"
|
||||
? Object.setPrototypeOf(
|
||||
Object.assign(function() {}, defaultOptions, {
|
||||
body: Effect.fnUntraced(body as any, ...pipeables as []),
|
||||
displayName: displayNameFromBody(body),
|
||||
body: Effect.fnUntraced(spanNameOrBody as any, ...pipeables as []),
|
||||
}),
|
||||
ComponentProto,
|
||||
)
|
||||
: (body: any, ...pipeables: any[]) => Object.setPrototypeOf(
|
||||
Object.assign(function() {}, defaultOptions, {
|
||||
body: Effect.fnUntraced(body, ...pipeables as []),
|
||||
displayName: spanNameOrBody,
|
||||
}),
|
||||
ComponentProto,
|
||||
)
|
||||
)
|
||||
|
||||
const displayNameFromBody = (body: Function) => !String.isEmpty(body.name) ? body.name : undefined
|
||||
|
||||
export const withOptions: {
|
||||
<T extends Component<any, any, any, any>>(
|
||||
options: Partial<Component.Options>
|
||||
|
||||
398
packages/effect-fc/src/Form.ts
Normal file
398
packages/effect-fc/src/Form.ts
Normal file
@@ -0,0 +1,398 @@
|
||||
import * as AsyncData from "@typed/async-data"
|
||||
import { Array, Cause, Chunk, Duration, Effect, Equal, Exit, Fiber, flow, identity, Option, ParseResult, pipe, Pipeable, Ref, Schema, Scope, Stream, Subscribable, SubscriptionRef } from "effect"
|
||||
import type { NoSuchElementException } from "effect/Cause"
|
||||
import * as React from "react"
|
||||
import { Hooks } from "./hooks/index.js"
|
||||
import { PropertyPath, Subscribable as SubscribableInternal, SubscriptionSubRef } from "./types/index.js"
|
||||
|
||||
|
||||
export const FormTypeId: unique symbol = Symbol.for("effect-fc/Form")
|
||||
export type FormTypeId = typeof FormTypeId
|
||||
|
||||
export interface Form<in out A, in out I = A, out R = never, in out SA = void, in out SE = A, out SR = never>
|
||||
extends Pipeable.Pipeable {
|
||||
readonly [FormTypeId]: FormTypeId
|
||||
|
||||
readonly schema: Schema.Schema<A, I, R>,
|
||||
readonly submit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>,
|
||||
|
||||
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
|
||||
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
||||
readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<void, never>>>
|
||||
readonly submitStateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<SA, SE>>,
|
||||
|
||||
readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>
|
||||
}
|
||||
|
||||
class FormImpl<in out A, in out I = A, out R = never, in out SA = void, in out SE = A, out SR = never>
|
||||
extends Pipeable.Class() implements Form<A, I, R, SA, SE, SR> {
|
||||
readonly [FormTypeId]: FormTypeId = FormTypeId
|
||||
|
||||
constructor(
|
||||
readonly schema: Schema.Schema<A, I, R>,
|
||||
readonly submit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>,
|
||||
|
||||
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
|
||||
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
||||
readonly validationFiberRef: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<void, never>>>,
|
||||
readonly submitStateRef: SubscriptionRef.SubscriptionRef<AsyncData.AsyncData<SA, SE>>,
|
||||
|
||||
readonly canSubmitSubscribable: Subscribable.Subscribable<boolean>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
export namespace make {
|
||||
export interface Options<in out A, in out I, out R, in out SA = void, in out SE = A, out SR = never> {
|
||||
readonly schema: Schema.Schema<A, I, R>
|
||||
readonly initialEncodedValue: NoInfer<I>
|
||||
readonly submit: (value: NoInfer<A>) => Effect.Effect<SA, SE, SR>
|
||||
}
|
||||
}
|
||||
|
||||
export const make: {
|
||||
<A, I = A, R = never, SA = void, SE = A, SR = never>(
|
||||
options: make.Options<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<Form<A, I, R, SA, SE, SR>>
|
||||
} = Effect.fnUntraced(function* <A, I = A, R = never, SA = void, SE = A, SR = never>(
|
||||
options: make.Options<A, I, R, SA, SE, SR>
|
||||
) {
|
||||
const valueRef = yield* SubscriptionRef.make(Option.none<A>())
|
||||
const errorRef = yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>())
|
||||
const validationFiberRef = yield* SubscriptionRef.make(Option.none<Fiber.Fiber<void, never>>())
|
||||
const submitStateRef = yield* SubscriptionRef.make(AsyncData.noData<SA, SE>())
|
||||
|
||||
return new FormImpl(
|
||||
options.schema,
|
||||
options.submit,
|
||||
|
||||
valueRef,
|
||||
yield* SubscriptionRef.make(options.initialEncodedValue),
|
||||
errorRef,
|
||||
validationFiberRef,
|
||||
submitStateRef,
|
||||
|
||||
pipe(
|
||||
<A>([value, error, validationFiber, submitState]: readonly [
|
||||
Option.Option<A>,
|
||||
Option.Option<ParseResult.ParseError>,
|
||||
Option.Option<Fiber.Fiber<void, never>>,
|
||||
AsyncData.AsyncData<SA, SE>,
|
||||
]) => Option.isSome(value) && Option.isNone(error) && Option.isNone(validationFiber) && !AsyncData.isLoading(submitState),
|
||||
|
||||
filter => SubscribableInternal.make({
|
||||
get: Effect.map(Effect.all([valueRef, errorRef, validationFiberRef, submitStateRef]), filter),
|
||||
get changes() {
|
||||
return Stream.map(
|
||||
Stream.zipLatestAll(
|
||||
valueRef.changes,
|
||||
errorRef.changes,
|
||||
validationFiberRef.changes,
|
||||
submitStateRef.changes,
|
||||
),
|
||||
filter,
|
||||
)
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
export const run = <A, I, R, SA, SE, SR>(
|
||||
self: Form<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<void, never, Scope.Scope | R> => Stream.runForEach(
|
||||
self.encodedValueRef.changes,
|
||||
encodedValue => self.validationFiberRef.pipe(
|
||||
Effect.andThen(Option.match({
|
||||
onSome: Fiber.interrupt,
|
||||
onNone: () => Effect.void,
|
||||
})),
|
||||
Effect.andThen(
|
||||
Effect.addFinalizer(() => SubscriptionRef.set(self.validationFiberRef, Option.none())).pipe(
|
||||
Effect.andThen(Schema.decode(self.schema, { errors: "all" })(encodedValue)),
|
||||
Effect.exit,
|
||||
Effect.andThen(flow(
|
||||
Exit.matchEffect({
|
||||
onSuccess: v => Effect.andThen(
|
||||
SubscriptionRef.set(self.valueRef, Option.some(v)),
|
||||
SubscriptionRef.set(self.errorRef, Option.none()),
|
||||
),
|
||||
onFailure: c => Option.match(
|
||||
Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"),
|
||||
{
|
||||
onSome: e => SubscriptionRef.set(self.errorRef, Option.some(e)),
|
||||
onNone: () => Effect.void,
|
||||
},
|
||||
),
|
||||
}),
|
||||
Effect.uninterruptible,
|
||||
)),
|
||||
Effect.scoped,
|
||||
Effect.forkScoped,
|
||||
)
|
||||
),
|
||||
Effect.andThen(fiber => SubscriptionRef.set(self.validationFiberRef, Option.some(fiber)))
|
||||
),
|
||||
)
|
||||
|
||||
export const submit = <A, I, R, SA, SE, SR>(
|
||||
self: Form<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<Option.Option<AsyncData.AsyncData<SA, SE>>, NoSuchElementException, SR> => Effect.whenEffect(
|
||||
self.valueRef.pipe(
|
||||
Effect.andThen(identity),
|
||||
Effect.tap(Ref.set(self.submitStateRef, AsyncData.loading())),
|
||||
Effect.andThen(flow(
|
||||
self.submit,
|
||||
Effect.exit,
|
||||
Effect.map(Exit.match({
|
||||
onSuccess: a => AsyncData.success(a),
|
||||
onFailure: e => AsyncData.failure(e),
|
||||
})),
|
||||
Effect.tap(v => Ref.set(self.submitStateRef, v))
|
||||
)),
|
||||
),
|
||||
|
||||
self.canSubmitSubscribable.get,
|
||||
)
|
||||
|
||||
export namespace service {
|
||||
export interface Options<in out A, in out I, out R, in out SA = void, in out SE = A, out SR = never>
|
||||
extends make.Options<A, I, R, SA, SE, SR> {}
|
||||
}
|
||||
|
||||
export const service = <A, I = A, R = never, SA = void, SE = A, SR = never>(
|
||||
options: service.Options<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<Form<A, I, R, SA, SE, SR>, never, R | Scope.Scope> => Effect.tap(
|
||||
make(options),
|
||||
form => Effect.forkScoped(run(form)),
|
||||
)
|
||||
|
||||
export const field = <A, I, R, SA, SE, SR, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
||||
self: Form<A, I, R, SA, SE, SR>,
|
||||
path: P,
|
||||
): FormField<PropertyPath.ValueFromPath<A, P>, PropertyPath.ValueFromPath<I, P>> => new FormFieldImpl(
|
||||
pipe(
|
||||
Option.match({
|
||||
onSome: (v: A) => Option.map(PropertyPath.get(v, path), Option.some),
|
||||
onNone: () => Option.some(Option.none()),
|
||||
}),
|
||||
filter => SubscribableInternal.make({
|
||||
get: Effect.flatMap(self.valueRef, filter),
|
||||
get changes() { return Stream.flatMap(self.valueRef.changes, filter) },
|
||||
}),
|
||||
),
|
||||
|
||||
SubscriptionSubRef.makeFromPath(self.encodedValueRef, path),
|
||||
|
||||
pipe(
|
||||
Option.match({
|
||||
onSome: (v: ParseResult.ParseError) => Effect.andThen(
|
||||
ParseResult.ArrayFormatter.formatError(v),
|
||||
Array.filter(issue => PropertyPath.equivalence(issue.path, path)),
|
||||
),
|
||||
onNone: () => Effect.succeed([]),
|
||||
}),
|
||||
filter => SubscribableInternal.make({
|
||||
get: Effect.flatMap(self.errorRef.get, filter),
|
||||
get changes() { return Stream.flatMap(self.errorRef.changes, filter) },
|
||||
}),
|
||||
),
|
||||
|
||||
pipe(
|
||||
Option.isSome,
|
||||
filter => SubscribableInternal.make({
|
||||
get: Effect.map(self.validationFiberRef.get, filter),
|
||||
get changes() { return Stream.map(self.validationFiberRef.changes, filter) },
|
||||
}),
|
||||
),
|
||||
|
||||
pipe(
|
||||
AsyncData.isLoading,
|
||||
filter => SubscribableInternal.make({
|
||||
get: Effect.map(self.submitStateRef, filter),
|
||||
get changes() { return Stream.map(self.submitStateRef.changes, filter) },
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
export const FormFieldTypeId: unique symbol = Symbol.for("effect-fc/FormField")
|
||||
export type FormFieldTypeId = typeof FormFieldTypeId
|
||||
|
||||
export interface FormField<in out A, in out I = A>
|
||||
extends Pipeable.Pipeable {
|
||||
readonly [FormFieldTypeId]: FormFieldTypeId
|
||||
|
||||
readonly valueSubscribable: Subscribable.Subscribable<Option.Option<A>, NoSuchElementException>
|
||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>
|
||||
readonly issuesSubscribable: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
||||
readonly isValidatingSubscribable: Subscribable.Subscribable<boolean>
|
||||
readonly isSubmittingSubscribable: Subscribable.Subscribable<boolean>
|
||||
}
|
||||
|
||||
class FormFieldImpl<in out A, in out I = A>
|
||||
extends Pipeable.Class() implements FormField<A, I> {
|
||||
readonly [FormFieldTypeId]: FormFieldTypeId = FormFieldTypeId
|
||||
|
||||
constructor(
|
||||
readonly valueSubscribable: Subscribable.Subscribable<Option.Option<A>, NoSuchElementException>,
|
||||
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
|
||||
readonly issuesSubscribable: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
|
||||
readonly isValidatingSubscribable: Subscribable.Subscribable<boolean>,
|
||||
readonly isSubmittingSubscribable: Subscribable.Subscribable<boolean>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export namespace useForm {
|
||||
export interface Options<in out A, in out I, out R, in out SA = void, in out SE = A, out SR = never>
|
||||
extends make.Options<A, I, R, SA, SE, SR> {}
|
||||
}
|
||||
|
||||
export const useForm: {
|
||||
<A, I = A, R = never, SA = void, SE = A, SR = never>(
|
||||
options: make.Options<A, I, R, SA, SE, SR>,
|
||||
deps: React.DependencyList,
|
||||
): Effect.Effect<Form<A, I, R, SA, SE, SR>, never, R>
|
||||
} = Effect.fnUntraced(function* <A, I = A, R = never, SA = void, SE = A, SR = never>(
|
||||
options: make.Options<A, I, R, SA, SE, SR>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
const form = yield* Hooks.useMemo(() => make(options), deps)
|
||||
yield* Hooks.useFork(() => run(form), [form])
|
||||
return form
|
||||
})
|
||||
|
||||
export const useSubmit = <A, I, R, SA, SE, SR>(
|
||||
self: Form<A, I, R, SA, SE, SR>
|
||||
): Effect.Effect<
|
||||
() => Promise<Option.Option<AsyncData.AsyncData<SA, SE>>>,
|
||||
never,
|
||||
SR
|
||||
> => Hooks.useCallbackPromise(() => submit(self), [self])
|
||||
|
||||
export const useField = <A, I, R, SA, SE, SR, const P extends PropertyPath.Paths<NoInfer<I>>>(
|
||||
self: Form<A, I, R, SA, SE, SR>,
|
||||
path: P,
|
||||
): FormField<
|
||||
PropertyPath.ValueFromPath<A, P>,
|
||||
PropertyPath.ValueFromPath<I, P>
|
||||
> => React.useMemo(() => field(self, path), [self, ...path])
|
||||
|
||||
export namespace useInput {
|
||||
export interface Options {
|
||||
readonly debounce?: Duration.DurationInput
|
||||
}
|
||||
|
||||
export interface Result<T> {
|
||||
readonly value: T
|
||||
readonly setValue: React.Dispatch<React.SetStateAction<T>>
|
||||
}
|
||||
}
|
||||
|
||||
export const useInput: {
|
||||
<A, I>(
|
||||
field: FormField<A, I>,
|
||||
options?: useInput.Options,
|
||||
): Effect.Effect<useInput.Result<I>, NoSuchElementException>
|
||||
} = Effect.fnUntraced(function* <A, I>(
|
||||
field: FormField<A, I>,
|
||||
options?: useInput.Options,
|
||||
) {
|
||||
const internalValueRef = yield* Hooks.useMemo(() => Effect.andThen(field.encodedValueRef, SubscriptionRef.make), [field])
|
||||
const [value, setValue] = yield* Hooks.useRefState(internalValueRef)
|
||||
|
||||
yield* Hooks.useFork(() => Effect.all([
|
||||
Stream.runForEach(
|
||||
Stream.drop(field.encodedValueRef, 1),
|
||||
upstreamEncodedValue => Effect.whenEffect(
|
||||
Ref.set(internalValueRef, upstreamEncodedValue),
|
||||
Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
|
||||
),
|
||||
),
|
||||
|
||||
Stream.runForEach(
|
||||
internalValueRef.changes.pipe(
|
||||
Stream.drop(1),
|
||||
Stream.changesWith(Equal.equivalence()),
|
||||
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
||||
),
|
||||
internalValue => Ref.set(field.encodedValueRef, internalValue),
|
||||
),
|
||||
], { concurrency: "unbounded" }), [field, internalValueRef, options?.debounce])
|
||||
|
||||
return { value, setValue }
|
||||
})
|
||||
|
||||
export namespace useOptionalInput {
|
||||
export interface Options<I> extends useInput.Options {
|
||||
readonly defaultValue: I
|
||||
}
|
||||
|
||||
export interface Result<T> extends useInput.Result<T> {
|
||||
readonly enabled: boolean
|
||||
readonly setEnabled: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
}
|
||||
|
||||
export const useOptionalInput: {
|
||||
<A, I>(
|
||||
field: FormField<A, Option.Option<I>>,
|
||||
options: useOptionalInput.Options<I>,
|
||||
): Effect.Effect<useOptionalInput.Result<I>, NoSuchElementException>
|
||||
} = Effect.fnUntraced(function* <A, I>(
|
||||
field: FormField<A, Option.Option<I>>,
|
||||
options: useOptionalInput.Options<I>,
|
||||
) {
|
||||
const [enabledRef, internalValueRef] = yield* Hooks.useMemo(() => Effect.andThen(
|
||||
field.encodedValueRef,
|
||||
Option.match({
|
||||
onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]),
|
||||
onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]),
|
||||
}),
|
||||
), [field])
|
||||
|
||||
const [enabled, setEnabled] = yield* Hooks.useRefState(enabledRef)
|
||||
const [value, setValue] = yield* Hooks.useRefState(internalValueRef)
|
||||
|
||||
yield* Hooks.useFork(() => Effect.all([
|
||||
Stream.runForEach(
|
||||
Stream.drop(field.encodedValueRef, 1),
|
||||
|
||||
upstreamEncodedValue => Effect.whenEffect(
|
||||
Option.match(upstreamEncodedValue, {
|
||||
onSome: v => Effect.andThen(
|
||||
Ref.set(enabledRef, true),
|
||||
Ref.set(internalValueRef, v),
|
||||
),
|
||||
onNone: () => Effect.andThen(
|
||||
Ref.set(enabledRef, false),
|
||||
Ref.set(internalValueRef, options.defaultValue),
|
||||
),
|
||||
}),
|
||||
|
||||
Effect.andThen(
|
||||
Effect.all([enabledRef, internalValueRef]),
|
||||
([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Stream.runForEach(
|
||||
enabledRef.changes.pipe(
|
||||
Stream.zipLatest(internalValueRef.changes),
|
||||
Stream.drop(1),
|
||||
Stream.changesWith(Equal.equivalence()),
|
||||
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
||||
),
|
||||
([enabled, internalValue]) => Ref.set(field.encodedValueRef, enabled ? Option.some(internalValue) : Option.none()),
|
||||
),
|
||||
], { concurrency: "unbounded" }), [field, enabledRef, internalValueRef, options.debounce])
|
||||
|
||||
return { enabled, setEnabled, value, setValue }
|
||||
})
|
||||
@@ -12,5 +12,5 @@ export * from "./useRefFromState.js"
|
||||
export * from "./useRefState.js"
|
||||
export * from "./useScope.js"
|
||||
export * from "./useStreamFromReactiveValues.js"
|
||||
export * from "./useSubscribe.js"
|
||||
export * from "./useSubscribables.js"
|
||||
export * from "./useSubscribeStream.js"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCallbackSync } from "../useCallbackSync.js"
|
||||
import { useFork } from "../useFork.js"
|
||||
import { useOnce } from "../useOnce.js"
|
||||
import { useRefState } from "../useRefState.js"
|
||||
import { useSubscribe } from "../useSubscribe.js"
|
||||
import { useSubscribables } from "../useSubscribables.js"
|
||||
|
||||
|
||||
export namespace useOptionalInput {
|
||||
@@ -101,7 +101,7 @@ export const useOptionalInput: {
|
||||
[options.schema, options.ref, internalRef, enabledRef],
|
||||
)
|
||||
|
||||
const [enabled] = yield* useSubscribe(enabledRef)
|
||||
const [enabled] = yield* useSubscribables(enabledRef)
|
||||
const [value, setValue] = yield* useRefState(internalRef)
|
||||
return { value, setValue, enabled, setEnabled, error }
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useFork } from "./useFork.js"
|
||||
import { useOnce } from "./useOnce.js"
|
||||
|
||||
|
||||
export const useSubscribe: {
|
||||
export const useSubscribables: {
|
||||
<const T extends readonly Subscribable.Subscribable<any, any, any>[]>(
|
||||
...elements: T
|
||||
): Effect.Effect<
|
||||
@@ -1,4 +1,5 @@
|
||||
export * as Component from "./Component.js"
|
||||
export * as Form from "./Form.js"
|
||||
export * as Memo from "./Memo.js"
|
||||
export * as ReactRuntime from "./ReactRuntime.js"
|
||||
export * as Suspense from "./Suspense.js"
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { Array, Function, Option, Predicate } from "effect"
|
||||
import { Array, Equivalence, Function, Option, Predicate } from "effect"
|
||||
|
||||
|
||||
export type PropertyPath = readonly PropertyKey[]
|
||||
|
||||
type Prev = readonly [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
|
||||
export type Paths<T, D extends number = 5, Seen = never> = [] | (
|
||||
D extends never ? [] :
|
||||
T extends Seen ? [] :
|
||||
T extends readonly any[] ? ArrayPaths<T, D, Seen | T> :
|
||||
T extends object ? ObjectPaths<T, D, Seen | T> :
|
||||
export type Paths<T, D extends number = 5, Seen = never> = readonly [] | (
|
||||
D extends never ? readonly [] :
|
||||
T extends Seen ? readonly [] :
|
||||
T extends readonly any[] ? {
|
||||
[K in keyof T as K extends number ? K : never]:
|
||||
| readonly [K]
|
||||
| readonly [K, ...Paths<T[K], Prev[D], Seen | T>]
|
||||
} extends infer O
|
||||
? O[keyof O]
|
||||
: never
|
||||
:
|
||||
T extends object ? {
|
||||
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
||||
NonNullable<T[K]> extends infer V
|
||||
? readonly [K] | readonly [K, ...Paths<V, Prev[D], Seen>]
|
||||
: never
|
||||
} extends infer O
|
||||
? O[keyof O]
|
||||
: never
|
||||
:
|
||||
never
|
||||
)
|
||||
|
||||
export type ArrayPaths<T extends readonly any[], D extends number, Seen> = {
|
||||
[K in keyof T as K extends number ? K : never]:
|
||||
| [K]
|
||||
| [K, ...Paths<T[K], Prev[D], Seen>]
|
||||
} extends infer O
|
||||
? O[keyof O]
|
||||
: never
|
||||
|
||||
export type ObjectPaths<T extends object, D extends number, Seen> = {
|
||||
[K in keyof T as K extends string | number | symbol ? K : never]-?:
|
||||
NonNullable<T[K]> extends infer V
|
||||
? [K] | [K, ...Paths<V, Prev[D], Seen>]
|
||||
: never
|
||||
} extends infer O
|
||||
? O[keyof O]
|
||||
: never
|
||||
|
||||
export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer Tail]
|
||||
export type ValueFromPath<T, P extends readonly any[]> = P extends readonly [infer Head, ...infer Tail]
|
||||
? Head extends keyof T
|
||||
? ValueFromPath<T[Head], Tail>
|
||||
: T extends readonly any[]
|
||||
@@ -38,8 +38,8 @@ export type ValueFromPath<T, P extends any[]> = P extends [infer Head, ...infer
|
||||
: never
|
||||
: T
|
||||
|
||||
export type AnyPath = readonly PropertyKey[]
|
||||
|
||||
export const equivalence: Equivalence.Equivalence<PropertyPath> = Equivalence.array(Equivalence.strict())
|
||||
|
||||
export const unsafeGet: {
|
||||
<T, const P extends Paths<T>>(path: P): (self: T) => ValueFromPath<T, P>
|
||||
@@ -64,16 +64,16 @@ export const get: {
|
||||
)
|
||||
|
||||
export const immutableSet: {
|
||||
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => ValueFromPath<T, P>
|
||||
<T, const P extends Paths<T>>(path: P, value: ValueFromPath<T, P>): (self: T) => Option.Option<T>
|
||||
<T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T>
|
||||
} = Function.dual(3, <T, const P extends Paths<T>>(self: T, path: P, value: ValueFromPath<T, P>): Option.Option<T> => {
|
||||
const key = Array.head(path as AnyPath)
|
||||
const key = Array.head(path as PropertyPath)
|
||||
if (Option.isNone(key))
|
||||
return Option.some(value as T)
|
||||
if (!Predicate.hasProperty(self, key.value))
|
||||
return Option.none()
|
||||
|
||||
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as AnyPath)), value)
|
||||
const child = immutableSet<any, any>(self[key.value], Option.getOrThrow(Array.tail(path as PropertyPath)), value)
|
||||
if (Option.isNone(child))
|
||||
return child
|
||||
|
||||
|
||||
37
packages/effect-fc/src/types/Schema.ts
Normal file
37
packages/effect-fc/src/types/Schema.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Array, Function, Option, Predicate, Schema } from "effect"
|
||||
import type { Simplify } from "effect/Types"
|
||||
|
||||
|
||||
export type SchemaFromPath<S, P extends readonly any[]> = S extends Schema.Schema.Any
|
||||
? P extends [infer Head, ...infer Tail]
|
||||
? Head extends keyof S["Type"]
|
||||
? (
|
||||
S extends Schema.TupleType<infer Elements, infer Rest> ? (
|
||||
Head extends keyof Elements ? SchemaFromPath<Elements[Head], Tail> :
|
||||
Head extends keyof Rest ? SchemaFromPath<Rest, Tail> :
|
||||
never
|
||||
) :
|
||||
S extends Schema.Array$<infer Value> ? SchemaFromPath<Value, Tail> :
|
||||
S extends Schema.Struct<infer Fields> ? SchemaFromPath<Fields[Head], Tail> :
|
||||
never
|
||||
)
|
||||
: never
|
||||
: S
|
||||
: never
|
||||
|
||||
const TestSchema = Schema.Struct({
|
||||
allUsers: Schema.Array(Schema.Struct({
|
||||
name: Schema.String
|
||||
})),
|
||||
|
||||
admins: Schema.Tuple(
|
||||
Schema.Struct({
|
||||
name: Schema.Literal("Gneugneu")
|
||||
}),
|
||||
Schema.Struct({
|
||||
name: Schema.Literal("AAAAYA")
|
||||
}),
|
||||
),
|
||||
})
|
||||
type S = SchemaFromPath<typeof TestSchema, ["admins", 0, "name"]>
|
||||
type T = number extends keyof typeof TestSchema.fields.admins.elements ? true : false
|
||||
@@ -78,7 +78,7 @@ extends Effectable.Class<A> implements SubscriptionSubRef<A, B> {
|
||||
return Effect.Do.pipe(
|
||||
Effect.bind("b", (): Effect.Effect<Effect.Effect.Success<B>> => this.parent),
|
||||
Effect.bind("ca", ({ b }) => f(this.getter(b))),
|
||||
Effect.tap(({ b, ca: [, a] }) => Ref.set(this.parent, this.setter(b, a))),
|
||||
Effect.tap(({ b, ca: [, a] }) => SubscriptionRef.set(this.parent, this.setter(b, a))),
|
||||
Effect.map(({ ca: [c] }) => c),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"globals": "^16.3.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"type-fest": "^4.41.0",
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"vite": "^7.1.3"
|
||||
},
|
||||
|
||||
46
packages/example/src/lib/form/TextFieldFormInput.tsx
Normal file
46
packages/example/src/lib/form/TextFieldFormInput.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Callout, Flex, Spinner, TextField } from "@radix-ui/themes"
|
||||
import { Array, Option } from "effect"
|
||||
import { Component, Form } from "effect-fc"
|
||||
import { useSubscribables } from "effect-fc/hooks"
|
||||
|
||||
|
||||
export interface TextFieldFormInputProps
|
||||
extends TextField.RootProps, Form.useInput.Options {
|
||||
readonly field: Form.FormField<any, string>
|
||||
}
|
||||
|
||||
export class TextFieldFormInput extends Component.makeUntraced("TextFieldFormInput")(function*(props: TextFieldFormInputProps) {
|
||||
const { value, setValue } = yield* Form.useInput(props.field, props)
|
||||
const [issues, isValidating, isSubmitting] = yield* useSubscribables(
|
||||
props.field.issuesSubscribable,
|
||||
props.field.isValidatingSubscribable,
|
||||
props.field.isSubmittingSubscribable,
|
||||
)
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="1">
|
||||
<TextField.Root
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
{...props}
|
||||
>
|
||||
{isValidating &&
|
||||
<TextField.Slot side="right">
|
||||
<Spinner />
|
||||
</TextField.Slot>
|
||||
}
|
||||
</TextField.Root>
|
||||
|
||||
{Option.match(Array.head(issues), {
|
||||
onSome: issue => (
|
||||
<Callout.Root>
|
||||
<Callout.Text>{issue.message}</Callout.Text>
|
||||
</Callout.Root>
|
||||
),
|
||||
|
||||
onNone: () => <></>,
|
||||
})}
|
||||
</Flex>
|
||||
)
|
||||
}) {}
|
||||
@@ -15,7 +15,7 @@ export const TextAreaInput = <A, R>(options: {
|
||||
React.JSX.Element,
|
||||
ParseResult.ParseError,
|
||||
R
|
||||
> => Component.makeUntraced(function* TextFieldInput(props) {
|
||||
> => Component.makeUntraced("TextFieldInput")(function* TextFieldInput(props) {
|
||||
const input = yield* useInput({ ...options, ...props })
|
||||
const issue = React.useMemo(() => input.error.pipe(
|
||||
Option.map(ParseResult.ArrayFormatter.formatErrorSync),
|
||||
|
||||
@@ -18,7 +18,7 @@ export const TextFieldInput = <A, R, O extends boolean = false>(options: {
|
||||
readonly optional?: O
|
||||
readonly schema: Schema.Schema<A, string, R>
|
||||
readonly equivalence?: Equivalence.Equivalence<A>
|
||||
}) => Component.makeUntraced(function* TextFieldInput(props: O extends true
|
||||
}) => Component.makeUntraced("TextFieldInput")(function* TextFieldInput(props: O extends true
|
||||
? TextFieldOptionalInputProps<A, R>
|
||||
: TextFieldInputProps<A, R>
|
||||
) {
|
||||
|
||||
@@ -9,12 +9,18 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as FormRouteImport } from './routes/form'
|
||||
import { Route as BlankRouteImport } from './routes/blank'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as DevMemoRouteImport } from './routes/dev/memo'
|
||||
import { Route as DevInputRouteImport } from './routes/dev/input'
|
||||
import { Route as DevAsyncRenderingRouteImport } from './routes/dev/async-rendering'
|
||||
|
||||
const FormRoute = FormRouteImport.update({
|
||||
id: '/form',
|
||||
path: '/form',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const BlankRoute = BlankRouteImport.update({
|
||||
id: '/blank',
|
||||
path: '/blank',
|
||||
@@ -44,6 +50,7 @@ const DevAsyncRenderingRoute = DevAsyncRenderingRouteImport.update({
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/input': typeof DevInputRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
@@ -51,6 +58,7 @@ export interface FileRoutesByFullPath {
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/input': typeof DevInputRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
@@ -59,6 +67,7 @@ export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/blank': typeof BlankRoute
|
||||
'/form': typeof FormRoute
|
||||
'/dev/async-rendering': typeof DevAsyncRenderingRoute
|
||||
'/dev/input': typeof DevInputRoute
|
||||
'/dev/memo': typeof DevMemoRoute
|
||||
@@ -68,15 +77,23 @@ export interface FileRouteTypes {
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/input'
|
||||
| '/dev/memo'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to: '/' | '/blank' | '/dev/async-rendering' | '/dev/input' | '/dev/memo'
|
||||
to:
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/input'
|
||||
| '/dev/memo'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/blank'
|
||||
| '/form'
|
||||
| '/dev/async-rendering'
|
||||
| '/dev/input'
|
||||
| '/dev/memo'
|
||||
@@ -85,6 +102,7 @@ export interface FileRouteTypes {
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
BlankRoute: typeof BlankRoute
|
||||
FormRoute: typeof FormRoute
|
||||
DevAsyncRenderingRoute: typeof DevAsyncRenderingRoute
|
||||
DevInputRoute: typeof DevInputRoute
|
||||
DevMemoRoute: typeof DevMemoRoute
|
||||
@@ -92,6 +110,13 @@ export interface RootRouteChildren {
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/form': {
|
||||
id: '/form'
|
||||
path: '/form'
|
||||
fullPath: '/form'
|
||||
preLoaderRoute: typeof FormRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/blank': {
|
||||
id: '/blank'
|
||||
path: '/blank'
|
||||
@@ -133,6 +158,7 @@ declare module '@tanstack/react-router' {
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
BlankRoute: BlankRoute,
|
||||
FormRoute: FormRoute,
|
||||
DevAsyncRenderingRoute: DevAsyncRenderingRoute,
|
||||
DevInputRoute: DevInputRoute,
|
||||
DevMemoRoute: DevMemoRoute,
|
||||
|
||||
@@ -51,7 +51,7 @@ const RouteComponent = Component.makeUntraced(function* AsyncRendering() {
|
||||
// )
|
||||
|
||||
|
||||
class AsyncComponent extends Component.makeUntraced(function* AsyncComponent() {
|
||||
class AsyncComponent extends Component.makeUntraced("AsyncComponent")(function*() {
|
||||
const SubComponentFC = yield* SubComponent
|
||||
|
||||
yield* Effect.sleep("500 millis") // Async operation
|
||||
@@ -69,7 +69,7 @@ class AsyncComponent extends Component.makeUntraced(function* AsyncComponent() {
|
||||
) {}
|
||||
class MemoizedAsyncComponent extends Memo.memo(AsyncComponent) {}
|
||||
|
||||
class SubComponent extends Component.makeUntraced(function* SubComponent() {
|
||||
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
||||
const [state] = React.useState(yield* Hooks.useOnce(() => Effect.provide(makeUuid4, GetRandomValues.CryptoRandom)))
|
||||
return <Text>{state}</Text>
|
||||
}) {}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Container } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Schema, SubscriptionRef } from "effect"
|
||||
import { Component, Memo } from "effect-fc"
|
||||
import { useInput, useOnce, useRefState } from "effect-fc/hooks"
|
||||
import { useOnce } from "effect-fc/hooks"
|
||||
|
||||
|
||||
const IntFromString = Schema.NumberFromString.pipe(Schema.int())
|
||||
@@ -12,18 +12,18 @@ const IntFromString = Schema.NumberFromString.pipe(Schema.int())
|
||||
const IntTextFieldInput = TextFieldInput({ schema: IntFromString })
|
||||
const StringTextFieldInput = TextFieldInput({ schema: Schema.String })
|
||||
|
||||
const Input = Component.makeUntraced(function* Input() {
|
||||
const Input = Component.makeUntraced("Input")(function*() {
|
||||
const IntTextFieldInputFC = yield* IntTextFieldInput
|
||||
const StringTextFieldInputFC = yield* StringTextFieldInput
|
||||
|
||||
const intRef1 = yield* useOnce(() => SubscriptionRef.make(0))
|
||||
const intRef2 = yield* useOnce(() => SubscriptionRef.make(0))
|
||||
// const intRef2 = yield* useOnce(() => SubscriptionRef.make(0))
|
||||
const stringRef = yield* useOnce(() => SubscriptionRef.make(""))
|
||||
// yield* useFork(() => Stream.runForEach(intRef1.changes, Console.log), [intRef1])
|
||||
|
||||
const input2 = yield* useInput({ schema: IntFromString, ref: intRef2 })
|
||||
// const input2 = yield* useInput({ schema: IntFromString, ref: intRef2 })
|
||||
|
||||
const [str, setStr] = yield* useRefState(stringRef)
|
||||
// const [str, setStr] = yield* useRefState(stringRef)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Component, Memo } from "effect-fc"
|
||||
import * as React from "react"
|
||||
|
||||
|
||||
const RouteComponent = Component.makeUntraced(function* RouteComponent() {
|
||||
const RouteComponent = Component.makeUntraced("RouteComponent")(function*() {
|
||||
const [value, setValue] = React.useState("")
|
||||
|
||||
return (
|
||||
@@ -25,7 +25,7 @@ const RouteComponent = Component.makeUntraced(function* RouteComponent() {
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
class SubComponent extends Component.makeUntraced(function* SubComponent() {
|
||||
class SubComponent extends Component.makeUntraced("SubComponent")(function*() {
|
||||
const id = yield* makeUuid4.pipe(Effect.provide(GetRandomValues.CryptoRandom))
|
||||
return <Text>{id}</Text>
|
||||
}) {}
|
||||
|
||||
86
packages/example/src/routes/form.tsx
Normal file
86
packages/example/src/routes/form.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { TextFieldFormInput } from "@/lib/form/TextFieldFormInput"
|
||||
import { runtime } from "@/runtime"
|
||||
import { Button, Container, Flex } from "@radix-ui/themes"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { Effect, ParseResult, Schema } from "effect"
|
||||
import { Component, Form } from "effect-fc"
|
||||
import { useContext, useSubscribables } from "effect-fc/hooks"
|
||||
|
||||
|
||||
const email = Schema.pattern<typeof Schema.String>(
|
||||
/^(?!\.)(?!.*\.\.)([A-Z0-9_+-.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i,
|
||||
|
||||
{
|
||||
identifier: "email",
|
||||
title: "email",
|
||||
message: () => "Not an email address",
|
||||
},
|
||||
)
|
||||
|
||||
const RegisterFormSchema = Schema.Struct({
|
||||
email: Schema.String.pipe(email),
|
||||
password: Schema.String.pipe(Schema.minLength(3)),
|
||||
})
|
||||
|
||||
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||
scoped: Form.service({
|
||||
schema: Schema.transformOrFail(
|
||||
Schema.encodedSchema(RegisterFormSchema),
|
||||
Schema.typeSchema(RegisterFormSchema),
|
||||
{
|
||||
decode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
|
||||
encode: v => Effect.andThen(Effect.sleep("500 millis"), ParseResult.succeed(v)),
|
||||
},
|
||||
),
|
||||
initialEncodedValue: { email: "", password: "" },
|
||||
submit: () => Effect.andThen(
|
||||
Effect.sleep("500 millis"),
|
||||
Effect.sync(() => alert("Done!")),
|
||||
),
|
||||
})
|
||||
}) {}
|
||||
|
||||
class RegisterPage extends Component.makeUntraced("RegisterPage")(function*() {
|
||||
const form = yield* RegisterForm
|
||||
const submit = yield* Form.useSubmit(form)
|
||||
const [canSubmit] = yield* useSubscribables(form.canSubmitSubscribable)
|
||||
|
||||
const TextFieldFormInputFC = yield* TextFieldFormInput
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<form onSubmit={e => {
|
||||
e.preventDefault()
|
||||
void submit()
|
||||
}}>
|
||||
<Flex direction="column" gap="2">
|
||||
<TextFieldFormInputFC
|
||||
field={Form.useField(form, ["email"])}
|
||||
debounce="200 millis"
|
||||
/>
|
||||
|
||||
<TextFieldFormInputFC
|
||||
field={Form.useField(form, ["password"])}
|
||||
debounce="200 millis"
|
||||
/>
|
||||
|
||||
<Button disabled={!canSubmit}>Submit</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
}) {}
|
||||
|
||||
|
||||
const RegisterRoute = Component.makeUntraced(function* RegisterRoute() {
|
||||
const context = yield* useContext(RegisterForm.Default, { finalizerExecutionMode: "fork" })
|
||||
const RegisterRouteFC = yield* Effect.provide(RegisterPage, context)
|
||||
|
||||
return <RegisterRouteFC />
|
||||
}).pipe(
|
||||
Component.withRuntime(runtime.context)
|
||||
)
|
||||
|
||||
export const Route = createFileRoute("/form")({
|
||||
component: RegisterRoute
|
||||
})
|
||||
@@ -9,7 +9,7 @@ import { useContext } from "effect-fc/hooks"
|
||||
|
||||
const TodosStateLive = TodosState.Default("todos")
|
||||
|
||||
const Index = Component.makeUntraced(function* Index() {
|
||||
const Index = Component.makeUntraced("Index")(function*() {
|
||||
const context = yield* useContext(TodosStateLive, { finalizerExecutionMode: "fork" })
|
||||
const TodosFC = yield* Effect.provide(Todos, context)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Box, Button, Flex, IconButton } from "@radix-ui/themes"
|
||||
import { GetRandomValues, makeUuid4 } from "@typed/id"
|
||||
import { Chunk, DateTime, Effect, Match, Option, Ref, Runtime, Schema, Stream, SubscriptionRef } from "effect"
|
||||
import { Component, Memo } from "effect-fc"
|
||||
import { useMemo, useOnce, useSubscribe } from "effect-fc/hooks"
|
||||
import { useMemo, useOnce, useSubscribables } from "effect-fc/hooks"
|
||||
import { Subscribable, SubscriptionSubRef } from "effect-fc/types"
|
||||
import { FaArrowDown, FaArrowUp } from "react-icons/fa"
|
||||
import { FaDeleteLeft } from "react-icons/fa6"
|
||||
@@ -31,7 +31,7 @@ export type TodoProps = (
|
||||
| { readonly _tag: "edit", readonly id: string }
|
||||
)
|
||||
|
||||
export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps) {
|
||||
export class Todo extends Component.makeUntraced("Todo")(function*(props: TodoProps) {
|
||||
const runtime = yield* Effect.runtime()
|
||||
const state = yield* TodosState
|
||||
|
||||
@@ -51,7 +51,7 @@ export class Todo extends Component.makeUntraced(function* Todo(props: TodoProps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
), [props._tag, props._tag === "edit" ? props.id : undefined])
|
||||
|
||||
const [index, size] = yield* useSubscribe(indexRef, state.sizeSubscribable)
|
||||
const [index, size] = yield* useSubscribables(indexRef, state.sizeSubscribable)
|
||||
|
||||
const StringTextAreaInputFC = yield* StringTextAreaInput
|
||||
const OptionalDateTimeInputFC = yield* OptionalDateTimeInput
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Container, Flex, Heading } from "@radix-ui/themes"
|
||||
import { Chunk, Console, Effect } from "effect"
|
||||
import { Component } from "effect-fc"
|
||||
import { useOnce, useSubscribe } from "effect-fc/hooks"
|
||||
import { useOnce, useSubscribables } from "effect-fc/hooks"
|
||||
import { Todo } from "./Todo"
|
||||
import { TodosState } from "./TodosState.service"
|
||||
|
||||
|
||||
export class Todos extends Component.makeUntraced(function* Todos() {
|
||||
export class Todos extends Component.makeUntraced("Todos")(function*() {
|
||||
const state = yield* TodosState
|
||||
const [todos] = yield* useSubscribe(state.ref)
|
||||
const [todos] = yield* useSubscribables(state.ref)
|
||||
|
||||
yield* useOnce(() => Effect.andThen(
|
||||
Console.log("Todos mounted"),
|
||||
|
||||
Reference in New Issue
Block a user