@kform/react
Version:
React integration for KForm.
1,497 lines • 55.8 kB
JavaScript
import { jsx } from "react/jsx-runtime";
import { Path, PromiseCancellationException, AbsolutePath, AbsolutePathFragment, ValueEvent, StateEvent, FormManager, convertIssuesTableRowIndicesToIds, listableToArray, locatedValidationIssueKtToJs, isLocatedValidationIssueKt, isList, listableSize, sliceList, LocatedValidationIssue, isComputedSchema, kFormFileToJsFile, emptyPlaceholderKFormFile, jsFileToKFormFile, compareSchemaPaths, arrayToTable, arrayToList, codeToChar, stringToLong, stringToBigInteger, stringToBigDecimal, charToCode, stringToInstant, stringToLocalDate, stringToLocalDateTime, setLogLevel } from "@kform/core";
import * as React from "react";
import { createStore, useStore } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import { shallow } from "zustand/shallow";
import { shallow as shallow2 } from "zustand/shallow";
import fromExponential from "from-exponential";
const FormContext = React.createContext(
null
);
class AtPathError extends Error {
/** Path at which the error occurred. */
path;
constructor(path, message, options) {
super(`At '${path.toString()}': ${message}`, options);
this.name = this.constructor.name;
this.path = path;
}
}
class InvalidPathError extends AtPathError {
constructor(path, message) {
super(path, message);
this.name = this.constructor.name;
}
}
class NoFormContextError extends Error {
constructor() {
super("No form context found");
this.name = this.constructor.name;
}
}
class NoFormManagerError extends Error {
constructor() {
super("No form manager found");
this.name = this.constructor.name;
}
}
class NoFormControllerError extends Error {
constructor() {
super("No form controller found");
this.name = this.constructor.name;
}
}
function useFormContext() {
const formContext = React.useContext(FormContext);
if (!formContext) {
throw new NoFormContextError();
}
return formContext;
}
function useResolvedPath(path = Path.CURRENT, currentPath) {
const formContext = React.useContext(FormContext);
currentPath ??= formContext?.currentPath;
if (currentPath == null) {
throw new NoFormContextError();
}
const stableAbsolutePathRef = React.useRef();
const absolutePath = React.useMemo(
() => currentPath.resolve(path),
[currentPath, path]
);
if (!absolutePath.equals(stableAbsolutePathRef.current)) {
stableAbsolutePathRef.current = absolutePath;
}
return stableAbsolutePathRef.current;
}
function CurrentPath({
path = Path.CURRENT,
children
}) {
const formContext = useFormContext();
const resolvedPath = useResolvedPath(path);
const newFormContext = React.useMemo(
() => ({
formManager: formContext.formManager,
currentPath: resolvedPath,
controller: formContext.controller
}),
[formContext.controller, formContext.formManager, resolvedPath]
);
return /* @__PURE__ */ jsx(FormContext.Provider, { value: newFormContext, children });
}
function ignorePromiseCancellationException(caughtError) {
if (!(caughtError instanceof PromiseCancellationException)) {
throw caughtError;
}
}
function useLatestValues(values) {
const valuesRef = React.useRef({});
Object.assign(valuesRef.current, values);
return valuesRef.current;
}
async function arrayFromAsync(iterable) {
const array = [];
for await (const el of iterable) {
array.push(el);
}
return array;
}
function isPromiseLike(value) {
return typeof value?.then === "function";
}
const INITIAL = Symbol();
function useEqualityFn(selector, equalityFn = Object.is) {
const latest = React.useRef(INITIAL);
return (state) => {
const next = selector(state);
return latest.current !== INITIAL && equalityFn(latest.current, next) ? latest.current : latest.current = next;
};
}
function useFormManager(formManager) {
const formContext = React.useContext(FormContext);
const relevantFormManager = formManager ?? formContext?.formManager;
if (relevantFormManager == null) {
throw new NoFormManagerError();
}
return relevantFormManager;
}
const UNINITIALIZED_CONTROLLER_STATE = {
initialized: false,
exists: void 0,
value: void 0,
dirty: void 0,
touched: void 0,
issues: void 0,
validationStatus: void 0,
displayStatus: void 0
};
const NONEXISTENT_VALUE_CONTROLLER_STATE = {
initialized: true,
exists: false,
value: void 0,
dirty: void 0,
touched: void 0,
issues: void 0,
validationStatus: void 0,
displayStatus: void 0
};
function useController(path, {
formManager: formManagerOption,
enabled = true,
_defaultState,
onInitialized,
onUninitialized,
onFormManagerEvent,
onValueChange,
onValidationStatusChange,
onDisplayStatusChange,
onDirtyStatusChange,
onTouchedStatusChange
} = {}) {
const formManager = useFormManager(formManagerOption);
const resolvedPath = useResolvedPath(
path,
formManagerOption != null ? AbsolutePath.ROOT : void 0
);
const observingDescendants = resolvedPath.lastFragment === AbsolutePathFragment.RecursiveWildcard;
const valuePath = React.useMemo(
() => observingDescendants ? new AbsolutePath(resolvedPath.fragments.slice(0, -1)) : resolvedPath,
[resolvedPath, observingDescendants]
);
if (!formManager.isValidPath(valuePath)) {
throw new Error(`Invalid path: '${resolvedPath.toString()}'.`);
}
if (valuePath.fragments.some(
(frag) => !(frag instanceof AbsolutePathFragment.Id)
)) {
throw new Error(
"Controller path must only contain ids (except for the last fragment, which may be a recursive wildcard)."
);
}
const schemaInfo = React.useMemo(
() => Array.from(formManager.schemaInfo(valuePath))[0],
[formManager, valuePath]
);
const defaultState = React.useRef(_defaultState);
defaultState.current = _defaultState;
const store = React.useMemo(
() => createStore()(
subscribeWithSelector(
() => ({
formManager,
schema: schemaInfo.schema,
path: valuePath,
schemaPath: schemaInfo.path,
observingDescendants,
...UNINITIALIZED_CONTROLLER_STATE,
...defaultState.current
})
)
),
[
formManager,
observingDescendants,
schemaInfo.path,
schemaInfo.schema,
valuePath
]
);
const controller = React.useMemo(
() => ({
getState: () => unwrapStateValue(store.getState()),
_setState: (state) => store.setState(state),
subscribe: (selector, listener, options) => store.subscribe(
(state) => selector(unwrapStateValue(state)),
listener,
options
),
useState: (selector, options) => {
const hasSelector = typeof selector === "function";
const result = useStore(
store,
useEqualityFn(
(state) => hasSelector ? selector(unwrapStateValue(state)) : state,
hasSelector ? options?.equalityFn : shallow
)
);
return hasSelector ? result : unwrapStateValue(result);
},
useFormManager: () => useStore(store, (state) => state.formManager),
useSchema: () => useStore(store, (state) => state.schema),
usePath: () => useStore(store, (state) => state.path),
useSchemaPath: () => useStore(store, (state) => state.schemaPath),
useObservingDescendants: () => useStore(store, (state) => state.observingDescendants),
useInitialized: () => useStore(store, (state) => state.initialized),
useExists: () => useStore(store, (state) => state.exists),
useValue: () => useStore(store, (state) => state.value)?.[0],
useDirty: () => useStore(store, (state) => state.dirty),
useTouched: () => useStore(store, (state) => state.touched),
useIssues: () => useStore(store, (state) => state.issues),
useValidationStatus: () => useStore(store, (state) => state.validationStatus),
useDisplayStatus: () => useStore(store, (state) => state.displayStatus),
get: (pathOrValueHandler, valueHandler) => {
const { formManager: formManager2, path: valuePath2 } = store.getState();
return valueHandler !== void 0 ? formManager2.get(
valuePath2.resolve(pathOrValueHandler),
valueHandler
) : formManager2.get(
valuePath2,
pathOrValueHandler
);
},
getClone: (path2) => {
const { formManager: formManager2, path: valuePath2 } = store.getState();
return formManager2.getClone(
path2 != null ? valuePath2.resolve(path2) : valuePath2
);
},
set: (pathOrToSet, toSet) => {
const { formManager: formManager2, path: valuePath2 } = store.getState();
return toSet !== void 0 ? formManager2.set(
valuePath2.resolve(pathOrToSet),
toSet
) : formManager2.set(valuePath2, pathOrToSet);
},
reset: (path2) => {
const { formManager: formManager2, path: valuePath2 } = store.getState();
return formManager2.reset(
path2 != null ? valuePath2.resolve(path2) : valuePath2
);
},
remove: (path2) => {
const { formManager: formManager2, path: valuePath2 } = store.getState();
return formManager2.remove(
path2 != null ? valuePath2.resolve(path2) : valuePath2
);
},
validate: (path2 = Path.CURRENT_DEEP) => {
const { formManager: formManager2, path: valuePath2 } = store.getState();
return formManager2.validate(
path2 != null ? valuePath2.resolve(path2) : valuePath2
);
},
setDirty: (path2) => {
const { formManager: formManager2, path: valuePath2 } = store.getState();
return formManager2.setDirty(
path2 != null ? valuePath2.resolve(path2) : valuePath2
);
},
setPristine: (path2) => {
const { formManager: formManager2, path: valuePath2 } = store.getState();
return formManager2.setPristine(
path2 != null ? valuePath2.resolve(path2) : valuePath2
);
},
setTouched: (path2) => {
const { formManager: formManager2, path: valuePath2 } = store.getState();
return formManager2.setTouched(
path2 != null ? valuePath2.resolve(path2) : valuePath2
);
},
setUntouched: (path2) => {
const { formManager: formManager2, path: valuePath2 } = store.getState();
return formManager2.setUntouched(
path2 != null ? valuePath2.resolve(path2) : valuePath2
);
}
}),
[store]
);
const latestValues = useLatestValues({
onInitialized,
onUninitialized,
onFormManagerEvent,
onValueChange,
onValidationStatusChange,
onDisplayStatusChange,
onDirtyStatusChange,
onTouchedStatusChange
});
React.useEffect(() => {
if (!enabled) {
return;
}
let cleanedUp = false;
let unsubscribe = void 0;
const initPromise = formManager.info(valuePath, async (infoIterable) => {
if (cleanedUp) {
return;
}
unsubscribe = await formManager.subscribe(resolvedPath, (event) => {
if (cleanedUp) {
return;
}
let specificEventHandlerResult = void 0;
if (event instanceof ValueEvent) {
if (!observingDescendants || event.path.equals(valuePath)) {
if (event instanceof ValueEvent.Init) {
store.setState({
initialized: true,
exists: true,
value: [event.value],
dirty: false,
touched: false,
issues: [],
validationStatus: "unvalidated",
displayStatus: "valid"
});
} else if (event instanceof ValueEvent.Destroy) {
store.setState(NONEXISTENT_VALUE_CONTROLLER_STATE);
} else {
store.setState({ value: [event.value] });
}
} else {
store.setState(
(state) => ({
value: state.value ? [state.value[0]] : void 0
})
);
}
specificEventHandlerResult = latestValues.onValueChange?.(
event,
controller.getState()
);
} else if (event instanceof StateEvent.ValidationChange) {
if (!observingDescendants || event.path.equals(valuePath)) {
store.setState({
issues: event.issues,
validationStatus: event.status
});
}
specificEventHandlerResult = latestValues.onValidationStatusChange?.(
event,
controller.getState()
);
} else if (event instanceof StateEvent.DisplayChange) {
if (!observingDescendants || event.path.equals(valuePath)) {
store.setState({ displayStatus: event.status });
}
specificEventHandlerResult = latestValues.onDisplayStatusChange?.(
event,
controller.getState()
);
} else if (event instanceof StateEvent.DirtyChange) {
if (!observingDescendants || event.path.equals(valuePath)) {
store.setState({ dirty: event.status });
}
specificEventHandlerResult = latestValues.onDirtyStatusChange?.(
event,
controller.getState()
);
} else if (event instanceof StateEvent.TouchedChange) {
if (!observingDescendants || event.path.equals(valuePath)) {
store.setState({ touched: event.status });
}
specificEventHandlerResult = latestValues.onTouchedStatusChange?.(
event,
controller.getState()
);
}
const formManagerEventHandlerResult = latestValues.onFormManagerEvent?.(
event,
controller.getState()
);
if (isPromiseLike(formManagerEventHandlerResult) || isPromiseLike(specificEventHandlerResult)) {
return Promise.all([
formManagerEventHandlerResult,
specificEventHandlerResult
]).then(() => {
});
}
});
const info = (await arrayFromAsync(infoIterable))[0];
store.setState(
info ? {
initialized: true,
exists: true,
value: [info.value],
dirty: info.dirty,
touched: info.touched,
issues: info.issues,
validationStatus: info.validationStatus,
displayStatus: info.displayStatus
} : NONEXISTENT_VALUE_CONTROLLER_STATE
);
return latestValues.onInitialized?.(controller.getState());
}).catch(ignorePromiseCancellationException);
return () => {
cleanedUp = true;
initPromise?.cancel(
`Clean up 'useEffect' access to info of '${valuePath.toString()}'.`
);
void unsubscribe?.();
store.setState(UNINITIALIZED_CONTROLLER_STATE);
latestValues.onUninitialized?.(controller.getState());
};
}, [
controller,
enabled,
formManager,
latestValues,
observingDescendants,
resolvedPath,
schemaInfo.path,
schemaInfo.schema,
store,
valuePath
]);
return controller;
}
function unwrapStateValue(state) {
return { ...state, value: state.value?.[0] };
}
function equals(v1, v2) {
return Object.is(v1, v2) || typeof v1?.equals === "function" && !!v1.equals(v2);
}
function usePrevious(value) {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function useNewFormManager(schema, initialValue, externalContexts, validationMode = "auto") {
const initialValueRef = React.useRef(initialValue);
const formManager = React.useMemo(
() => new FormManager(
schema,
initialValueRef.current,
void 0,
void 0,
false
),
[schema]
);
React.useEffect(() => {
void formManager.init(externalContexts, validationMode);
return () => void formManager.destroy();
}, [formManager]);
const prevFormManager = usePrevious(formManager);
const prevExternalContexts = usePrevious(externalContexts);
React.useEffect(() => {
if (formManager === prevFormManager) {
if (externalContexts) {
for (const contextName of Object.keys(externalContexts)) {
const externalContext = externalContexts[contextName];
if (externalContext !== void 0 && (!prevExternalContexts || !equals(externalContext, prevExternalContexts[contextName]))) {
void formManager.setExternalContext(contextName, externalContext);
}
}
}
if (prevExternalContexts) {
for (const contextName of Object.keys(prevExternalContexts)) {
if (prevExternalContexts[contextName] !== void 0 && (!externalContexts || externalContexts[contextName] === void 0)) {
void formManager.removeExternalContext(contextName);
}
}
}
}
}, [externalContexts, formManager, prevExternalContexts, prevFormManager]);
const prevValidationMode = usePrevious(validationMode);
React.useEffect(() => {
if (formManager === prevFormManager && validationMode !== prevValidationMode) {
void formManager.setValidationMode(validationMode);
}
}, [formManager, validationMode, prevValidationMode, prevFormManager]);
return formManager;
}
const DEFAULT_CONFIRM_UNLOAD_MESSAGE = "Are you sure you want to leave?";
function useForm(schema, {
_defaultState,
initialValue,
externalContexts,
validationMode,
confirmUnloadWhenDirty = process.env.NODE_ENV === "production",
confirmUnloadMessage,
onSubmit,
onInvalidSubmit,
onSuccessfulSubmit,
onFailedSubmit,
setTouchedOnSubmit = true,
validateOnSubmit = true,
setPristineOnSuccessfulSubmit = true,
convertExternalIssuesTableRowIndicesToIds = false,
onReset,
...options
} = {}) {
const formManager = useNewFormManager(
schema,
initialValue,
externalContexts,
validationMode
);
const controller = useController(
AbsolutePath.ROOT,
{
formManager,
_defaultState: {
autoValidationStatus: formManager.autoValidationStatus,
submitting: false,
resetting: false,
..._defaultState
},
...options
}
);
const latestValues = useLatestValues({
confirmUnloadMessage,
onSubmit,
onInvalidSubmit,
onSuccessfulSubmit,
onFailedSubmit,
setTouchedOnSubmit,
validateOnSubmit,
setPristineOnSuccessfulSubmit,
convertExternalIssuesTableRowIndicesToIds,
onReset
});
const handleSubmit = React.useCallback(
async (event, {
onSubmit: onSubmit2 = latestValues.onSubmit,
onInvalidSubmit: onInvalidSubmit2 = latestValues.onInvalidSubmit,
onSuccessfulSubmit: onSuccessfulSubmit2 = latestValues.onSuccessfulSubmit,
onFailedSubmit: onFailedSubmit2 = latestValues.onFailedSubmit,
setTouchedOnSubmit: setTouchedOnSubmit2 = latestValues.setTouchedOnSubmit,
validateOnSubmit: validateOnSubmit2 = latestValues.validateOnSubmit,
setPristineOnSuccessfulSubmit: setPristineOnSuccessfulSubmit2 = latestValues.setPristineOnSuccessfulSubmit,
convertExternalIssuesTableRowIndicesToIds: convertExternalIssuesTableRowIndicesToIds2 = latestValues.convertExternalIssuesTableRowIndicesToIds
} = {}) => {
if (!onSubmit2) {
throw new Error(
"Missing `onSubmit` implementation. The `onSubmit` function may be provided as a property of the `Form` component, as an option of the `useForm` hook, or directly as an option of the `submit` function."
);
}
event?.preventDefault();
const { initialized, exists, schema: schema2 } = controller.getState();
if (initialized && exists) {
controller._setState({ submitting: true });
try {
if (setTouchedOnSubmit2) {
await controller.setTouched(Path.CURRENT_DEEP);
}
const issues = validateOnSubmit2 ? await controller.validate(Path.CURRENT_DEEP) : void 0;
if (issues?.some((issue) => issue.severity === "error")) {
onInvalidSubmit2?.(issues, event);
return;
}
await controller.get(async (value) => {
const submitResult = await onSubmit2(
value,
issues,
event
);
if (isExternalValidationIssues(submitResult)) {
const externalIssues = convertExternalIssuesTableRowIndicesToIds2 ? await convertIssuesTableRowIndicesToIds(
submitResult,
schema2,
value
) : listableToArray(submitResult).map(
(issue) => isLocatedValidationIssueKt(issue) ? locatedValidationIssueKtToJs(issue) : issue
);
const newExternalIssues = issues === void 0 ? externalIssues : externalIssues.filter(
(externalIssue) => !issues.some((issue) => issue.equals(externalIssue))
);
if (newExternalIssues.length > 0) {
void formManager.addExternalIssues(newExternalIssues);
}
onInvalidSubmit2?.(externalIssues, event);
} else {
void Promise.resolve(
setPristineOnSuccessfulSubmit2 && formManager.setPristine(Path.CURRENT)
).then(() => onSuccessfulSubmit2?.(submitResult, event));
}
});
} catch (err) {
if (onFailedSubmit2) {
onFailedSubmit2(err, event);
} else {
throw err;
}
} finally {
controller._setState({ submitting: false });
}
}
},
[controller, formManager, latestValues]
);
const handleReset = React.useCallback(
async (event) => {
const { initialized, exists } = controller.getState();
await latestValues.onReset?.(event);
if (!event?.defaultPrevented) {
event?.preventDefault();
if (initialized && exists) {
controller._setState({ resetting: true });
try {
await controller.reset();
await Promise.all([
controller.setPristine(),
controller.setUntouched()
]);
} finally {
controller._setState({ resetting: false });
}
}
}
},
[controller, latestValues]
);
React.useEffect(() => {
let unsubscribe = void 0;
const subscribePromise = formManager.onAutoValidationStatusChange(
(autoValidationStatus) => controller._setState({ autoValidationStatus })
).then((fn) => unsubscribe = fn).catch(ignorePromiseCancellationException);
return () => {
subscribePromise.cancel(
"Clean up 'useEffect' subscription to auto validation status changes."
);
void unsubscribe?.();
};
}, [controller, formManager]);
React.useEffect(() => {
if (confirmUnloadWhenDirty) {
const handleBeforeUnload = (event) => {
if (controller.getState().dirty) {
event.preventDefault();
return event.returnValue = latestValues.confirmUnloadMessage || DEFAULT_CONFIRM_UNLOAD_MESSAGE;
}
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}
}, [confirmUnloadWhenDirty, controller, latestValues]);
const formProps = React.useMemo(
() => ({ noValidate: true, onSubmit: handleSubmit, onReset: handleReset }),
[handleReset, handleSubmit]
);
const ownController = React.useMemo(
() => ({
submit: (eventOrOptions, maybeOptions) => {
const [event, options2] = isEventLike(eventOrOptions) ? [eventOrOptions, maybeOptions] : [void 0, eventOrOptions];
return handleSubmit(event, options2);
},
useAutoValidationStatus: () => controller.useState((state) => state.autoValidationStatus),
useSubmitting: () => controller.useState((state) => state.submitting),
useResetting: () => controller.useState((state) => state.resetting),
formProps
}),
[controller, formProps, handleSubmit]
);
return React.useMemo(
() => ({ ...controller, ...ownController }),
[controller, ownController]
);
}
function isEventLike(value) {
return typeof value?.preventDefault === "function" && "defaultPrevented" in value;
}
function isExternalValidationIssues(value) {
const firstEl = isList(value) ? listableSize(value) > 0 ? sliceList(value, 0, 1)[0] : void 0 : Array.isArray(value) ? value[0] : void 0;
return firstEl != null && (firstEl instanceof LocatedValidationIssue || isLocatedValidationIssueKt(firstEl));
}
const Form = React.forwardRef(function Form2({
schema,
initialValue,
enabled,
_defaultState,
onInitialized,
onUninitialized,
onFormManagerEvent,
onValueChange,
onValidationStatusChange,
onDisplayStatusChange,
onDirtyStatusChange,
onTouchedStatusChange,
externalContexts,
validationMode,
confirmUnloadWhenDirty,
confirmUnloadMessage,
onSubmit,
onInvalidSubmit,
onSuccessfulSubmit,
onFailedSubmit,
setTouchedOnSubmit,
validateOnSubmit,
setPristineOnSuccessfulSubmit,
convertExternalIssuesTableRowIndicesToIds,
onReset,
...otherProps
}, forwardedRef) {
const formController = useForm(schema, {
initialValue,
enabled,
_defaultState,
onInitialized,
onUninitialized,
onFormManagerEvent,
onValueChange,
onValidationStatusChange,
onDisplayStatusChange,
onDirtyStatusChange,
onTouchedStatusChange,
externalContexts,
validationMode,
confirmUnloadWhenDirty,
confirmUnloadMessage,
onSubmit,
onInvalidSubmit,
onSuccessfulSubmit,
onFailedSubmit,
setTouchedOnSubmit,
validateOnSubmit,
setPristineOnSuccessfulSubmit,
convertExternalIssuesTableRowIndicesToIds,
onReset
});
const formManager = formController.useFormManager();
const formContext = React.useMemo(
() => ({
formManager,
currentPath: AbsolutePath.ROOT,
controller: formController
}),
[formController, formManager]
);
return /* @__PURE__ */ jsx(FormContext.Provider, { value: formContext, children: /* @__PURE__ */ jsx("form", { ...formController.formProps, ...otherProps, ref: forwardedRef }) });
});
function useFormatter(path = Path.CURRENT, {
_defaultState,
setFormattedValue,
format,
...options
}) {
const { onInitialized, onUninitialized, onValueChange } = options;
const latestOptions = useLatestValues({
setFormattedValue,
format,
onInitialized,
onUninitialized,
onValueChange
});
const controller = useController(path, {
...options,
_defaultState: {
formattedValue: void 0,
..._defaultState
},
onInitialized: (state) => {
const result = formatAndSetFormattedValue(
state.value,
latestOptions.format,
latestOptions.setFormattedValue,
controller
);
const handlerResult = latestOptions.onInitialized?.(state);
return isPromiseLike(result) ? Promise.all([result, handlerResult]).then(() => {
}) : handlerResult;
},
onUninitialized: (state) => {
controller._setState({ formattedValue: void 0 });
return latestOptions.onUninitialized?.(state);
},
onValueChange: (event, state) => {
const result = formatAndSetFormattedValue(
state.value,
latestOptions.format,
latestOptions.setFormattedValue,
controller
);
const handlerResult = latestOptions.onValueChange?.(event, state);
return isPromiseLike(result) ? Promise.all([result, handlerResult]).then(() => {
}) : handlerResult;
}
});
const ownController = React.useMemo(
() => ({
useFormattedValue: () => controller.useState((state) => state.formattedValue)
}),
[controller]
);
React.useEffect(() => {
const { initialized, value } = controller.getState();
if (initialized) {
void formatAndSetFormattedValue(
value,
format,
setFormattedValue,
controller
);
}
}, [controller, format, setFormattedValue]);
return React.useMemo(
() => ({ ...controller, ...ownController }),
[controller, ownController]
);
}
function formatAndSetFormattedValue(value, format, setFormattedValue, controller) {
const state = controller.getState();
const set = (formattedValue2) => {
controller._setState({ formattedValue: formattedValue2 });
return setFormattedValue?.(formattedValue2, state);
};
const formattedValue = format ? format(value, state) : value;
if (isPromiseLike(formattedValue)) {
return formattedValue.then(set);
} else {
return set(formattedValue);
}
}
function FormattedValue({
path,
children,
...props
}) {
const formContext = React.useContext(FormContext);
const formatterController = useFormatter(path, props);
const formManager = formatterController.useFormManager();
const currentPath = formatterController.usePath();
const newFormContext = React.useMemo(
() => ({
formManager,
currentPath,
controller: formContext?.formManager === formManager ? formContext.controller : void 0
}),
[
currentPath,
formContext?.controller,
formContext?.formManager,
formManager
]
);
const state = formatterController.useState(
(state2) => children ? state2 : {
initialized: state2.initialized,
formattedValue: state2.formattedValue
},
{ equalityFn: shallow }
);
return /* @__PURE__ */ jsx(FormContext.Provider, { value: newFormContext, children: children ? children(state) : state.initialized && (React.isValidElement(state.formattedValue) ? state.formattedValue : state.formattedValue?.toString()) });
}
function useInput(path = Path.CURRENT, {
setFormattedValue = defaultSetFormattedValue,
format,
parse,
...options
} = {}) {
const latestOptions = useLatestValues({ parse, setFormattedValue });
const inputRef = React.useRef(null);
const activeSetPromiseRef = React.useRef(
null
);
const formatterSetFormattedValue = React.useCallback(
(formattedValue, state) => {
if (activeSetPromiseRef.current == null) {
setFormattedValue(formattedValue, state, inputRef.current);
}
},
[setFormattedValue]
);
const formatterFormat = React.useCallback(
(value, state) => format ? format(value, state, inputRef.current) : value,
[format]
);
const controller = useFormatter(path, {
setFormattedValue: formatterSetFormattedValue,
format: formatterFormat,
...options
});
const handleChange = React.useCallback(
async (eventOrValue) => {
const {
schema,
observingDescendants,
initialized,
exists,
dirty,
formattedValue
} = controller.getState();
if (initialized && exists && !isComputedSchema(schema)) {
const newValue = isChangeEvent(eventOrValue) ? getEventValue(eventOrValue.target, eventOrValue) : eventOrValue;
const parsedValue = latestOptions.parse ? await latestOptions.parse(
newValue,
controller.getState(),
inputRef.current
) : newValue;
if (!equals(formattedValue, parsedValue)) {
activeSetPromiseRef.current?.cancel("New value is being set.");
const newSetPromise = activeSetPromiseRef.current = controller.set(parsedValue).catch(ignorePromiseCancellationException).finally(() => {
if (activeSetPromiseRef.current === newSetPromise) {
activeSetPromiseRef.current = null;
}
});
if (!dirty) {
void controller.setDirty(
observingDescendants ? Path.CURRENT_DEEP : Path.CURRENT
);
}
}
}
},
[controller, latestOptions]
);
const handleBlur = React.useCallback(() => {
const {
observingDescendants,
initialized,
exists,
touched,
formattedValue
} = controller.getState();
if (initialized && exists) {
if (!touched) {
void controller.setTouched(
observingDescendants ? Path.CURRENT_DEEP : Path.CURRENT
);
}
if (activeSetPromiseRef.current == null) {
latestOptions.setFormattedValue(
formattedValue,
controller.getState(),
inputRef.current
);
} else {
activeSetPromiseRef.current = null;
}
}
}, [controller, latestOptions]);
const listeners = useLatestValues({
onChange: handleChange,
onBlur: handleBlur
});
const inputName = controller.useState((state) => state.path.toString());
const isComputed = controller.useState(
(state) => isComputedSchema(state.schema)
);
const inputProps = React.useMemo(
() => ({
name: inputName,
readOnly: isComputed,
ref: inputRef,
onChange: (eventOrValue) => listeners.onChange(eventOrValue),
onBlur: () => listeners.onBlur()
}),
[inputName, isComputed, listeners]
);
return React.useMemo(
() => ({ ...controller, inputProps }),
[controller, inputProps]
);
}
function defaultSetFormattedValue(formattedValue, _state, input) {
if (input == null) {
return;
}
if (input instanceof HTMLInputElement && input.type === "checkbox") {
const oldInputValue = input.checked;
const newInputValue = !!formattedValue;
if (oldInputValue !== newInputValue) {
input.checked = newInputValue;
}
} else if (input instanceof HTMLInputElement && input.type === "file") {
if (formattedValue instanceof FileList) {
input.files = formattedValue;
} else {
input.value = "";
}
} else if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement || input instanceof HTMLTextAreaElement) {
const oldInputValue = input.value;
const newInputValue = String(formattedValue ?? "");
if (oldInputValue !== newInputValue) {
input.value = newInputValue;
}
} else {
throw new Error(
"Unknown input element, please provide a custom `setFormattedValue` function."
);
}
}
function isChangeEvent(value) {
return value !== null && typeof value === "object" && "target" in value && value.target !== null && typeof value.target === "object";
}
function getEventValue(eventTarget, defaultValue) {
return eventTarget instanceof HTMLInputElement || eventTarget instanceof HTMLTextAreaElement || eventTarget instanceof HTMLSelectElement ? eventTarget.type === "checkbox" ? eventTarget.checked : eventTarget.type === "file" ? eventTarget.files : eventTarget.value : defaultValue;
}
function Input({ path, children, ...props }) {
const formContext = React.useContext(FormContext);
const inputController = useInput(path, props);
const formManager = inputController.useFormManager();
const currentPath = inputController.usePath();
const newFormContext = React.useMemo(
() => ({
formManager,
currentPath,
controller: formContext?.formManager === formManager ? formContext.controller : void 0
}),
[
currentPath,
formContext?.controller,
formContext?.formManager,
formManager
]
);
const state = inputController.useState((state2) => state2, {
equalityFn: shallow
});
return /* @__PURE__ */ jsx(FormContext.Provider, { value: newFormContext, children: children(inputController.inputProps, state) });
}
function useCurrentPath() {
return useFormContext().currentPath;
}
function formatKFormFileAsFileList(value, schema) {
const dataTransfer = new DataTransfer();
if (value != null) {
dataTransfer.items.add(kFormFileToJsFile(value));
}
return dataTransfer.files;
}
function parseFileListAsKFormFile(formattedValue, schema) {
if (formattedValue.length === 0) {
return schema.typeInfo.nullable ? null : emptyPlaceholderKFormFile();
}
return jsFileToKFormFile(formattedValue[0]);
}
function useFileInput(path = Path.CURRENT, {
formatFromFileList,
parseToFileList,
...options
} = {}) {
const latestOptions = useLatestValues({ parseToFileList });
const format = React.useCallback(
(value, state, input) => {
const fileList = formatKFormFileAsFileList(value, state.schema);
return formatFromFileList ? formatFromFileList(fileList, state, input) : fileList;
},
[formatFromFileList]
);
const parse = React.useCallback(
(formattedValue, state, input) => {
return parseFileListAsKFormFile(
latestOptions.parseToFileList ? latestOptions.parseToFileList(formattedValue, state, input) : formattedValue,
state.schema
);
},
[latestOptions]
);
const inputController = useInput(path, { format, parse, ...options });
const { typeInfo } = inputController.useSchema();
if (typeInfo.name !== "File") {
throw new InvalidPathError(
inputController.getState().path,
`Unsupported schema: Expected schema representing (possibly nullable) file values, but got schema representing values of type '${typeInfo.toString()}'.`
);
}
return inputController;
}
function useFormattedValue(path = Path.CURRENT, options = {}) {
const formatterController = useFormatter(path, options);
return formatterController.useState(
(state) => [state.formattedValue, formatterController],
{ equalityFn: shallow }
);
}
function useFormController() {
const formController = useFormContext().controller;
if (formController == null) {
throw new NoFormControllerError();
}
return formController;
}
function binarySearch(array, element, compareFn) {
let low = 0;
let high = array.length - 1;
while (low <= high) {
const mid = low + high >>> 1;
const cmp = compareFn(array[mid], element);
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid - 1;
} else {
return mid;
}
}
return -(low + 1);
}
function useIssuesTracker(path = Path.CURRENT_DEEP, {
formManager: formManagerOption,
issuesOrderCompareFn,
enabled = true
} = {}) {
const formManager = useFormManager(formManagerOption);
const absolutePath = useResolvedPath(
path,
formManagerOption != null ? AbsolutePath.ROOT : void 0
);
const formSchema = React.useMemo(() => formManager.schema(), [formManager]);
const compareInfo = React.useCallback(
(info1, info2) => issuesOrderCompareFn?.(info1.path, info2.path) || compareSchemaPaths(formSchema, info1.path, info2.path),
[formSchema, issuesOrderCompareFn]
);
const [trackedState, setTrackedState] = React.useState({
initialized: false
});
React.useEffect(() => {
if (!enabled) {
return;
}
let cleanedUp = false;
let unsubscribe = void 0;
const stateByPath = /* @__PURE__ */ new Map();
let trackedInfo = [];
let errors = 0;
let warnings = 0;
const infoPromise = formManager.info(absolutePath, async (infoIterable) => {
if (cleanedUp) {
return;
}
for await (const info of infoIterable) {
const newState = { touched: info.touched, issues: info.issues };
stateByPath.set(info.path.toString(), newState);
const newInfo = validationStateToValidationInfo(info.path, newState);
if (newInfo !== void 0) {
trackedInfo.push(newInfo);
const counts = countIssueSeverities(info.issues);
errors += counts.errors;
warnings += counts.warnings;
}
}
setTrackedState({
initialized: true,
info: trackedInfo.sort(compareInfo),
errors,
warnings
});
unsubscribe = await formManager.subscribe(absolutePath, (event) => {
if (cleanedUp) {
return;
}
const pathStr = event.path.toString();
const oldState = stateByPath.get(pathStr);
let newState = oldState;
if (event instanceof ValueEvent.Init) {
newState = { touched: false, issues: [] };
} else if (event instanceof ValueEvent.Destroy) {
newState = void 0;
} else if (event instanceof StateEvent.ValidationChange) {
newState = {
touched: oldState?.touched ?? false,
issues: event.issues
};
} else if (event instanceof StateEvent.TouchedChange) {
newState = {
touched: event.status,
issues: oldState?.issues ?? []
};
}
if (oldState === newState) {
return;
}
if (!newState) {
stateByPath.delete(pathStr);
} else {
stateByPath.set(pathStr, newState);
}
const oldCountedIssues = oldState?.touched ? oldState.issues : [];
const newCountedIssues = newState?.touched ? newState.issues : [];
if (!equalIssues(oldCountedIssues, newCountedIssues)) {
trackedInfo = updateTrackedInfo(
trackedInfo,
event.path,
newState,
compareInfo
);
const oldCounts = countIssueSeverities(oldCountedIssues);
const newCounts = countIssueSeverities(newCountedIssues);
errors += newCounts.errors - oldCounts.errors;
warnings += newCounts.warnings - oldCounts.warnings;
setTrackedState({
initialized: true,
info: trackedInfo,
errors,
warnings
});
}
});
}).catch(ignorePromiseCancellationException);
return () => {
cleanedUp = true;
infoPromise.cancel(
`Clean up 'useEffect' access to info of '${absolutePath.toString()}'.`
);
void unsubscribe?.();
setTrackedState({ initialized: false });
};
}, [absolutePath, compareInfo, enabled, formManager]);
return trackedState;
}
function validationStateToValidationInfo(path, state) {
return state?.touched && state.issues.length > 0 ? {
path,
issues: state.issues,
localDisplayStatus: state.issues.some(
(issue) => issue.severity === "error"
) ? "error" : "warning"
} : void 0;
}
function updateTrackedInfo(trackedInfo, path, newState, compareInfo) {
const infoOfPath = { path };
const index = binarySearch(trackedInfo, infoOfPath, compareInfo);
const newInfo = validationStateToValidationInfo(path, newState);
if (index < 0 && newInfo === void 0) {
return trackedInfo;
}
const result = trackedInfo.slice();
if (index >= 0) {
if (newInfo === void 0) {
result.splice(index, 1);
} else {
result[index] = newInfo;
}
} else {
result.splice(-index - 1, 0, newInfo);
}
return result;
}
function countIssueSeverities(issues) {
let errors = 0;
let warnings = 0;
for (const issue of issues) {
if (issue.severity === "error") {
++errors;
} else {
++warnings;
}
}
return { errors, warnings };
}
function equalIssues(issues1, issues2) {
return issues1.length === issues2.length && issues1.every((issue, i) => issue.equals(issues2[i]));
}
function formatListableAsArray(value, schema) {
return value == null ? null : listableToArray(value);
}
function parseArrayAsListable(formattedValue, schema) {
const supportsNull = schema.typeInfo.nullable;
const schemaType = schema.typeInfo.name;
if (formattedValue == null) {
if (supportsNull) {
return null;
} else {
formattedValue = [];
}
}
switch (schemaType) {
case "Array":
return formattedValue;
case "List":
return arrayToList(formattedValue);
default:
return arrayToTable(formattedValue);
}
}
const ALLOWED_COLLECTION_TYPES = ["Array", "List", "Table"];
function useListableInput(path = Path.CURRENT, {
formatFromArray,
parseToArray,
...options
} = {}) {
const latestOptions = useLatestValues({ parseToArray });
const format = React.useCallback(
(value, state, input) => {
const array = formatListableAsArray(value, state.schema);
return formatFromArray ? formatFromArray(array, state, input) : array;
},
[formatFromArray]
);
const parse = React.useCallback(
(formattedValue, state, input) => {
return parseArrayAsListable(
latestOptions.parseToArray ? latestOptions.parseToArray(formattedValue, state, input) : formattedValue,
state.schema
);
},
[latestOptions]
);
const inputController = useInput(path, { format, parse, ...options });
const { typeInfo } = inputController.useSchema();
if (!ALLOWED_COLLECTION_TYPES.includes(typeInfo.name)) {
throw new InvalidPathError(
inputController.getState().path,
`Unsupported schema: Expected schema representing (possibly nullable) listable values, but got schema representing values of type '${typeInfo.toString()}'.`
);
}
return inputController;
}
const CHAR_MIN_NUMBER = 0;
const CHAR_MAX_NUMBER = 65535;
const BYTE_MIN_VALUE = -128;
const BYTE_MAX_VALUE = 127;
const CHAR_MIN_VALUE = codeToChar(CHAR_MIN_NUMBER);
const CHAR_MAX_VALUE = codeToChar(CHAR_MAX_NUMBER);
const SHORT_MIN_VALUE = -32768;
const SHORT_MAX_VALUE = 32767;
const INT_MIN_VALUE = -2147483648;
const INT_MAX_VALUE = 2147483647;
const LONG_MIN_VALUE = stringToLong("-9223372036854775808");
const LONG_MAX_VALUE = stringToLong("9223372036854775807");
const CHAR_ZERO = CHAR_MIN_VALUE;
const LONG_ZERO = stringToLong("0");
const BIG_INTEGER_ZERO = stringToBigInteger("0");
const BIG_DECIMAL_ZERO = stringToBigDecimal("0");
function formatNumericAsString(value, schema) {
if (value == null) {
return "";
}
const schemaType = schema.typeInfo.name;
switch (schemaType) {
case "Char":
return charToCode(value).toString();
case "Float":
case "Double":
return Number.isNaN(value) ? "" : fromExponential(value);
case "String":
return value === "" || Number.isNaN(Number(value)) ? "" : fromExponential(value);
default:
return value.toString();
}
}
function parseStringAsNumeric(formattedValue, schema) {
const supportsNull = schema.typeInfo.nullable;
const schemaType = schema.typeInfo.name;
if (formattedValue === "" || formattedValue === "-" || formattedValue === "." || formattedValue === "-.") {
return supportsNull ? null : schemaType === "Char" ? CHAR_ZERO : schemaType === "Long" ? LONG_ZERO : schemaType === "String" ? "" : 0;
}
switch (schemaType) {
case "Byte":
case "Short":
case "Int": {
const { min, max } = numericTypeMinMax(schema);
const value = parseInt(formattedValue);
return Number.isNaN(value) ? supportsNull ? null : 0 : Math.min(Math.max(value, min), max);
}
case "Char": {
const value = parseInt(formattedValue);
return Number.isNaN(value) ? supportsNull ? null : CHAR_ZERO : codeToChar(
Math.min(Math.max(value, CHAR_MIN_NUMBER), CHAR_MAX_NUMBER)
);
}
case "Long": {
const value = parseIntString(formattedValue);
if (value === null) {
return supportsNull ? null : LONG_ZERO;
}
const isNegative = value.startsWith("-");
try {
return stringToLong(value);
} catch {
return isNegative ? LONG_MIN_VALUE : LONG_MAX_VALUE;
}
}
case "BigInteger": {
const value = parseIntString(formattedValue);
if (value === null) {
return supportsNull ? null : BIG_INTEGER_ZERO;
}
return stringToBigInteger(value);
}
case "BigDecimal": {
const preferredScale = schema.typeInfo.restrictions.scale ?? null;
let bigDecimal;
try {
bigDecimal = stringToBigDecimal(formattedValue);
} catch {
bigDecimal = supportsNull ? null : BIG_DECIMAL_ZERO;
}
return bigDecimal === null || preferredScale === null || bigDecimal.scale >= preferredScale ? bigDecimal : bigDecimal.setScale(preferredScale);
}
case "Float":
case "Double":
return parseFloat(formattedValue);
default:
return formattedValue;
}
}
function numericTypeMinMax(schema) {
switch (schema.typeInfo.name) {
case "Byte":
return {
min: BYTE_MIN_VALUE,
max: BYTE_MAX_VALUE
};
case "Char":
return {
min: CHAR_MIN_VALUE,
max: CHAR_MAX_VALUE
};
case "Short":
return {
min: SHORT_MIN_VALUE,
max: SHORT_MAX_VALUE
};
case "Int":
return {
min: INT_MIN_VALUE,
max: INT_MAX_VALUE
};
case "Long":
return {
min: LONG_MIN_VALUE,
max: LONG_MAX_VALUE
};
default:
return { min: void 0, max: void 0 };
}
}
function parseIntString(str) {
const len = str.length;
const isNegative = str.startsWith("-");
let start = isNegative ? 1 : 0;
while (start < len - 1 && str[start] === "0" && str[start + 1] >= "0" && str[start + 1] <= "9") {
++start;
}
let end = len;
for (let i = start; i < len; ++i) {
if (str[i] < "0" || str[i] > "9") {
end = i;
break;
}
}
if (start === end) {
return null;
}
const normalized = str.slice(start, end);
return isNegative && normalized !== "0" ? `-${normalized}` : normalized;
}
const ALLOWED_NUMERIC_TYPES = [
"Byte",
"Char",
"Short",
"Int",
"Long",
"Float",
"Double",
"BigInteger",
"BigDecimal",
// Not exactly a numeric type, but it is sometimes useful to treat strings
// with numeric patterns as "numeric"
"String"
];
function useNumericInput(path = Path.CURRENT, {
formatFromString,
parseToString,
...options
} = {}) {
const latestOptions = useLatestValues({ parseToString });
const format = React.useCallback(
(value, state, input) => {
const numberString = formatNumericAsString(value, state.schema);
return formatFromString ? formatFromString(numberString, state, input) : numberString;
},
[formatFromString]
);
const parse = React.useCallback(
(formattedValue, state, input) => {
return parseStringAsNumeric(
latestOptions.parseToString ? latestOptions.parseToString(formattedValue, state, inpu