import { RegistryContext, useAtom, useAtomMount, useAtomSet, useAtomValue } from "@effect-atom/atom-react" import * as Atom from "@effect-atom/atom/Atom" import type * as Registry from "@effect-atom/atom/Registry" import { Field, FormAtoms } from "@lucas-barake/effect-form" import type { FieldState as FieldStateModule, Mode } from "@lucas-barake/effect-form" import type * as FormBuilder from "@lucas-barake/effect-form/FormBuilder" import { getNestedValue } from "@lucas-barake/effect-form/Path" import type * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import * as Option from "effect/Option" import type * as ParseResult from "effect/ParseResult" import type * as Schema from "effect/Schema" import * as React from "react" import { createContext, useContext } from "react" export type FieldValue = FieldStateModule.FieldValue export type FieldState = FieldStateModule.FieldState export type ArrayFieldOperations = FieldStateModule.ArrayFieldOperations export interface FieldComponentProps,> { readonly field: FieldState readonly props: P } export type FieldComponent,> = React.FC, P>> export type ExtractExtraProps = C extends React.FC> ? P : Record type StructFieldsFromSchema = S extends Schema.Struct ? Fields : S extends { readonly from: infer From } ? StructFieldsFromSchema : never export type ArrayItemComponentMap = StructFieldsFromSchema extends Schema.Struct.Fields ? { readonly [K in keyof StructFieldsFromSchema]: StructFieldsFromSchema[K] extends Schema.Schema.Any ? React.FC[K]>, any>> : never } : React.FC, any>> export type FieldComponentMap = { readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef ? React.FC, any>> : TFields[K] extends Field.ArrayFieldDef ? ArrayItemComponentMap : never } export type FieldRefs = FormAtoms.FieldRefs export type BuiltForm< TFields extends Field.FieldsRecord, R, A = void, E = never, SubmitArgs = void, CM extends FieldComponentMap = FieldComponentMap, > = { readonly values: Atom.Atom>> readonly isDirty: Atom.Atom readonly hasChangedSinceSubmit: Atom.Atom readonly lastSubmittedValues: Atom.Atom>> readonly submitCount: Atom.Atom readonly validationCount: Atom.Atom readonly schema: Schema.Schema, Field.EncodedFromFields, R> readonly fields: FieldRefs readonly Initialize: React.FC<{ readonly defaultValues: Field.EncodedFromFields readonly validateOnInit?: boolean readonly children: React.ReactNode }> readonly submit: Atom.AtomResultFn readonly validate: Atom.AtomResultFn readonly reset: Atom.Writable readonly revertToLastSubmit: Atom.Writable readonly setValues: Atom.Writable> readonly getFieldAtoms: (field: FormBuilder.FieldRef) => FormAtoms.PublicFieldAtoms readonly mount: Atom.Atom readonly KeepAlive: React.FC } & FieldComponents type FieldComponents,> = { readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef ? React.FC> : TFields[K] extends Field.ArrayFieldDef ? ArrayFieldComponent> : never } type ExtractArrayItemExtraProps = StructFieldsFromSchema extends Schema.Struct.Fields ? { readonly [K in keyof StructFieldsFromSchema]: CM extends { readonly [P in K]: infer C } ? ExtractExtraProps : never } : CM extends React.FC> ? P : never type ArrayFieldComponent = & React.FC<{ readonly children: (ops: ArrayFieldOperations>) => React.ReactNode }> & { readonly Item: React.FC<{ readonly index: number readonly children: React.ReactNode | ((props: { readonly remove: () => void }) => React.ReactNode) }> } & (StructFieldsFromSchema extends Schema.Struct.Fields ? { readonly [K in keyof StructFieldsFromSchema]: React.FC< ExtraPropsMap extends { readonly [P in K]: infer EP } ? EP : Record > } : unknown) interface ArrayItemContextValue { readonly index: number readonly parentPath: string } const ArrayItemContext = createContext(null) const makeFieldComponent = ( fieldKey: string, fieldDef: Field.FieldDef, getOrCreateFieldAtoms: (fieldPath: string, schema: Schema.Schema.Any) => FormAtoms.FieldAtoms, Component: React.FC, P>>, onBlurSubmitAtom: Atom.Writable ): React.FC

