@kform/react
Version:
React integration for KForm.
1 lines • 182 kB
Source Map (JSON)
{"version":3,"file":"kform-react.cjs","sources":["../../src/contexts/FormContext.ts","../../src/utils/errors.ts","../../src/hooks/useFormContext.ts","../../src/hooks/useResolvedPath.ts","../../src/components/CurrentPath.tsx","../../src/utils/ignorePromiseCancellationException.ts","../../src/utils/useLatestValues.ts","../../src/utils/iterableUtils.ts","../../src/utils/promiseUtils.ts","../../src/utils/useEqualityFn.ts","../../src/hooks/useFormManager.ts","../../src/hooks/useController.ts","../../src/utils/equals.ts","../../src/utils/usePrevious.ts","../../src/hooks/useNewFormManager.ts","../../src/hooks/useForm.ts","../../src/components/Form.tsx","../../src/hooks/useFormatter.ts","../../src/components/FormattedValue.tsx","../../src/hooks/useInput.ts","../../src/components/Input.tsx","../../src/hooks/useCurrentPath.ts","../../src/utils/fileUtils.ts","../../src/hooks/useFileInput.ts","../../src/hooks/useFormattedValue.ts","../../src/hooks/useFormController.ts","../../src/utils/binarySearch.ts","../../src/hooks/useIssuesTracker.ts","../../src/utils/listableUtils.ts","../../src/hooks/useListableInput.ts","../../src/utils/numericUtils.ts","../../src/hooks/useNumericInput.ts","../../src/hooks/useSubmitting.ts","../../src/hooks/useSubscription.ts","../../src/utils/temporalUtils.ts","../../src/hooks/useTemporalInput.ts","../../src/utils/setKFormLogLevel.ts"],"sourcesContent":["import { type AbsolutePath, FormManager } from \"@kform/core\";\nimport * as React from \"react\";\n\nimport { type FormController } from \"../hooks/useForm\";\n\n/**\n * Value of the form context.\n */\nexport interface FormContextValue<T = unknown> {\n /**\n * Form manager in scope.\n */\n formManager: FormManager;\n /**\n * Current path in context.\n *\n * Paths provided to e.g. `useController` are relative to this path.\n */\n currentPath: AbsolutePath;\n /**\n * Controller of the form in scope.\n */\n controller?: FormController<T>;\n}\n\n/**\n * Context of a form.\n */\nexport const FormContext = React.createContext<FormContextValue<any> | null>(\n null,\n);\n","import { AbsolutePath } from \"@kform/core\";\n\n/** Error occurring at a certain path. */\nexport class AtPathError extends Error {\n /** Path at which the error occurred. */\n public readonly path: AbsolutePath;\n\n constructor(path: AbsolutePath, message: string, options?: ErrorOptions) {\n super(`At '${path.toString()}': ${message}`, options);\n this.name = this.constructor.name;\n this.path = path;\n }\n}\n\n/** Error thrown when an invalid {@link AtPathError.path path} was provided. */\nexport class InvalidPathError extends AtPathError {\n constructor(path: AbsolutePath, message: string) {\n super(path, message);\n this.name = this.constructor.name;\n }\n}\n\n/**\n * Error thrown when attempting to access the context of a form when no form is\n * in context.\n */\nexport class NoFormContextError extends Error {\n constructor() {\n super(\"No form context found\");\n this.name = this.constructor.name;\n }\n}\n\n/**\n * Error thrown when attempting to access a form value but no form is in context\n * and no form manager was provided via an option.\n */\nexport class NoFormManagerError extends Error {\n constructor() {\n super(\"No form manager found\");\n this.name = this.constructor.name;\n }\n}\n\n/**\n * Error thrown when attempting to access the controller of a form but no form\n * controller is in context.\n */\nexport class NoFormControllerError extends Error {\n constructor() {\n super(\"No form controller found\");\n this.name = this.constructor.name;\n }\n}\n","import * as React from \"react\";\n\nimport { FormContext, FormContextValue } from \"../contexts/FormContext\";\nimport { NoFormContextError } from \"../utils/errors\";\n\n/**\n * Hook providing access to the current context within a form.\n * @throws {NoFormContextError} When no form is in context.\n * @returns The current context within a form.\n */\nexport function useFormContext<T = unknown>(): FormContextValue<T> {\n const formContext = React.useContext(FormContext);\n if (!formContext) {\n throw new NoFormContextError();\n }\n return formContext;\n}\n","import { AbsolutePath, Path } from \"@kform/core\";\nimport * as React from \"react\";\n\nimport { FormContext } from \"../contexts/FormContext\";\nimport { NoFormContextError } from \"../utils/errors\";\n\n/**\n * Resolves a given path against the current form path or against\n * {@link currentPath} when provided. The returned absolute path has a stable\n * identity.\n * @param path Path to resolve against the current path.\n * @param currentPath Optional current path argument. When provided, this path\n * is used instead of the form context's current path.\n * @returns Resolved absolute path with a stable identity.\n */\nexport function useResolvedPath(\n path: Path | string = Path.CURRENT,\n currentPath?: AbsolutePath,\n): AbsolutePath {\n const formContext = React.useContext(FormContext);\n currentPath ??= formContext?.currentPath;\n if (currentPath == null) {\n throw new NoFormContextError();\n }\n\n const stableAbsolutePathRef = React.useRef<AbsolutePath>();\n const absolutePath = React.useMemo(\n () => currentPath.resolve(path),\n [currentPath, path],\n );\n if (!absolutePath.equals(stableAbsolutePathRef.current)) {\n stableAbsolutePathRef.current = absolutePath;\n }\n return stableAbsolutePathRef.current!;\n}\n","import { Path } from \"@kform/core\";\nimport * as React from \"react\";\n\nimport { FormContext, FormContextValue } from \"../contexts/FormContext\";\nimport { useFormContext } from \"../hooks/useFormContext\";\nimport { useResolvedPath } from \"../hooks/useResolvedPath\";\n\n/**\n * Properties of the current path component.\n */\nexport interface CurrentPathProps {\n /**\n * Path to set as the new current path for all children of this component.\n *\n * This path will be resolved against the current path.\n * @default Path.CURRENT\n */\n path?: Path | string;\n children?: React.ReactNode;\n}\n\n/**\n * Component used to specify a new current path for its children.\n *\n * This component must be rendered within a form context. The provided path will\n * be resolved against the current path in context.\n */\nexport function CurrentPath({\n path = Path.CURRENT,\n children,\n}: CurrentPathProps) {\n const formContext = useFormContext();\n const resolvedPath = useResolvedPath(path);\n\n const newFormContext = React.useMemo<FormContextValue>(\n () => ({\n formManager: formContext.formManager,\n currentPath: resolvedPath,\n controller: formContext.controller,\n }),\n [formContext.controller, formContext.formManager, resolvedPath],\n );\n\n return (\n <FormContext.Provider value={newFormContext}>\n {children}\n </FormContext.Provider>\n );\n}\n","import { PromiseCancellationException } from \"@kform/core\";\n\n/**\n * Function that re-throws the provided {@link caughtError} argument, unless it\n * is an instance of {@link PromiseCancellationException}.\n *\n * This is useful in scenarios where cancellations are expected and, as such,\n * shouldn't be regarded as errors.\n * @param caughtError Caught error.\n * @throws {caughtError} When not an instance of\n * {@link PromiseCancellationException}.\n */\nexport function ignorePromiseCancellationException(caughtError: unknown) {\n if (!(caughtError instanceof PromiseCancellationException)) {\n throw caughtError;\n }\n}\n","import * as React from \"react\";\n\n/**\n * Hook providing a stable identity object containing the latest values. These\n * values should **not** be accessed during render time.\n * @param values Object containing all values.\n * @returns Stable identity object containing said values.\n */\nexport function useLatestValues<T extends Record<keyof T, unknown>>(\n values: T,\n): T {\n const valuesRef = React.useRef<T>({} as T);\n Object.assign(valuesRef.current, values);\n return valuesRef.current;\n}\n","/**\n * Simple `Array.fromAsync` implementation.\n * @param iterable Iterable to transform into an array.\n * @returns Promise containing an array with all the elements in the iterable.\n */\nexport async function arrayFromAsync<T>(\n iterable: Iterable<T> | AsyncIterable<T>,\n): Promise<T[]> {\n const array = [];\n for await (const el of iterable) {\n array.push(el);\n }\n return array;\n}\n","/**\n * Whether a given value is [promise-like]{@link PromiseLike}.\n * @param value Value to check if it is promise-like.\n * @returns Whether the provided value is like a promise.\n */\nexport function isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n return typeof (value as any)?.then === \"function\";\n}\n","import * as React from \"react\";\n\nconst INITIAL = Symbol();\n\nexport function useEqualityFn<TState, TResult>(\n selector: (state: TState) => TResult,\n equalityFn: (v1: TResult, v2: TResult) => boolean = Object.is,\n): (state: TState) => TResult {\n const latest = React.useRef<TResult | typeof INITIAL>(INITIAL);\n return (state) => {\n const next = selector(state);\n return latest.current !== INITIAL && equalityFn(latest.current, next)\n ? latest.current\n : (latest.current = next);\n };\n}\n","import { type FormManager } from \"@kform/core\";\nimport * as React from \"react\";\n\nimport { FormContext } from \"../contexts/FormContext\";\nimport { NoFormManagerError } from \"../utils/errors\";\n\n/**\n * Hook providing access to the form manager within the context of a form. If a\n * form manager is provided as argument, then the provided argument is returned\n * instead.\n * @param formManager Optional argument which, when provided, is simply returned\n * as-is instead of the form manager within the context of a form.\n * @throws {NoFormManagerError} When no form manager was found.\n * @returns The form manager within the context of a form or, when provided, the\n * given form manager argument.\n */\nexport function useFormManager(formManager?: FormManager): FormManager {\n const formContext = React.useContext(FormContext);\n const relevantFormManager = formManager ?? formContext?.formManager;\n if (relevantFormManager == null) {\n throw new NoFormManagerError();\n }\n return relevantFormManager;\n}\n","import {\n AbsolutePath,\n AbsolutePathFragment,\n CancellablePromise,\n DisplayStatus,\n FormManager,\n Path,\n Schema,\n SealedFormManagerEvent,\n SealedLocatedValidationIssue,\n SealedValidationIssue,\n SealedValueEvent,\n StateEvent,\n ValidationStatus,\n ValueEvent,\n} from \"@kform/core\";\nimport * as React from \"react\";\nimport { createStore, useStore } from \"zustand\";\nimport { subscribeWithSelector } from \"zustand/middleware\";\n\nimport { ignorePromiseCancellationException } from \"../utils/ignorePromiseCancellationException\";\nimport { arrayFromAsync } from \"../utils/iterableUtils\";\nimport { isPromiseLike } from \"../utils/promiseUtils\";\nimport { shallow } from \"../utils/shallow\";\nimport { useEqualityFn } from \"../utils/useEqualityFn\";\nimport { useLatestValues } from \"../utils/useLatestValues\";\nimport { useFormManager } from \"./useFormManager\";\nimport { useResolvedPath } from \"./useResolvedPath\";\n\n/**\n * Options available to the {@link useController} hook.\n */\nexport interface ControllerOptions<\n T = unknown,\n TState extends ControllerState<T> = ControllerState<T>,\n> {\n /**\n * Required if no form context is in scope.\n *\n * If a form context is in scope and this value is also provided, then the\n * provided form manager will be used, in which case the current path of the\n * form context is ignored.\n */\n formManager?: FormManager;\n /**\n * Whether to enable the controller.\n *\n * @default true\n */\n enabled?: boolean;\n /**\n * Default extra state.\n * @internal\n */\n _defaultState?: Partial<TState>;\n /**\n * Function called once the controller has been initialised.\n *\n * @param info Controller's info.\n */\n onInitialized?: (\n state: TState & InitializedControllerState<T>,\n ) => void | PromiseLike<void>;\n /**\n * Function called whenever the controller is uninitialised.\n */\n onUninitialized?: (state: TState & UninitializedControllerState<T>) => void;\n /**\n * Function called whenever an event matching the controller's path is\n * emitted.\n *\n * @param event Form manager event.\n */\n onFormManagerEvent?: (\n event: SealedFormManagerEvent,\n state: TState & InitializedControllerState<T>,\n ) => void | PromiseLike<void>;\n onValueChange?: (\n event: SealedValueEvent,\n state: TState & InitializedControllerState<T>,\n ) => void | PromiseLike<void>;\n onValidationStatusChange?: (\n event: StateEvent.ValidationChange,\n state: TState & InitializedControllerState<T>,\n ) => void | PromiseLike<void>;\n onDisplayStatusChange?: (\n event: StateEvent.DisplayChange,\n state: TState & InitializedControllerState<T>,\n ) => void | PromiseLike<void>;\n onDirtyStatusChange?: (\n event: StateEvent.DirtyChange,\n state: TState & InitializedControllerState<T>,\n ) => void | PromiseLike<void>;\n onTouchedStatusChange?: (\n event: StateEvent.TouchedChange,\n state: TState & InitializedControllerState<T>,\n ) => void | PromiseLike<void>;\n}\n\n/**\n * Controller for a value of the form.\n */\nexport interface Controller<\n T = unknown,\n TState extends ControllerState<T> = ControllerState<T>,\n> {\n /**\n * Returns the current state of the controller.\n */\n readonly getState: () => TState;\n /**\n * Sets the (non-private) state of the controller.\n * @param state State to set.\n * @internal\n */\n readonly _setState: (\n state: Partial<TState> | ((state: TState) => Partial<TState>),\n ) => void;\n /**\n * Subscribes to changes in the controller's state.\n *\n * @param selector Selector used to select which part of the controller's\n * state to observe.\n * @param listener Function called whenever the selected state changes.\n * @param options Subscription options.\n * @returns Function which should be called to unsubscribe.\n */\n readonly subscribe: <TSelected = unknown>(\n selector: (state: TState) => TSelected,\n listener: (\n selectedState: TSelected,\n prevSelectedState: TSelected | undefined,\n ) => void,\n options?: ControllerSubscriptionOptions<TSelected>,\n ) => () => void;\n /**\n * Hook used to select part of the controller's state.\n *\n * A component using this hook is re-rendered whenever said part of the state\n * changes.\n * @param selector Selector used to select part of the controller's state.\n */\n readonly useState: (() => TState) &\n (<TResult = unknown>(\n selector: (state: TState) => TResult,\n options?: ControllerUseStateOptions<TResult>,\n ) => TResult);\n /**\n * Hook which returns the form manager being used by the controller.\n */\n readonly useFormManager: () => FormManager;\n /**\n * Hook which returns the schema of the value being controlled.\n */\n readonly useSchema: () => Schema<T>;\n /**\n * Hook which returns the path of the value being controlled by this\n * controller.\n *\n * This path will not contain any provided recursive wildcards.\n */\n readonly usePath: () => AbsolutePath;\n /**\n * Hook which returns the schema path of the value being controlled.\n */\n readonly useSchemaPath: () => AbsolutePath;\n /**\n * Hook which returns whether the controller is currently observing\n * descendants.\n */\n readonly useObservingDescendants: () => boolean;\n /**\n * Hook which returns whether the controller has been initialised.\n */\n readonly useInitialized: () => boolean;\n /**\n * Hook which returns whether a value exists at the path being controlled.\n *\n * Returns `undefined` when the controller is not initialised.\n */\n readonly useExists: () => boolean | undefined;\n /**\n * Hook which returns the form value being controlled.\n *\n * Note that, when observing descendants, this hook will cause a component to\n * rerender when a descendant is changed, even if the identity of the value\n * hasn't changed.\n *\n * Returns `undefined` when the controller is not initialised.\n */\n readonly useValue: () => T | undefined;\n /**\n * Hook which returns whether the value being controlled is dirty.\n *\n * Returns `undefined` when the controller is not initialised.\n */\n readonly useDirty: () => boolean | undefined;\n /**\n * Hook which returns whether the value being controlled is touched.\n *\n * Returns `undefined` when the controller is not initialised.\n */\n readonly useTouched: () => boolean | undefined;\n /**\n * Hook which returns the issues of the value being controlled.\n *\n * Returns `undefined` when the controller is not initialised.\n */\n readonly useIssues: () => SealedValidationIssue[] | undefined;\n /**\n * Hook which returns the validation status of the value being controlled.\n *\n * Returns `undefined` when the controller is not initialised.\n */\n readonly useValidationStatus: () => ValidationStatus | undefined;\n /**\n * Hook which returns the display status of the value being controlled.\n *\n * Returns `undefined` when the controller is not initialised.\n */\n readonly useDisplayStatus: () => DisplayStatus | undefined;\n\n readonly get: (<TValue = unknown, TResult = unknown>(\n path: Path | string,\n valueHandler: (value: TValue) => TResult | PromiseLike<TResult>,\n ) => CancellablePromise<TResult>) &\n (<TResult = unknown>(\n valueHandler: (value: T) => TResult | PromiseLike<TResult>,\n ) => CancellablePromise<TResult>);\n readonly getClone: (() => CancellablePromise<T>) &\n (<TValue = unknown>(path?: Path | string) => CancellablePromise<TValue>);\n readonly set: ((\n path: Path | string,\n toSet: unknown,\n ) => CancellablePromise<void>) &\n ((toSet: T) => CancellablePromise<void>);\n readonly reset: (path?: Path | string) => CancellablePromise<void>;\n readonly remove: (path?: Path | string) => CancellablePromise<void>;\n readonly validate: (\n path?: Path | string,\n ) => CancellablePromise<SealedLocatedValidationIssue[]>;\n readonly setDirty: (path?: Path | string) => CancellablePromise<void>;\n readonly setPristine: (path?: Path | string) => CancellablePromise<void>;\n readonly setTouched: (path?: Path | string) => CancellablePromise<void>;\n readonly setUntouched: (path?: Path | string) => CancellablePromise<void>;\n}\n\n/**\n * Options available to the controller's `useState` hook.\n */\nexport interface ControllerUseStateOptions<T = unknown> {\n /**\n * Function used to specify when two selections are considered equal to each\n * other.\n * @param v1 First selection.\n * @param v2 Second selection.\n * @returns Whether the two selections are considered equal.\n */\n equalityFn?: (v1: T, v2: T) => boolean;\n}\n\n/**\n * Options available when subscribing to the state of the controller.\n */\nexport interface ControllerSubscriptionOptions<T = unknown> {\n /**\n * Function used to specify when two selections are considered equal to each\n * other.\n * @param v1 First selection.\n * @param v2 Second selection.\n * @returns Whether the two selections are considered equal.\n */\n equalityFn?: (v1: T, v2: T) => boolean;\n /**\n * Whether the subscription's listener should be invoked immediately.\n */\n fireImmediately?: boolean;\n}\n\n/**\n * Controller's state.\n */\nexport type ControllerState<T = unknown> =\n | UninitializedControllerState<T>\n | InitializedControllerState<T>;\n\n/**\n * Base controller's state.\n */\nexport interface BaseControllerState<T = unknown> {\n /**\n * Form manager being used by the controller.\n */\n readonly formManager: FormManager;\n /**\n * Schema of the form value being controlled.\n */\n readonly schema: Schema<T>;\n /**\n * Path of the form value being controlled.\n */\n readonly path: AbsolutePath;\n /**\n * Schema path of the form value being controller.\n */\n readonly schemaPath: AbsolutePath;\n /**\n * Whether the controller is observing descendants.\n *\n * This will be `true` when the path provided to the controller ends in a\n * recursive wildcard.\n */\n readonly observingDescendants: boolean;\n /**\n * Whether the controller has been initialised.\n */\n readonly initialized: boolean;\n /**\n * Whether the form value being controlled exists.\n */\n readonly exists: boolean | undefined;\n /**\n * Form value.\n */\n readonly value: T | undefined;\n /**\n * Whether the form value being controlled is dirty.\n */\n readonly dirty: boolean | undefined;\n /**\n * Whether the form value being controlled has been touched.\n */\n readonly touched: boolean | undefined;\n /**\n * Validation issues associated with the form value being controlled.\n */\n readonly issues: SealedValidationIssue[] | undefined;\n /**\n * Validation status of the form value being controlled.\n */\n readonly validationStatus: ValidationStatus | undefined;\n /**\n * Display status of the form value being controlled.\n */\n readonly displayStatus: DisplayStatus | undefined;\n}\n\n/**\n * Uninitialised controller's state.\n */\nexport interface UninitializedControllerState<T = unknown>\n extends BaseControllerState<T> {\n readonly initialized: false;\n readonly exists: undefined;\n readonly value: undefined;\n readonly dirty: undefined;\n readonly touched: undefined;\n readonly issues: undefined;\n readonly validationStatus: undefined;\n readonly displayStatus: undefined;\n}\n\n/**\n * Initialized controller's state.\n */\nexport type InitializedControllerState<T = unknown> =\n | NonexistentValueControllerState<T>\n | ExistingValueControllerState<T>;\n\n/**\n * Controller state of a nonexistent form value.\n */\nexport interface NonexistentValueControllerState<T = unknown>\n extends BaseControllerState<T> {\n readonly initialized: true;\n readonly exists: false;\n readonly value: undefined;\n readonly dirty: undefined;\n readonly touched: undefined;\n readonly issues: undefined;\n readonly validationStatus: undefined;\n readonly displayStatus: undefined;\n}\n\n/**\n * Controller state of an existing form value.\n */\nexport interface ExistingValueControllerState<T = unknown>\n extends BaseControllerState<T> {\n readonly initialized: true;\n readonly exists: true;\n readonly value: T;\n readonly dirty: boolean;\n readonly touched: boolean;\n readonly issues: SealedValidationIssue[];\n readonly validationStatus: ValidationStatus;\n readonly displayStatus: DisplayStatus;\n}\n\n/**\n * State internal to the controller's Zustand store.\n *\n * We wrap the stored value within an array so that we can trigger Zustand\n * updates by simply replacing the wrapper, even if the underlying value's\n * identity hasn't changed.\n */\ntype InternalControllerState<\n T = unknown,\n TState extends ControllerState<T> = ControllerState<T>,\n> = Omit<TState, \"value\"> & {\n value: [T] | undefined;\n} & Record<string, unknown>;\n\n/**\n * Value representing an uninitialized controller state.\n */\nconst UNINITIALIZED_CONTROLLER_STATE = {\n initialized: false,\n exists: undefined,\n value: undefined,\n dirty: undefined,\n touched: undefined,\n issues: undefined,\n validationStatus: undefined,\n displayStatus: undefined,\n} as const;\n\n/**\n * Value representing a controller state of a nonexistent form value.\n */\nconst NONEXISTENT_VALUE_CONTROLLER_STATE = {\n initialized: true,\n exists: false,\n value: undefined,\n dirty: undefined,\n touched: undefined,\n issues: undefined,\n validationStatus: undefined,\n displayStatus: undefined,\n} as const;\n\n/**\n * Hook providing access to a controller used to read and control a value of\n * the form.\n * @param path Path of the form value to control, relative to the current path.\n *\n * The path must consist of only identifiers, except for the last fragment,\n * which may be a recursive wildcard to indicate that descendants should also be\n * observed.\n * @param options Available options.\n * @throws {Error} When {@link path} is invalid or contains fragments other than\n * ids.\n * @returns A controller used to read and control the form value.\n */\nexport function useController<T = unknown>(\n path?: Path | string,\n options?: undefined,\n): Controller<T>;\nexport function useController<\n T = unknown,\n TState extends ControllerState<T> = ControllerState<T>,\n>(\n path: Path | string | undefined,\n options: ControllerOptions<T, TState>,\n): Controller<T, TState>;\nexport function useController<\n T = unknown,\n TState extends ControllerState<T> = ControllerState<T>,\n>(\n path?: Path | string,\n {\n formManager: formManagerOption,\n enabled = true,\n _defaultState,\n onInitialized,\n onUninitialized,\n onFormManagerEvent,\n onValueChange,\n onValidationStatusChange,\n onDisplayStatusChange,\n onDirtyStatusChange,\n onTouchedStatusChange,\n }: ControllerOptions<T, TState> = {},\n): Controller<T, TState> {\n const formManager = useFormManager(formManagerOption);\n const resolvedPath = useResolvedPath(\n path,\n formManagerOption != null ? AbsolutePath.ROOT : undefined,\n );\n\n // Descendants are being observed when the last fragment of the path is a\n // recursive wildcard\n const observingDescendants =\n resolvedPath.lastFragment === AbsolutePathFragment.RecursiveWildcard;\n // Path of the value being controlled (excluding the possible recursive\n // wildcard)\n const valuePath = React.useMemo(\n () =>\n observingDescendants\n ? new AbsolutePath(resolvedPath.fragments.slice(0, -1))\n : resolvedPath,\n [resolvedPath, observingDescendants],\n );\n\n // Refuse invalid paths\n if (!formManager.isValidPath(valuePath)) {\n throw new Error(`Invalid path: '${resolvedPath.toString()}'.`);\n }\n\n // Refuse value paths that don't consist of identifiers only\n if (\n valuePath.fragments.some(\n (frag) => !(frag instanceof AbsolutePathFragment.Id),\n )\n ) {\n throw new Error(\n \"Controller path must only contain ids (except for the last fragment, \" +\n \"which may be a recursive wildcard).\",\n );\n }\n\n // Schema info\n const schemaInfo = React.useMemo(\n () => Array.from(formManager.schemaInfo<T>(valuePath))[0],\n [formManager, valuePath],\n );\n\n const defaultState = React.useRef(_defaultState);\n defaultState.current = _defaultState;\n // Controller's Zustand store\n const store = React.useMemo(\n () =>\n createStore<InternalControllerState<T, TState>>()(\n subscribeWithSelector(\n () =>\n ({\n formManager,\n schema: schemaInfo.schema,\n path: valuePath,\n schemaPath: schemaInfo.path,\n observingDescendants,\n ...UNINITIALIZED_CONTROLLER_STATE,\n ...defaultState.current,\n }) as any,\n ),\n ),\n [\n formManager,\n observingDescendants,\n schemaInfo.path,\n schemaInfo.schema,\n valuePath,\n ],\n );\n\n // Controller functions:\n const controller: Controller<T, TState> = React.useMemo(\n () => ({\n getState: () => unwrapStateValue(store.getState()),\n _setState: (state) => store.setState(state as never),\n subscribe: (selector, listener, options) =>\n store.subscribe(\n (state) => selector(unwrapStateValue(state)),\n listener,\n options,\n ),\n useState: (selector?: any, options?: any) => {\n const hasSelector = typeof selector === \"function\";\n const result = useStore(\n store,\n useEqualityFn(\n (state) =>\n hasSelector ? selector(unwrapStateValue(state)) : state,\n hasSelector ? options?.equalityFn : shallow,\n ),\n );\n return hasSelector\n ? result\n : unwrapStateValue(result as InternalControllerState);\n },\n useFormManager: () => useStore(store, (state) => state.formManager),\n useSchema: () => useStore(store, (state) => state.schema),\n usePath: () => useStore(store, (state) => state.path),\n useSchemaPath: () => useStore(store, (state) => state.schemaPath),\n useObservingDescendants: () =>\n useStore(store, (state) => state.observingDescendants),\n useInitialized: () => useStore(store, (state) => state.initialized),\n useExists: () => useStore(store, (state) => state.exists),\n useValue: () => useStore(store, (state) => state.value)?.[0],\n useDirty: () => useStore(store, (state) => state.dirty),\n useTouched: () => useStore(store, (state) => state.touched),\n useIssues: () => useStore(store, (state) => state.issues),\n useValidationStatus: () =>\n useStore(store, (state) => state.validationStatus),\n useDisplayStatus: () => useStore(store, (state) => state.displayStatus),\n get: <TValue, TResult>(\n pathOrValueHandler:\n | Path\n | string\n | ((value: T) => TResult | PromiseLike<TResult>),\n valueHandler?: (value: TValue) => TResult | PromiseLike<TResult>,\n ) => {\n const { formManager, path: valuePath } = store.getState();\n return valueHandler !== undefined\n ? formManager.get(\n valuePath.resolve(pathOrValueHandler as Path | string),\n valueHandler,\n )\n : formManager.get(\n valuePath,\n pathOrValueHandler as (\n value: T,\n ) => TResult | PromiseLike<TResult>,\n );\n },\n getClone: <TValue>(path?: Path | string): CancellablePromise<TValue> => {\n const { formManager, path: valuePath } = store.getState();\n return formManager.getClone(\n path != null ? valuePath.resolve(path) : valuePath,\n );\n },\n set: (pathOrToSet: Path | string | T, toSet?: unknown) => {\n const { formManager, path: valuePath } = store.getState();\n return toSet !== undefined\n ? formManager.set(\n valuePath.resolve(pathOrToSet as Path | string),\n toSet,\n )\n : formManager.set(valuePath, pathOrToSet);\n },\n reset: (path?: Path | string) => {\n const { formManager, path: valuePath } = store.getState();\n return formManager.reset(\n path != null ? valuePath.resolve(path) : valuePath,\n );\n },\n remove: (path?: Path | string) => {\n const { formManager, path: valuePath } = store.getState();\n return formManager.remove(\n path != null ? valuePath.resolve(path) : valuePath,\n );\n },\n validate: (path: Path | string = Path.CURRENT_DEEP) => {\n const { formManager, path: valuePath } = store.getState();\n return formManager.validate(\n path != null ? valuePath.resolve(path) : valuePath,\n );\n },\n setDirty: (path?: Path | string) => {\n const { formManager, path: valuePath } = store.getState();\n return formManager.setDirty(\n path != null ? valuePath.resolve(path) : valuePath,\n );\n },\n setPristine: (path?: Path | string) => {\n const { formManager, path: valuePath } = store.getState();\n return formManager.setPristine(\n path != null ? valuePath.resolve(path) : valuePath,\n );\n },\n setTouched: (path?: Path | string) => {\n const { formManager, path: valuePath } = store.getState();\n return formManager.setTouched(\n path != null ? valuePath.resolve(path) : valuePath,\n );\n },\n setUntouched: (path?: Path | string) => {\n const { formManager, path: valuePath } = store.getState();\n return formManager.setUntouched(\n path != null ? valuePath.resolve(path) : valuePath,\n );\n },\n }),\n [store],\n );\n\n // Store latest values and listeners used during API calls or event handling\n const latestValues = useLatestValues({\n onInitialized,\n onUninitialized,\n onFormManagerEvent,\n onValueChange,\n onValidationStatusChange,\n onDisplayStatusChange,\n onDirtyStatusChange,\n onTouchedStatusChange,\n });\n\n // Initialise controller\n React.useEffect(() => {\n if (!enabled) {\n return;\n }\n\n let cleanedUp = false;\n let unsubscribe: (() => CancellablePromise<void>) | undefined = undefined;\n const initPromise = formManager\n .info<T, void>(valuePath, async (infoIterable) => {\n if (cleanedUp) {\n return;\n }\n\n unsubscribe = await formManager.subscribe(resolvedPath, (event) => {\n // Unsubscribing is asynchronous, so it's possible that an event is\n // emitted after cleaning up, but before unsubscribing, which we\n // should ignore\n if (cleanedUp) {\n return;\n }\n\n let specificEventHandlerResult: void | PromiseLike<void> = undefined;\n if (event instanceof ValueEvent) {\n if (!observingDescendants || event.path.equals(valuePath)) {\n if (event instanceof ValueEvent.Init) {\n store.setState({\n initialized: true,\n exists: true,\n value: [event.value as T],\n dirty: false,\n touched: false,\n issues: [],\n validationStatus: \"unvalidated\",\n displayStatus: \"valid\",\n } as never);\n } else if (event instanceof ValueEvent.Destroy) {\n store.setState(NONEXISTENT_VALUE_CONTROLLER_STATE as never);\n } else {\n // Force Zustand update even if value's identity hasn't changed\n store.setState({ value: [event.value as T] } as never);\n }\n } else {\n // Force Zustand update even if value's identity hasn't changed\n store.setState(\n (state) =>\n ({\n value: state.value ? [state.value[0]] : undefined,\n }) as never,\n );\n }\n specificEventHandlerResult = latestValues.onValueChange?.(\n event,\n controller.getState() as never,\n );\n } else if (event instanceof StateEvent.ValidationChange) {\n if (!observingDescendants || event.path.equals(valuePath)) {\n store.setState({\n issues: event.issues,\n validationStatus: event.status,\n } as never);\n }\n specificEventHandlerResult =\n latestValues.onValidationStatusChange?.(\n event,\n controller.getState() as never,\n );\n } else if (event instanceof StateEvent.DisplayChange) {\n if (!observingDescendants || event.path.equals(valuePath)) {\n store.setState({ displayStatus: event.status } as never);\n }\n specificEventHandlerResult = latestValues.onDisplayStatusChange?.(\n event,\n controller.getState() as never,\n );\n } else if (event instanceof StateEvent.DirtyChange) {\n if (!observingDescendants || event.path.equals(valuePath)) {\n store.setState({ dirty: event.status } as never);\n }\n specificEventHandlerResult = latestValues.onDirtyStatusChange?.(\n event,\n controller.getState() as never,\n );\n } else if (event instanceof StateEvent.TouchedChange) {\n if (!observingDescendants || event.path.equals(valuePath)) {\n store.setState({ touched: event.status } as never);\n }\n specificEventHandlerResult = latestValues.onTouchedStatusChange?.(\n event,\n controller.getState() as never,\n );\n }\n\n // Results of handling the event\n const formManagerEventHandlerResult =\n latestValues.onFormManagerEvent?.(\n event,\n controller.getState() as never,\n );\n\n // Return a promise if either of the handler results is a promise\n if (\n isPromiseLike(formManagerEventHandlerResult) ||\n isPromiseLike(specificEventHandlerResult)\n ) {\n return Promise.all([\n formManagerEventHandlerResult,\n specificEventHandlerResult,\n ]).then(() => {});\n }\n });\n\n const info = (await arrayFromAsync(infoIterable))[0];\n store.setState(\n (info\n ? {\n initialized: true,\n exists: true,\n value: [info.value],\n dirty: info.dirty,\n touched: info.touched,\n issues: info.issues,\n validationStatus: info.validationStatus,\n displayStatus: info.displayStatus,\n }\n : NONEXISTENT_VALUE_CONTROLLER_STATE) as never,\n );\n return latestValues.onInitialized?.(controller.getState() as never);\n })\n .catch(ignorePromiseCancellationException);\n\n return () => {\n cleanedUp = true;\n initPromise?.cancel(\n `Clean up 'useEffect' access to info of '${valuePath.toString()}'.`,\n );\n void unsubscribe?.();\n store.setState(UNINITIALIZED_CONTROLLER_STATE as never);\n latestValues.onUninitialized?.(controller.getState() as never);\n };\n }, [\n controller,\n enabled,\n formManager,\n latestValues,\n observingDescendants,\n resolvedPath,\n schemaInfo.path,\n schemaInfo.schema,\n store,\n valuePath,\n ]);\n\n return controller;\n}\n\n/**\n * Unwraps the value within the controller's state.\n */\nfunction unwrapStateValue<\n T = unknown,\n TState extends ControllerState<T> = ControllerState<T>,\n>(state: InternalControllerState<T, TState>) {\n return { ...state, value: state.value?.[0] } as TState;\n}\n","/**\n * Determines the equality between {@link v1} and {@link v2} via\n * {@link Object.is} or, when `!Object.is(v1, v2)`, via an `equals` function in\n * {@link v1} (when defined).\n * @param v1 First value being compared (if `!Object.is(v1, v2)` and `v1.equals`\n * is a function, then `v1.equals(v2)` is used to determine the equality).\n * @param v2 Second value being compared.\n * @returns Whether {@link v1} and {@link v2} are considered equal.\n */\nexport function equals(v1: any, v2: any): boolean {\n return (\n Object.is(v1, v2) || (typeof v1?.equals === \"function\" && !!v1.equals(v2))\n );\n}\n","import * as React from \"react\";\n\n/**\n * Hook providing access to the value of a provided argument in the previous\n * render.\n * @param value Value to store and return on the next render.\n * @returns The value of the provided argument in the previous render or\n * `undefined` during the first render.\n */\nexport function usePrevious<T>(value: T): T | undefined {\n const ref = React.useRef<T>();\n React.useEffect(() => {\n ref.current = value;\n }, [value]);\n return ref.current;\n}\n","import { FormManager, Schema, SchemaKt, ValidationMode } from \"@kform/core\";\nimport * as React from \"react\";\n\nimport { equals } from \"../utils/equals\";\nimport { usePrevious } from \"../utils/usePrevious\";\n\n/**\n * Hook that creates a new form manager given a {@link schema} and\n * [external contexts]{@link externalContexts}.\n *\n * Note that a new instance of a form manager will be created every time\n * {@link schema} changes; the external contexts and validation mode, however,\n * may change over time and the hook will simply update the external contexts of\n * the current form manager.\n * @param schema Schema of the form, used to create a new instance of a form\n * manager.\n * @param initialValue Initial form value.\n * @param externalContexts External contexts available to validations of\n * {@link schema}.\n * @param validationMode Validation mode (automatic or manual validation).\n * @returns New form manager.\n */\nexport function useNewFormManager(\n schema: Schema | SchemaKt,\n initialValue?: unknown,\n externalContexts?: Record<string, unknown>,\n validationMode: ValidationMode = \"auto\",\n): FormManager {\n const initialValueRef = React.useRef(initialValue);\n const formManager = React.useMemo(\n () =>\n new FormManager(\n schema,\n initialValueRef.current,\n undefined,\n undefined,\n false,\n ),\n [schema],\n );\n React.useEffect(() => {\n void formManager.init(externalContexts, validationMode);\n return () => void formManager.destroy();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [formManager]);\n const prevFormManager = usePrevious(formManager);\n\n // Manage external contexts without recreating the form manager\n const prevExternalContexts = usePrevious(externalContexts);\n React.useEffect(() => {\n if (formManager === prevFormManager) {\n // Add new external contexts\n if (externalContexts) {\n for (const contextName of Object.keys(externalContexts)) {\n const externalContext = externalContexts[contextName];\n if (\n externalContext !== undefined &&\n (!prevExternalContexts ||\n !equals(externalContext, prevExternalContexts[contextName]))\n ) {\n void formManager.setExternalContext(contextName, externalContext);\n }\n }\n }\n // Remove old (no longer provided) external contexts\n if (prevExternalContexts) {\n for (const contextName of Object.keys(prevExternalContexts)) {\n if (\n prevExternalContexts[contextName] !== undefined &&\n (!externalContexts || externalContexts[contextName] === undefined)\n ) {\n void formManager.removeExternalContext(contextName);\n }\n }\n }\n }\n }, [externalContexts, formManager, prevExternalContexts, prevFormManager]);\n\n // Manage validation mode without recreating the form manager\n const prevValidationMode = usePrevious(validationMode);\n React.useEffect(() => {\n if (\n formManager === prevFormManager &&\n validationMode !== prevValidationMode\n ) {\n void formManager.setValidationMode(validationMode);\n }\n }, [formManager, validationMode, prevValidationMode, prevFormManager]);\n\n return formManager;\n}\n","import {\n AbsolutePath,\n AutoValidationStatus,\n CancellablePromise,\n convertIssuesTableRowIndicesToIds,\n FormManager,\n isList,\n isLocatedValidationIssueKt,\n listableSize,\n listableToArray,\n LocatedValidationIssue,\n locatedValidationIssueKtToJs,\n LocatedValidationWarning,\n Path,\n Schema,\n SchemaKt,\n SealedLocatedValidationIssue,\n sliceList,\n ValidationMode,\n} from \"@kform/core\";\nimport * as React from \"react\";\n\nimport { ignorePromiseCancellationException } from \"../utils/ignorePromiseCancellationException\";\nimport { DistributedOmit, MaybePromise } from \"../utils/typeUtils\";\nimport { useLatestValues } from \"../utils/useLatestValues\";\nimport {\n Controller,\n ControllerOptions,\n ControllerState,\n useController,\n} from \"./useController\";\nimport { useNewFormManager } from \"./useNewFormManager\";\n\n/**\n * Default message passed when preventing the \"before unload\" event.\n */\nexport const DEFAULT_CONFIRM_UNLOAD_MESSAGE = \"Are you sure you want to leave?\";\n\n/**\n * Type of external issues accepted by the form manager's `addExternalIssues`\n * method.\n */\ntype ExternalValidationIssues = Parameters<FormManager[\"addExternalIssues\"]>[0];\n\n/**\n * An object which is event-like.\n */\ntype EventLike = Pick<Event, \"preventDefault\" | \"defaultPrevented\">;\n\n/**\n * Options available to the {@link useForm} hook.\n */\nexport type FormOptions<T = unknown, TSubmitResult = unknown> = DistributedOmit<\n ControllerOptions<T, FormControllerState<T>>,\n \"formManager\"\n> &\n FormOwnOptions<T, TSubmitResult>;\n\n/**\n * Own options available to the {@link useForm} hook.\n */\nexport interface FormOwnOptions<T = unknown, TSubmitResult = unknown>\n extends SubmitOptions<T, TSubmitResult> {\n /**\n * Initial form value.\n */\n initialValue?: T;\n /**\n * External contexts available to validations. This value may change over time\n * and will not cause a new form manager to be instantiated.\n */\n externalContexts?: Record<string, unknown>;\n /**\n * Form manager's validation mode. This value may change over time and will\n * not cause a new form manager to be instantiated.\n */\n validationMode?: ValidationMode;\n /**\n * Whether to display a confirmation that the page should be unloaded when the\n * form is dirty.\n * @default true\n */\n confirmUnloadWhenDirty?: boolean;\n /**\n * Message to provide when confirming a page unload due to the dirty status of\n * the form.\n *\n * Note that most recent browsers will not honour this string and will instead\n * display a predefined message (see:\n * https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#compatibility_notes).\n */\n confirmUnloadMessage?: string;\n /**\n * Function called during reset, before actually resetting the form. Calling\n * [preventDefault()]{@link event.preventDefault} on the event will prevent\n * the reset from occurring.\n * @param event Original `onReset` event.\n */\n onReset?: (event?: EventLike) => MaybePromise<void>;\n}\n\n/**\n * Controller used to read and control the root form value, exposing properties\n * that should be set on a {@link HTMLFormElement <form>} element.\n */\nexport type FormController<T = unknown, TSubmitResult = unknown> = Controller<\n T,\n FormControllerState<T>\n> &\n FormOwnController<T, TSubmitResult>;\n\n/**\n * Form's own controller.\n */\nexport interface FormOwnController<T = unknown, TSubmitResult = unknown> {\n /**\n * Function used to submit the form.\n * @param event Event which caused the submission.\n * @param options Options used when submitting the form.\n */\n readonly submit: (<TOptionsSubmitResult = TSubmitResult>(\n event: EventLike,\n options?: SubmitOptions<T, TOptionsSubmitResult>,\n ) => Promise<void>) &\n (<TOptionsSubmitResult = TSubmitResult>(\n options?: SubmitOptions<T, TOptionsSubmitResult>,\n ) => Promise<void>);\n /**\n * Hook returning the status of the form's automatic validations.\n */\n readonly useAutoValidationStatus: () => AutoValidationStatus;\n /**\n * Hook returning whether the form is currently being submitted.\n */\n readonly useSubmitting: () => boolean;\n /**\n * Hook returning whether the form is currently being reset.\n */\n readonly useResetting: () => boolean;\n /**\n * Properties to set on a {@link HTMLFormElement <form>} element.\n */\n readonly formProps: FormElementProps;\n}\n\n/**\n * Options available when submitting a value.\n */\nexport interface SubmitOptions<T = unknown, TSubmitResult = unknown> {\n /**\n * Function called during submission when the form value to submit is locally\n * valid (no validation errors were found), or always when `validateOnSubmit`\n * is set to `false`.\n *\n * This function may (possibly asynchronously) return two types of values, or\n * throw an error:\n * - Function returns external validation issues: the submission is considered\n * invalid (even if all returned issues are warnings). Returned issues that\n * aren't already in the form manager are added to it via\n * {@link FormManager.addExternalIssues}. `onInvalidSubmit` is subsequently\n * called with the returned issues.\n * - Function returns any other value: the submission is considered\n * successful. `onSuccessfulSubmit` will be called with the returned value\n * after setting the form as pristine (unless\n * `setPristineOnSuccessfulSubmit` is set to `false`).\n * - Function throws: the submission is considered to have failed.\n * `onFailedSubmit` will be called with the thrown error.\n * @param value Form value to submit.\n * @param warnings List of form warnings. `undefined` when `validateOnSubmit`\n * is set to `false`.\n * @param event Original event (with the default behaviour already prevented).\n * @returns External validation issues, or any other value (including\n * `undefined`) to indicate success; or a promise which resolves to such\n * values.\n */\n onSubmit?: (\n value: T,\n warnings?: LocatedValidationWarning[],\n event?: EventLike,\n ) => MaybePromise<ExternalValidationIssues | TSubmitResult>;\n /**\n * Function called during submission when the form value to submit is locally\n * invalid (validation errors were found), or after `onSubmit` if it returns\n * external validation issues.\n * @param issues List of form issues.\n * @param event Original event (with the default behaviour already prevented).\n */\n onInvalidSubmit?: (\n issues: SealedLocatedValidationIssue[],\n event?: EventLike,\n ) => void;\n /**\n * Function called after a successful submission with the result of\n * `onSubmit`, when this result isn't external validation