0.1.4 #5
@@ -1,4 +1,4 @@
|
|||||||
import { Array, Effect, Equivalence, flow, Option, ParseResult, Pipeable, Schema, Stream, type Subscribable, SubscriptionRef } from "effect"
|
import { Array, Duration, Effect, Equivalence, flow, identity, Option, ParseResult, Pipeable, Schema, Stream, Subscribable, SubscriptionRef } from "effect"
|
||||||
import type { NoSuchElementException } from "effect/Cause"
|
import type { NoSuchElementException } from "effect/Cause"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Hooks } from "./hooks/index.js"
|
import { Hooks } from "./hooks/index.js"
|
||||||
@@ -10,72 +10,29 @@ 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 latestValueSubscribable: Subscribable.Subscribable<A>
|
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
||||||
|
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
|
||||||
|
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
||||||
|
|
||||||
useFieldLatestValueSubscribable<P extends PropertyPath.Paths<A>>(
|
makeFieldIssuesSubscribable<P extends PropertyPath.Paths<A>>(path: P): Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
||||||
path: P
|
|
||||||
): Subscribable.Subscribable<PropertyPath.ValueFromPath<A, P>, NoSuchElementException>
|
|
||||||
useFieldIssuesSubscribable(
|
|
||||||
path: PropertyPath.Paths<A>
|
|
||||||
): Subscribable.Subscribable<readonly ParseResult.ArrayFormatterIssue[]>
|
|
||||||
|
|
||||||
useInput<P extends PropertyPath.Paths<I>>(
|
|
||||||
options: Form.useInput.Options<I, P>
|
|
||||||
): Effect.Effect<
|
|
||||||
Form.useInput.Result<PropertyPath.ValueFromPath<I, P>>,
|
|
||||||
ParseResult.ParseError | NoSuchElementException,
|
|
||||||
R
|
|
||||||
>
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace Form {
|
|
||||||
export namespace useInput {
|
|
||||||
export interface Options<I, P extends PropertyPath.Paths<I>> {
|
|
||||||
readonly path: P
|
|
||||||
readonly defaultValue?: PropertyPath.ValueFromPath<NoInfer<I>, NoInfer<P>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Result<T> {
|
|
||||||
readonly value: T
|
|
||||||
readonly setValue: React.Dispatch<React.SetStateAction<T>>
|
|
||||||
readonly issues: readonly ParseResult.ArrayFormatterIssue[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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<A>
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly schema: Schema.Schema<A, I, R>,
|
readonly schema: Schema.Schema<A, I, R>,
|
||||||
readonly latestValueRef: SubscriptionRef.SubscriptionRef<A>,
|
readonly valueRef: SubscriptionRef.SubscriptionRef<Option.Option<A>>,
|
||||||
readonly latestCandidateRef: SubscriptionRef.SubscriptionRef<I>,
|
readonly encodedValueRef: SubscriptionRef.SubscriptionRef<I>,
|
||||||
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
readonly errorRef: SubscriptionRef.SubscriptionRef<Option.Option<ParseResult.ParseError>>,
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.latestValueSubscribable = latestValueRef
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useFieldLatestValueSubscribable<P extends PropertyPath.Paths<A>>(
|
makeFieldIssuesSubscribable<P extends PropertyPath.Paths<A>>(path: P) {
|
||||||
path: P
|
|
||||||
) {
|
|
||||||
return React.useMemo(() => {
|
|
||||||
const latestValueRef = this.latestValueRef
|
|
||||||
return SubscribableInternal.make({
|
|
||||||
get: Effect.flatMap(latestValueRef.get, PropertyPath.get(path)),
|
|
||||||
get changes() { return Stream.flatMap(latestValueRef.changes, PropertyPath.get(path)) },
|
|
||||||
})
|
|
||||||
}, [this.latestValueRef, ...path])
|
|
||||||
}
|
|
||||||
|
|
||||||
useFieldIssuesSubscribable(
|
|
||||||
path: PropertyPath.Paths<A>
|
|
||||||
) {
|
|
||||||
return React.useMemo(() => {
|
|
||||||
const filter = Option.match({
|
const filter = Option.match({
|
||||||
onSome: (v: ParseResult.ParseError) => Effect.andThen(
|
onSome: (v: ParseResult.ParseError) => Effect.andThen(
|
||||||
ParseResult.ArrayFormatter.formatError(v),
|
ParseResult.ArrayFormatter.formatError(v),
|
||||||
@@ -89,68 +46,80 @@ extends Pipeable.Class() implements Form<A, I, R> {
|
|||||||
get: Effect.flatMap(errorRef.get, filter),
|
get: Effect.flatMap(errorRef.get, filter),
|
||||||
get changes() { return Stream.flatMap(errorRef.changes, filter) },
|
get changes() { return Stream.flatMap(errorRef.changes, filter) },
|
||||||
})
|
})
|
||||||
}, [this.latestValueRef, ...path])
|
|
||||||
}
|
|
||||||
|
|
||||||
useInput<P extends PropertyPath.Paths<I>>(
|
|
||||||
options: Form.useInput.Options<I, P>
|
|
||||||
) {
|
|
||||||
const self = this
|
|
||||||
return Effect.gen(function*() {
|
|
||||||
const internalValueRef = yield* Hooks.useMemo(() => self.latestValueRef.pipe(
|
|
||||||
Effect.andThen(flow(
|
|
||||||
Schema.encode(self.schema),
|
|
||||||
Effect.andThen(PropertyPath.get(options.path)),
|
|
||||||
Effect.catchTag("ParseError", e => options.defaultValue !== undefined && options.defaultValue !== null
|
|
||||||
? Effect.succeed(options.defaultValue)
|
|
||||||
: Effect.fail(e)
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
Effect.andThen(SubscriptionRef.make<PropertyPath.ValueFromPath<I, P>>),
|
|
||||||
), [self.latestValueRef, ...options.path])
|
|
||||||
|
|
||||||
const issuesSubscribable = self.useFieldIssuesSubscribable(options.path)
|
|
||||||
|
|
||||||
const [value, setValue] = yield* Hooks.useRefState(internalValueRef)
|
|
||||||
const [issues] = yield* Hooks.useSubscribe(issuesSubscribable)
|
|
||||||
|
|
||||||
yield* Hooks.useFork(() => Stream.runForEach(
|
|
||||||
internalValueRef.changes.pipe(
|
|
||||||
Stream.changesWith(Equivalence.strict()),
|
|
||||||
// options.debounce ? Stream.debounce(options.debounce) : identity,
|
|
||||||
Stream.drop(1),
|
|
||||||
),
|
|
||||||
internalValue => self.latestValueRef.pipe(
|
|
||||||
Effect.andThen(Schema.encode(self.schema)),
|
|
||||||
Effect.andThen(PropertyPath.immutableSet(options.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, ...options.path])
|
|
||||||
|
|
||||||
return { value, setValue, issues }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const make: {
|
|
||||||
<A, I = A, R = never>(options: make.Options<A, I, R>): Effect.Effect<Form<A, I, R>>
|
|
||||||
} = Effect.fnUntraced(function* <A, I = A, R = never>(options: make.Options<A, I, R>) {
|
|
||||||
return new FormImpl(
|
|
||||||
options.schema,
|
|
||||||
yield* SubscriptionRef.make(options.initialValue),
|
|
||||||
yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export namespace make {
|
export namespace make {
|
||||||
export interface Options<in out A, in out I = A, out R = never> {
|
export interface Options<in out A, in out I = A, out R = never> {
|
||||||
readonly schema: Schema.Schema<A, I, R>
|
readonly schema: Schema.Schema<A, I, R>
|
||||||
readonly initialEncodedValue: NoInfer<I>
|
readonly initialEncodedValue: NoInfer<I>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const make: {
|
||||||
|
<A, I = A, R = never>(options: make.Options<A, I, R>): Effect.Effect<Form<A, I, R>>
|
||||||
|
} = Effect.fnUntraced(function* <A, I = A, R = never>(options: make.Options<A, I, R>) {
|
||||||
|
return new FormImpl(
|
||||||
|
options.schema,
|
||||||
|
yield* SubscriptionRef.make(Option.none<A>()),
|
||||||
|
yield* SubscriptionRef.make(options.initialEncodedValue),
|
||||||
|
yield* SubscriptionRef.make(Option.none<ParseResult.ParseError>()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export namespace useInput {
|
||||||
|
export interface Options<I, P extends PropertyPath.Paths<I>> {
|
||||||
|
readonly path: P
|
||||||
|
readonly debounce?: Duration.DurationInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result<T> {
|
||||||
|
readonly value: T
|
||||||
|
readonly setValue: React.Dispatch<React.SetStateAction<T>>
|
||||||
|
readonly issues: readonly ParseResult.ArrayFormatterIssue[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInput: {
|
||||||
|
<A, I, R, P extends PropertyPath.Paths<I>>(
|
||||||
|
self: Form<A, I, R>,
|
||||||
|
options: useInput.Options<NoInfer<I>, P>,
|
||||||
|
): Effect.Effect<useInput.Result<PropertyPath.ValueFromPath<I, P>>, NoSuchElementException, R>
|
||||||
|
} = Effect.fnUntraced(function* <A, I, R, P extends PropertyPath.Paths<I>>(
|
||||||
|
self: Form<A, I, R>,
|
||||||
|
options: useInput.Options<NoInfer<I>, P>,
|
||||||
|
) {
|
||||||
|
const [internalValueRef, issuesSubscribable] = yield* Hooks.useMemo(() => Effect.all([
|
||||||
|
self.encodedValueRef.pipe(
|
||||||
|
Effect.andThen(PropertyPath.get(options.path)),
|
||||||
|
Effect.andThen(SubscriptionRef.make<PropertyPath.ValueFromPath<I, P>>),
|
||||||
|
),
|
||||||
|
Effect.succeed(self.makeFieldIssuesSubscribable(options.path)),
|
||||||
|
]), [self, ...options.path])
|
||||||
|
|
||||||
|
const [value, setValue] = yield* Hooks.useRefState(internalValueRef)
|
||||||
|
const [issues] = yield* Hooks.useSubscribe(issuesSubscribable)
|
||||||
|
|
||||||
|
yield* Hooks.useFork(() => Stream.runForEach(
|
||||||
|
internalValueRef.changes.pipe(
|
||||||
|
Stream.changesWith(Equivalence.strict()),
|
||||||
|
options.debounce ? Stream.debounce(options.debounce) : identity,
|
||||||
|
Stream.drop(1),
|
||||||
|
),
|
||||||
|
|
||||||
|
internalValue => self.encodedValueRef.pipe(
|
||||||
|
Effect.andThen(encodedValue => PropertyPath.immutableSet(encodedValue, options.path, internalValue)),
|
||||||
|
Effect.tap(encodedValue => SubscriptionRef.set(self.encodedValueRef, encodedValue)),
|
||||||
|
Effect.andThen(flow(
|
||||||
|
Schema.decode(self.schema),
|
||||||
|
Effect.andThen(v => SubscriptionRef.set(self.valueRef, Option.some(v))),
|
||||||
|
Effect.andThen(SubscriptionRef.set(self.errorRef, Option.none())),
|
||||||
|
Effect.catchTag("ParseError", e => SubscriptionRef.set(self.errorRef, Option.some(e)))
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
), [internalValueRef, self, ...options.path])
|
||||||
|
|
||||||
|
return { value, setValue, issues }
|
||||||
|
})
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ const RegisterFormSchema = Schema.Struct({
|
|||||||
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
class RegisterForm extends Effect.Service<RegisterForm>()("RegisterForm", {
|
||||||
scoped: Form.make({
|
scoped: Form.make({
|
||||||
schema: RegisterFormSchema,
|
schema: RegisterFormSchema,
|
||||||
initialValue: { email: "", password: "" },
|
initialEncodedValue: { email: "", password: "" },
|
||||||
})
|
})
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
class RegisterPage extends Component.makeUntraced(function* RegisterPage() {
|
class RegisterPage extends Component.makeUntraced(function* RegisterPage() {
|
||||||
const form = yield* RegisterForm
|
const form = yield* RegisterForm
|
||||||
const emailInput = yield* form.useInput({ path: ["email"], defaultValue: "" })
|
const emailInput = yield* Form.useInput(form, { path: ["email"] })
|
||||||
const passwordInput = yield* form.useInput({ path: ["password"], defaultValue: "" })
|
const passwordInput = yield* Form.useInput(form, { path: ["password"] })
|
||||||
|
|
||||||
yield* useFork(() => Stream.runForEach(form.latestValueSubscribable.changes, Console.log), [])
|
yield* useFork(() => Stream.runForEach(form.valueRef.changes, Console.log), [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
|||||||
Reference in New Issue
Block a user