=> { const FieldComponent: React.FC

= (extraProps) => { const arrayCtx = useContext(ArrayItemContext) const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey const fieldAtoms = React.useMemo( () => getOrCreateFieldAtoms(fieldPath, fieldDef.schema), [fieldPath] ) const [value, setValue] = useAtom(fieldAtoms.valueAtom) as [Schema.Schema.Encoded, (v: unknown) => void] const [isTouched, setTouched] = useAtom(fieldAtoms.touchedAtom) const displayError = useAtomValue(fieldAtoms.displayErrorAtom) const isDirty = useAtomValue(fieldAtoms.isDirtyAtom) const isValidating = useAtomValue(fieldAtoms.validationAtom).waiting const setOnBlurSubmit = useAtomSet(onBlurSubmitAtom) useAtomMount(fieldAtoms.triggerValidationAtom) const onChange = React.useCallback( (newValue: Schema.Schema.Encoded) => setValue(newValue), [setValue] ) const onBlur = React.useCallback(() => { setTouched(true) setOnBlurSubmit() }, [setTouched, setOnBlurSubmit]) const fieldState = React.useMemo( () => ({ path: fieldPath, value, onChange, onBlur, error: displayError, isTouched, isValidating, isDirty }), [fieldPath, value, onChange, onBlur, displayError, isTouched, isValidating, isDirty] ) return } return React.memo(FieldComponent) as React.FC

} const makeArrayFieldComponent = ( fieldKey: string, def: Field.ArrayFieldDef, stateAtom: Atom.Writable>, Option.Option>>, getOrCreateFieldAtoms: (fieldPath: string, schema: Schema.Schema.Any) => FormAtoms.FieldAtoms, operations: FormAtoms.FormOperations, componentMap: ArrayItemComponentMap, onBlurSubmitAtom: Atom.Writable ): ArrayFieldComponent => { const ArrayWrapper: React.FC<{ readonly children: (ops: ArrayFieldOperations>) => React.ReactNode }> = ({ children }) => { const arrayCtx = useContext(ArrayItemContext) const [formStateOption, setFormState] = useAtom(stateAtom) const formState = Option.getOrThrow(formStateOption) const fieldPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey const items = React.useMemo( () => (getNestedValue(formState.values, fieldPath) ?? []) as ReadonlyArray>, [formState.values, fieldPath] ) const append = React.useCallback( (value?: Schema.Schema.Encoded) => { setFormState((prev) => { if (Option.isNone(prev)) return prev return Option.some(operations.appendArrayItem(prev.value, fieldPath, def.itemSchema, value)) }) }, [fieldPath, setFormState] ) const remove = React.useCallback( (index: number) => { setFormState((prev) => { if (Option.isNone(prev)) return prev return Option.some(operations.removeArrayItem(prev.value, fieldPath, index)) }) }, [fieldPath, setFormState] ) const swap = React.useCallback( (indexA: number, indexB: number) => { setFormState((prev) => { if (Option.isNone(prev)) return prev return Option.some(operations.swapArrayItems(prev.value, fieldPath, indexA, indexB)) }) }, [fieldPath, setFormState] ) const move = React.useCallback( (from: number, to: number) => { setFormState((prev) => { if (Option.isNone(prev)) return prev return Option.some(operations.moveArrayItem(prev.value, fieldPath, from, to)) }) }, [fieldPath, setFormState] ) return <>{children({ items, append, remove, swap, move })} } const ItemWrapper: React.FC<{ readonly index: number readonly children: React.ReactNode | ((props: { readonly remove: () => void }) => React.ReactNode) }> = ({ children, index }) => { const arrayCtx = useContext(ArrayItemContext) const setFormState = useAtomSet(stateAtom) const parentPath = arrayCtx ? `${arrayCtx.parentPath}.${fieldKey}` : fieldKey const itemPath = `${parentPath}[${index}]` const remove = React.useCallback(() => { setFormState((prev) => { if (Option.isNone(prev)) return prev return Option.some(operations.removeArrayItem(prev.value, parentPath, index)) }) }, [parentPath, index, setFormState]) return ( {typeof children === "function" ? children({ remove }) : children} ) } const itemFieldComponents: Record = {} const subFieldDefs = Field.extractStructFieldDefs(def.itemSchema) if (subFieldDefs) { for (const subDef of subFieldDefs) { const itemComponent = (componentMap as Record>>)[subDef.key] itemFieldComponents[subDef.key] = makeFieldComponent( subDef.key, subDef, getOrCreateFieldAtoms, itemComponent, onBlurSubmitAtom ) } } const properties: Record = { Item: ItemWrapper, ...itemFieldComponents } return new Proxy(ArrayWrapper, { get(target, prop) { if (prop in properties) { return properties[prop as string] } return Reflect.get(target, prop) } }) as ArrayFieldComponent } const makeFieldComponents = ,>( fields: TFields, stateAtom: Atom.Writable< Option.Option>, Option.Option> >, getOrCreateFieldAtoms: (fieldPath: string, schema: Schema.Schema.Any) => FormAtoms.FieldAtoms, operations: FormAtoms.FormOperations, componentMap: CM, onBlurSubmitAtom: Atom.Writable ): FieldComponents => { const components: Record = {} for (const [key, def] of Object.entries(fields)) { if (Field.isArrayFieldDef(def)) { const arrayComponentMap = (componentMap as Record)[key] components[key] = makeArrayFieldComponent( key, def as Field.ArrayFieldDef, stateAtom, getOrCreateFieldAtoms, operations, arrayComponentMap, onBlurSubmitAtom ) } else if (Field.isFieldDef(def)) { const fieldComponent = (componentMap as Record>>)[key] components[key] = makeFieldComponent( key, def, getOrCreateFieldAtoms, fieldComponent, onBlurSubmitAtom ) } } return components as FieldComponents } export const make: { < TFields extends Field.FieldsRecord, R extends Registry.AtomRegistry, A, E, SubmitArgs = void, CM extends FieldComponentMap = FieldComponentMap, >( self: FormBuilder.FormBuilder, options: { readonly runtime?: Atom.AtomRuntime readonly fields: CM readonly mode?: SubmitArgs extends void ? Mode.FormMode : Mode.FormModeWithoutAutoSubmit readonly reactivityKeys?: ReadonlyArray | Readonly>> | undefined readonly onSubmit: ( args: SubmitArgs, ctx: { readonly decoded: Field.DecodedFromFields readonly encoded: Field.EncodedFromFields readonly get: Atom.FnContext } ) => A | Effect.Effect } ): BuiltForm < TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = void, ER = never, CM extends FieldComponentMap = FieldComponentMap, >( self: FormBuilder.FormBuilder, options: { readonly runtime: Atom.AtomRuntime readonly fields: CM readonly mode?: SubmitArgs extends void ? Mode.FormMode : Mode.FormModeWithoutAutoSubmit readonly reactivityKeys?: ReadonlyArray | Readonly>> | undefined readonly onSubmit: ( args: SubmitArgs, ctx: { readonly decoded: Field.DecodedFromFields readonly encoded: Field.EncodedFromFields readonly get: Atom.FnContext } ) => A | Effect.Effect } ): BuiltForm } = (self: any, options: any): any => { const { fields: components, mode, onSubmit, runtime: providedRuntime, reactivityKeys } = options const runtime = providedRuntime ?? Atom.runtime(Layer.empty) const { fields } = self const formAtoms = FormAtoms.make({ formBuilder: self, runtime, onSubmit, reactivityKeys, mode }) const { autoSubmitAtom, combinedSchema, fieldRefs, getFieldAtoms, getOrCreateFieldAtoms, hasChangedSinceSubmitAtom, isDirtyAtom, keepAliveActiveAtom, lastSubmittedValuesAtom, mountAtom, onBlurSubmitAtom, operations, resetAtom, revertToLastSubmitAtom, rootErrorAtom, setValuesAtom, stateAtom, submitAtom, submitCountAtom, validateAtom, validationCountAtom, valuesAtom } = formAtoms const InitializeComponent: React.FC<{ readonly defaultValues: any readonly validateOnInit?: boolean readonly children: React.ReactNode }> = ({ children, defaultValues, validateOnInit }) => { const registry = React.useContext(RegistryContext) const state = useAtomValue(stateAtom) const setFormState = useAtomSet(stateAtom) const triggerValidate = useAtomSet(validateAtom) const [isInitialized, setIsInitialized] = React.useState(false) React.useEffect(() => { const shouldInit = !registry.get(keepAliveActiveAtom) || Option.isNone(registry.get(stateAtom)) if (shouldInit) { setFormState(Option.some(operations.createInitialState(defaultValues))) if (validateOnInit) { triggerValidate() } } setIsInitialized(true) }, [registry]) useAtomMount(autoSubmitAtom) if (!isInitialized) return null if (Option.isNone(state)) return null return <>{children} } const fieldComponents = makeFieldComponents( fields, stateAtom, getOrCreateFieldAtoms, operations, components, onBlurSubmitAtom ) const KeepAlive: React.FC = () => { const setKeepAliveActive = useAtomSet(keepAliveActiveAtom) React.useLayoutEffect(() => { setKeepAliveActive(true) return () => setKeepAliveActive(false) }, [setKeepAliveActive]) useAtomMount(mountAtom) return null } return { values: valuesAtom, isDirty: isDirtyAtom, hasChangedSinceSubmit: hasChangedSinceSubmitAtom, lastSubmittedValues: lastSubmittedValuesAtom, submitCount: submitCountAtom, validationCount: validationCountAtom, rootError: rootErrorAtom, schema: combinedSchema, fields: fieldRefs, Initialize: InitializeComponent, submit: submitAtom, validate: validateAtom, reset: resetAtom, revertToLastSubmit: revertToLastSubmitAtom, setValues: setValuesAtom, getFieldAtoms, mount: mountAtom, KeepAlive, ...fieldComponents } }