0.2.5 #43
@@ -1,12 +1,12 @@
|
|||||||
import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, flow, Hash, HashMap, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect"
|
import { Array, Cause, Chunk, type Context, type Duration, Effect, Equal, Exit, Fiber, flow, Hash, HashMap, identity, Option, ParseResult, Pipeable, Predicate, Ref, Schema, type Scope, Stream } from "effect"
|
||||||
import type * as React from "react"
|
import type * as React from "react"
|
||||||
import * as Component from "./Component.js"
|
import * as Component from "./Component.js"
|
||||||
|
import * as Lens from "./Lens.js"
|
||||||
import * as Mutation from "./Mutation.js"
|
import * as Mutation from "./Mutation.js"
|
||||||
import * as PropertyPath from "./PropertyPath.js"
|
import * as PropertyPath from "./PropertyPath.js"
|
||||||
import * as Result from "./Result.js"
|
import * as Result from "./Result.js"
|
||||||
import * as Subscribable from "./Subscribable.js"
|
import * as Subscribable from "./Subscribable.js"
|
||||||
import * as SubscriptionRef from "./SubscriptionRef.js"
|
import * as SubscriptionRef from "./SubscriptionRef.js"
|
||||||
import * as SubscriptionSubRef from "./SubscriptionSubRef.js"
|
|
||||||
|
|
||||||
|
|
||||||
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
|
export const FormTypeId: unique symbol = Symbol.for("@effect-fc/Form/Form")
|
||||||
@@ -26,7 +26,7 @@ extends Pipeable.Pipeable {
|
|||||||
readonly debounce: Option.Option<Duration.DurationInput>
|
readonly debounce: Option.Option<Duration.DurationInput>
|
||||||
|
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>>
|
readonly value: Subscribable.Subscribable<Option.Option<A>>
|
||||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
readonly encodedValue: Lens.Lens<I>
|
||||||
readonly error: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
|
readonly error: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
|
||||||
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>
|
readonly validationFiber: Subscribable.Subscribable<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>
|
||||||
|
|
||||||
@@ -54,10 +54,10 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
readonly autosubmit: boolean,
|
readonly autosubmit: boolean,
|
||||||
readonly debounce: Option.Option<Duration.DurationInput>,
|
readonly debounce: Option.Option<Duration.DurationInput>,
|
||||||
|
|
||||||
readonly value: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
readonly value: Lens.Lens<Option.Option<A>>,
|
||||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
readonly encodedValue: Lens.Lens<I>,
|
||||||
readonly error: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
readonly error: Lens.Lens<Option.Option<ParseResult.ParseError>>,
|
||||||
readonly validationFiber: SubscriptionRef.SubscriptionRef<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>,
|
readonly validationFiber: Lens.Lens<Option.Option<Fiber.Fiber<A, ParseResult.ParseError>>>,
|
||||||
|
|
||||||
readonly runSemaphore: Effect.Semaphore,
|
readonly runSemaphore: Effect.Semaphore,
|
||||||
readonly fieldCache: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
|
readonly fieldCache: Ref.Ref<HashMap.HashMap<FormFieldKey, FormField<unknown, unknown>>>,
|
||||||
@@ -99,7 +99,7 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity
|
Option.isSome(this.debounce) ? Stream.debounce(this.debounce.value) : identity
|
||||||
),
|
),
|
||||||
|
|
||||||
encodedValue => this.validationFiber.pipe(
|
encodedValue => Lens.get(this.validationFiber).pipe(
|
||||||
Effect.andThen(Option.match({
|
Effect.andThen(Option.match({
|
||||||
onSome: Fiber.interrupt,
|
onSome: Fiber.interrupt,
|
||||||
onNone: () => Effect.void,
|
onNone: () => Effect.void,
|
||||||
@@ -110,18 +110,18 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
exit => Effect.andThen(
|
exit => Effect.andThen(
|
||||||
Exit.matchEffect(exit, {
|
Exit.matchEffect(exit, {
|
||||||
onSuccess: v => Effect.andThen(
|
onSuccess: v => Effect.andThen(
|
||||||
Ref.set(this.value, Option.some(v)),
|
Lens.set(this.value, Option.some(v)),
|
||||||
Ref.set(this.error, Option.none()),
|
Lens.set(this.error, Option.none()),
|
||||||
),
|
),
|
||||||
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
onFailure: c => Option.match(Chunk.findFirst(Cause.failures(c), e => e._tag === "ParseError"), {
|
||||||
onSome: e => Ref.set(this.error, Option.some(e)),
|
onSome: e => Lens.set(this.error, Option.some(e)),
|
||||||
onNone: () => Effect.void,
|
onNone: () => Effect.void,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
Ref.set(this.validationFiber, Option.none()),
|
Lens.set(this.validationFiber, Option.none()),
|
||||||
),
|
),
|
||||||
)).pipe(
|
)).pipe(
|
||||||
Effect.tap(fiber => Ref.set(this.validationFiber, Option.some(fiber))),
|
Effect.tap(fiber => Lens.set(this.validationFiber, Option.some(fiber))),
|
||||||
Effect.andThen(Fiber.join),
|
Effect.andThen(Fiber.join),
|
||||||
Effect.andThen(value => this.autosubmit
|
Effect.andThen(value => this.autosubmit
|
||||||
? Effect.asVoid(Effect.forkScoped(this.submitValue(value)))
|
? Effect.asVoid(Effect.forkScoped(this.submitValue(value)))
|
||||||
@@ -136,7 +136,7 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
|
get submit(): Effect.Effect<Option.Option<Result.Final<MA, ME, MP>>, Cause.NoSuchElementException> {
|
||||||
return this.value.pipe(
|
return Lens.get(this.value).pipe(
|
||||||
Effect.andThen(identity),
|
Effect.andThen(identity),
|
||||||
Effect.andThen(value => this.submitValue(value)),
|
Effect.andThen(value => this.submitValue(value)),
|
||||||
)
|
)
|
||||||
@@ -153,7 +153,7 @@ extends Pipeable.Class() implements Form<A, I, R, MA, ME, MR, MP> {
|
|||||||
e => e._tag === "ParseError",
|
e => e._tag === "ParseError",
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
onSome: e => Ref.set(this.error, Option.some(e)),
|
onSome: e => Lens.set(this.error, Option.some(e)),
|
||||||
onNone: () => Effect.void,
|
onNone: () => Effect.void,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -193,10 +193,10 @@ export const make = Effect.fnUntraced(function* <A, I = A, R = never, MA = void,
|
|||||||
options.autosubmit ?? false,
|
options.autosubmit ?? false,
|
||||||
Option.fromNullable(options.debounce),
|
Option.fromNullable(options.debounce),
|
||||||
|
|
||||||
yield* SubscriptionRef.make(Option.none<A>()),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<A>())),
|
||||||
yield* SubscriptionRef.make(options.initialEncodedValue),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(options.initialEncodedValue)),
|
||||||
yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>())),
|
||||||
yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>()),
|
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<Fiber.Fiber<A, ParseResult.ParseError>>())),
|
||||||
|
|
||||||
yield* Effect.makeSemaphore(1),
|
yield* Effect.makeSemaphore(1),
|
||||||
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
|
yield* Ref.make(HashMap.empty<FormFieldKey, FormField<unknown, unknown>>()),
|
||||||
@@ -228,7 +228,7 @@ extends Pipeable.Pipeable {
|
|||||||
readonly [FormFieldTypeId]: FormFieldTypeId
|
readonly [FormFieldTypeId]: FormFieldTypeId
|
||||||
|
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>
|
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>
|
||||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>
|
readonly encodedValue: Lens.Lens<I>
|
||||||
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
||||||
readonly isValidating: Subscribable.Subscribable<boolean>
|
readonly isValidating: Subscribable.Subscribable<boolean>
|
||||||
readonly isSubmitting: Subscribable.Subscribable<boolean>
|
readonly isSubmitting: Subscribable.Subscribable<boolean>
|
||||||
@@ -240,7 +240,7 @@ extends Pipeable.Class() implements FormField<A, I> {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>,
|
readonly value: Subscribable.Subscribable<Option.Option<A>, Cause.NoSuchElementException>,
|
||||||
readonly encodedValue: SubscriptionRef.SubscriptionRef<I>,
|
readonly encodedValue: Lens.Lens<I>,
|
||||||
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
|
readonly issues: Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>,
|
||||||
readonly isValidating: Subscribable.Subscribable<boolean>,
|
readonly isValidating: Subscribable.Subscribable<boolean>,
|
||||||
readonly isSubmitting: Subscribable.Subscribable<boolean>,
|
readonly isSubmitting: Subscribable.Subscribable<boolean>,
|
||||||
@@ -276,7 +276,7 @@ export const makeFormField = <A, I, R, MA, ME, MR, MP, const P extends PropertyP
|
|||||||
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
|
onSome: v => Option.map(PropertyPath.get(v, path), Option.some),
|
||||||
onNone: () => Option.some(Option.none()),
|
onNone: () => Option.some(Option.none()),
|
||||||
})),
|
})),
|
||||||
SubscriptionSubRef.makeFromPath(self.encodedValue, path),
|
Lens.map(self.encodedValue, a => Option.getOrThrow(PropertyPath.get(a, path)), (a, b) => Option.getOrThrow(PropertyPath.immutableSet(a, path, b))),
|
||||||
Subscribable.mapEffect(self.error, Option.match({
|
Subscribable.mapEffect(self.error, Option.match({
|
||||||
onSome: flow(
|
onSome: flow(
|
||||||
ParseResult.ArrayFormatter.formatError,
|
ParseResult.ArrayFormatter.formatError,
|
||||||
@@ -305,29 +305,35 @@ export const useInput = Effect.fnUntraced(function* <A, I>(
|
|||||||
field: FormField<A, I>,
|
field: FormField<A, I>,
|
||||||
options?: useInput.Options,
|
options?: useInput.Options,
|
||||||
): Effect.fn.Return<useInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
|
): Effect.fn.Return<useInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
|
||||||
const internalValueRef = yield* Component.useOnChange(() => Effect.tap(
|
const internalValueLens = yield* Component.useOnChange(() => Effect.gen(function*() {
|
||||||
Effect.andThen(field.encodedValue, SubscriptionRef.make),
|
const internalValueLens = yield* Lens.get(field.encodedValue).pipe(
|
||||||
internalValueRef => Effect.forkScoped(Effect.all([
|
Effect.flatMap(SubscriptionRef.make),
|
||||||
|
Effect.map(Lens.fromSubscriptionRef),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Effect.forkScoped(Effect.all([
|
||||||
Stream.runForEach(
|
Stream.runForEach(
|
||||||
Stream.drop(field.encodedValue, 1),
|
Stream.drop(field.encodedValue.changes, 1),
|
||||||
upstreamEncodedValue => Effect.whenEffect(
|
upstreamEncodedValue => Effect.whenEffect(
|
||||||
Ref.set(internalValueRef, upstreamEncodedValue),
|
Lens.set(internalValueLens, upstreamEncodedValue),
|
||||||
Effect.andThen(internalValueRef, internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
|
Effect.andThen(Lens.get(internalValueLens), internalValue => !Equal.equals(upstreamEncodedValue, internalValue)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Stream.runForEach(
|
Stream.runForEach(
|
||||||
internalValueRef.changes.pipe(
|
internalValueLens.changes.pipe(
|
||||||
Stream.drop(1),
|
Stream.drop(1),
|
||||||
Stream.changesWith(Equal.equivalence()),
|
Stream.changesWith(Equal.equivalence()),
|
||||||
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
||||||
),
|
),
|
||||||
internalValue => Ref.set(field.encodedValue, internalValue),
|
internalValue => Lens.set(field.encodedValue, internalValue),
|
||||||
),
|
),
|
||||||
], { concurrency: "unbounded" })),
|
], { concurrency: "unbounded" }))
|
||||||
), [field, options?.debounce])
|
|
||||||
|
|
||||||
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef)
|
return internalValueLens
|
||||||
|
}), [field, options?.debounce])
|
||||||
|
|
||||||
|
const [value, setValue] = yield* Lens.useState(internalValueLens)
|
||||||
return { value, setValue }
|
return { value, setValue }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -346,51 +352,59 @@ export const useOptionalInput = Effect.fnUntraced(function* <A, I>(
|
|||||||
field: FormField<A, Option.Option<I>>,
|
field: FormField<A, Option.Option<I>>,
|
||||||
options: useOptionalInput.Options<I>,
|
options: useOptionalInput.Options<I>,
|
||||||
): Effect.fn.Return<useOptionalInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
|
): Effect.fn.Return<useOptionalInput.Success<I>, Cause.NoSuchElementException, Scope.Scope> {
|
||||||
const [enabledRef, internalValueRef] = yield* Component.useOnChange(() => Effect.tap(
|
const [enabledLens, internalValueLens] = yield* Component.useOnChange(() => Effect.gen(function*() {
|
||||||
Effect.andThen(
|
const [enabledLens, internalValueLens] = yield* Effect.flatMap(
|
||||||
field.encodedValue,
|
Lens.get(field.encodedValue),
|
||||||
Option.match({
|
Option.match({
|
||||||
onSome: v => Effect.all([SubscriptionRef.make(true), SubscriptionRef.make(v)]),
|
onSome: v => Effect.all([
|
||||||
onNone: () => Effect.all([SubscriptionRef.make(false), SubscriptionRef.make(options.defaultValue)]),
|
Effect.map(SubscriptionRef.make(true), Lens.fromSubscriptionRef),
|
||||||
|
Effect.map(SubscriptionRef.make(v), Lens.fromSubscriptionRef),
|
||||||
|
]),
|
||||||
|
onNone: () => Effect.all([
|
||||||
|
Effect.map(SubscriptionRef.make(false), Lens.fromSubscriptionRef),
|
||||||
|
Effect.map(SubscriptionRef.make(options.defaultValue), Lens.fromSubscriptionRef),
|
||||||
|
]),
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
|
|
||||||
([enabledRef, internalValueRef]) => Effect.forkScoped(Effect.all([
|
yield* Effect.forkScoped(Effect.all([
|
||||||
Stream.runForEach(
|
Stream.runForEach(
|
||||||
Stream.drop(field.encodedValue, 1),
|
Stream.drop(field.encodedValue.changes, 1),
|
||||||
|
|
||||||
upstreamEncodedValue => Effect.whenEffect(
|
upstreamEncodedValue => Effect.whenEffect(
|
||||||
Option.match(upstreamEncodedValue, {
|
Option.match(upstreamEncodedValue, {
|
||||||
onSome: v => Effect.andThen(
|
onSome: v => Effect.andThen(
|
||||||
Ref.set(enabledRef, true),
|
Lens.set(enabledLens, true),
|
||||||
Ref.set(internalValueRef, v),
|
Lens.set(internalValueLens, v),
|
||||||
),
|
),
|
||||||
onNone: () => Effect.andThen(
|
onNone: () => Effect.andThen(
|
||||||
Ref.set(enabledRef, false),
|
Lens.set(enabledLens, false),
|
||||||
Ref.set(internalValueRef, options.defaultValue),
|
Lens.set(internalValueLens, options.defaultValue),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Effect.andThen(
|
Effect.andThen(
|
||||||
Effect.all([enabledRef, internalValueRef]),
|
Effect.all([Lens.get(enabledLens), Lens.get(internalValueLens)]),
|
||||||
([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
([enabled, internalValue]) => !Equal.equals(upstreamEncodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Stream.runForEach(
|
Stream.runForEach(
|
||||||
enabledRef.changes.pipe(
|
enabledLens.changes.pipe(
|
||||||
Stream.zipLatest(internalValueRef.changes),
|
Stream.zipLatest(internalValueLens.changes),
|
||||||
Stream.drop(1),
|
Stream.drop(1),
|
||||||
Stream.changesWith(Equal.equivalence()),
|
Stream.changesWith(Equal.equivalence()),
|
||||||
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
options?.debounce ? Stream.debounce(options.debounce) : identity,
|
||||||
),
|
),
|
||||||
([enabled, internalValue]) => Ref.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
([enabled, internalValue]) => Lens.set(field.encodedValue, enabled ? Option.some(internalValue) : Option.none()),
|
||||||
),
|
),
|
||||||
], { concurrency: "unbounded" })),
|
], { concurrency: "unbounded" }))
|
||||||
), [field, options.debounce])
|
|
||||||
|
|
||||||
const [enabled, setEnabled] = yield* SubscriptionRef.useSubscriptionRefState(enabledRef)
|
return [enabledLens, internalValueLens] as const
|
||||||
const [value, setValue] = yield* SubscriptionRef.useSubscriptionRefState(internalValueRef)
|
}), [field, options.debounce])
|
||||||
|
|
||||||
|
const [enabled, setEnabled] = yield* Lens.useState(enabledLens)
|
||||||
|
const [value, setValue] = yield* Lens.useState(internalValueLens)
|
||||||
return { enabled, setEnabled, value, setValue }
|
return { enabled, setEnabled, value, setValue }
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user