0.1.4 #5
@@ -1,5 +1,7 @@
|
|||||||
import { Array, Effect, Option, ParseResult, Pipeable, Schema, Stream, Subscribable, SubscriptionRef } from "effect"
|
import { Array, Effect, Either, Equivalence, flow, Option, ParseResult, Pipeable, Schema, Stream, Subscribable, SubscriptionRef } from "effect"
|
||||||
|
import type { NoSuchElementException } from "effect/Cause"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { Hooks } from "./hooks/index.js"
|
||||||
import { PropertyPath, Subscribable as SubscribableInternal, SubscriptionSubRef } from "./types/index.js"
|
import { PropertyPath, Subscribable as SubscribableInternal, SubscriptionSubRef } from "./types/index.js"
|
||||||
|
|
||||||
|
|
||||||
@@ -9,45 +11,134 @@ export type TypeId = typeof TypeId
|
|||||||
export interface Form<in out A, in out I = A, out R = never>
|
export interface Form<in out A, in out I = A, out R = never>
|
||||||
extends Pipeable.Pipeable {
|
extends Pipeable.Pipeable {
|
||||||
readonly schema: Schema.Schema<A, I, R>
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
readonly valueRef: SubscriptionRef.SubscriptionRef<A>
|
readonly latestValueSubscribable: Subscribable.Subscribable<NoInfer<A>>
|
||||||
readonly errorSubscribable: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
|
|
||||||
|
|
||||||
useRef<P extends PropertyPath.Paths<A>>(path: P): SubscriptionRef.SubscriptionRef<PropertyPath.ValueFromPath<A, P>>
|
readonly fieldLatestValueSubscribable: <P extends PropertyPath.Paths<NoInfer<A>>>(
|
||||||
useIssuesSubscribable(path: PropertyPath.Paths<A>): Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
path: P
|
||||||
|
) => Effect.Effect<Subscribable.Subscribable<PropertyPath.ValueFromPath<NoInfer<A>, P>, NoSuchElementException>>
|
||||||
|
readonly fieldIssuesSubscribable: (
|
||||||
|
path: PropertyPath.Paths<NoInfer<A>>
|
||||||
|
) => Effect.Effect<Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>>
|
||||||
|
|
||||||
|
useInput<P extends PropertyPath.Paths<NoInfer<I>>>(
|
||||||
|
path: P
|
||||||
|
): Effect.Effect<
|
||||||
|
Form.useInput.Result<PropertyPath.ValueFromPath<NoInfer<I>, P>>,
|
||||||
|
ParseResult.ParseError | NoSuchElementException,
|
||||||
|
R
|
||||||
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace Form {
|
||||||
|
export namespace useInput {
|
||||||
|
export interface Result<I> {
|
||||||
|
readonly value: I
|
||||||
|
readonly setValue: React.Dispatch<React.SetStateAction<I>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class FormImpl<in out A, in out I, out R>
|
class FormImpl<in out A, in out I, out R>
|
||||||
extends Pipeable.Class() implements Form<A, I, R> {
|
extends Pipeable.Class() implements Form<A, I, R> {
|
||||||
readonly [TypeId]: TypeId = TypeId
|
readonly [TypeId]: TypeId = TypeId
|
||||||
|
readonly latestValueSubscribable: Subscribable.Subscribable<NoInfer<A>>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly schema: Schema.Schema<A, I, R>,
|
readonly schema: Schema.Schema<A, I, R>,
|
||||||
readonly valueRef: SubscriptionRef.SubscriptionRef<A>,
|
readonly latestValueRef: SubscriptionRef.SubscriptionRef<NoInfer<A>>,
|
||||||
readonly errorSubscribable: Subscribable.Subscribable<Option.Option<ParseResult.ParseError>>
|
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
||||||
|
|
||||||
|
readonly fieldLatestValueSubscribable: <P extends PropertyPath.Paths<NoInfer<A>>>(
|
||||||
|
path: P
|
||||||
|
) => Effect.Effect<Subscribable.Subscribable<PropertyPath.ValueFromPath<NoInfer<A>, P>, NoSuchElementException>>,
|
||||||
|
readonly fieldIssuesSubscribable: (
|
||||||
|
path: PropertyPath.Paths<NoInfer<A>>
|
||||||
|
) => Effect.Effect<Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>>,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
this.latestValueSubscribable = latestValueRef
|
||||||
}
|
}
|
||||||
|
|
||||||
useRef<P extends PropertyPath.Paths<A>>(path: P) {
|
useInput<P extends PropertyPath.Paths<I>>(path: P) {
|
||||||
return React.useMemo(() => SubscriptionSubRef.makeFromPath(this.valueRef, path), [this.valueRef, ...path])
|
const self = this
|
||||||
}
|
return Effect.gen(function*() {
|
||||||
|
const issuesSubscribable = yield* self.fieldIssuesSubscribable(path)
|
||||||
|
const internalValueRef = yield* Hooks.useMemo(() => self.latestValueRef.pipe(
|
||||||
|
Effect.andThen(Schema.encode(self.schema)),
|
||||||
|
Effect.andThen(PropertyPath.get(path)),
|
||||||
|
Effect.andThen(SubscriptionRef.make<PropertyPath.ValueFromPath<I, P>>),
|
||||||
|
), [self.latestValueRef, ...path])
|
||||||
|
|
||||||
useIssuesSubscribable(path: PropertyPath.Paths<A>) {
|
const [value, setValue] = yield* Hooks.useRefState(internalValueRef)
|
||||||
return React.useMemo(() => {
|
const [issues] = yield* Hooks.useSubscribe(issuesSubscribable)
|
||||||
const filter = Option.match({
|
|
||||||
onSome: (v: ParseResult.ParseError) => Effect.andThen(
|
yield* Hooks.useFork(() => Stream.runForEach(
|
||||||
ParseResult.ArrayFormatter.formatError(v),
|
internalValueRef.changes.pipe(
|
||||||
Array.filter(issue => PropertyPath.equivalence(issue.path, path)),
|
Stream.changesWith(Equivalence.strict()),
|
||||||
|
// options.debounce ? Stream.debounce(options.debounce) : identity,
|
||||||
|
Stream.drop(1),
|
||||||
),
|
),
|
||||||
onNone: () => Effect.succeed([]),
|
internalValue => self.latestValueRef.pipe(
|
||||||
})
|
Effect.andThen(Schema.encode(self.schema)),
|
||||||
|
Effect.andThen(PropertyPath.immutableSet(path, internalValue)),
|
||||||
|
Effect.andThen(flow(
|
||||||
|
Schema.decode(self.schema),
|
||||||
|
Effect.andThen(v => SubscriptionRef.set(self.latestValueRef, v)),
|
||||||
|
Effect.andThen(SubscriptionRef.set(self.errorRef, Option.none())),
|
||||||
|
Effect.catchTag("ParseError", e => SubscriptionRef.set(self.errorRef, Option.some(e)))
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
), [internalValueRef, self.latestValueRef, self.schema, self.errorRef, ...path])
|
||||||
|
|
||||||
const errorSubscribable = this.errorSubscribable
|
return { value, setValue, issues }
|
||||||
return SubscribableInternal.make({
|
})
|
||||||
get: Effect.andThen(errorSubscribable.get, filter),
|
}
|
||||||
get changes() { return Stream.flatMap(errorSubscribable.changes, filter) },
|
}
|
||||||
})
|
|
||||||
}, [this.errorSubscribable, ...path])
|
|
||||||
|
export const make = <A, I = A, R = never>(
|
||||||
|
options: make.Options<A, I, R>
|
||||||
|
): Effect.Effect<Form<A, I, R>> => Effect.gen(function*() {
|
||||||
|
const latestValueRef = yield* SubscriptionRef.make(options.initialValue)
|
||||||
|
const errorRef = yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>())
|
||||||
|
|
||||||
|
return new FormImpl(
|
||||||
|
options.schema,
|
||||||
|
latestValueRef,
|
||||||
|
errorRef,
|
||||||
|
|
||||||
|
yield* Effect.cachedFunction(
|
||||||
|
(path: PropertyPath.PropertyPath) => Effect.succeed(SubscribableInternal.make({
|
||||||
|
get: Effect.flatMap(latestValueRef.get, PropertyPath.get(path as PropertyPath.Paths<A>)),
|
||||||
|
get changes() { return Stream.flatMap(latestValueRef.changes, PropertyPath.get(path as PropertyPath.Paths<A>)) },
|
||||||
|
})),
|
||||||
|
PropertyPath.equivalence,
|
||||||
|
),
|
||||||
|
yield* Effect.cachedFunction(
|
||||||
|
(path: PropertyPath.PropertyPath) => Effect.gen(function*() {
|
||||||
|
const filter = Option.match({
|
||||||
|
onSome: (v: ParseResult.ParseError) => Effect.andThen(
|
||||||
|
ParseResult.ArrayFormatter.formatError(v),
|
||||||
|
Array.filter(issue => PropertyPath.equivalence(issue.path, path)),
|
||||||
|
),
|
||||||
|
onNone: () => Effect.succeed([]),
|
||||||
|
})
|
||||||
|
|
||||||
|
return SubscribableInternal.make({
|
||||||
|
get: Effect.flatMap(errorRef.get, filter),
|
||||||
|
get changes() { return Stream.flatMap(errorRef.changes, filter) },
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
PropertyPath.equivalence,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export namespace make {
|
||||||
|
export interface Options<in out A, in out I = A, out R = never> {
|
||||||
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
|
readonly initialValue: NoInfer<A>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,4 +147,4 @@ const TestSchema = Schema.Struct({
|
|||||||
name: Schema.String
|
name: Schema.String
|
||||||
})
|
})
|
||||||
declare const form: Form<typeof TestSchema["Type"], typeof TestSchema["Encoded"], typeof TestSchema["Context"]>
|
declare const form: Form<typeof TestSchema["Type"], typeof TestSchema["Encoded"], typeof TestSchema["Context"]>
|
||||||
const t = form.useRef(["name"])
|
const t = form.useInput(["name"])
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const get: {
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const immutableSet: {
|
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>
|
<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> => {
|
} = 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 PropertyPath)
|
const key = Array.head(path as PropertyPath)
|
||||||
|
|||||||
Reference in New Issue
Block a user