niceform-hook
Version:
Dynamic workhorse for form in react
670 lines (669 loc) • 28.6 kB
JavaScript
import {jsx}from'react/jsx-runtime';import {createContext as createContext$1,useRef,useEffect,useContext,useState,useMemo,useCallback,memo}from'react';import {useController as useController$1,useForm as useForm$1}from'react-hook-form';function createProvider(ProviderOriginal) {
return ({ value, children }) => {
const valueRef = useRef(value);
const listenersRef = useRef(new Set());
const contextValue = useRef({
value: valueRef,
registerListener: (listener) => {
listenersRef.current.add(listener);
return () => listenersRef.current.delete(listener);
}
});
useEffect(() => {
valueRef.current = value;
listenersRef.current.forEach((listener) => {
listener(value);
});
}, [value]);
return (jsx(ProviderOriginal, { value: contextValue.current, children: children }));
};
}
function createContext(defaultValue) {
const context = createContext$1(defaultValue);
delete context.Consumer;
context.Provider = createProvider(context.Provider);
return context;
}/*
* This code is based on an implementation provided by Luke Edwards
* [https://github.com/lukeed/dequal]
* Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com)
* release v2.0.3
*/
const has = Object.prototype.hasOwnProperty;
function dequalLite(foo, bar) {
let ctor, len;
if (foo === bar)
return true;
if (foo && bar && (ctor = foo.constructor) === bar.constructor) {
if (ctor === Date)
return foo.getTime() === bar.getTime();
if (ctor === RegExp)
return foo.toString() === bar.toString();
if (ctor === Array) {
if ((len = foo.length) === bar.length) {
while (len-- && dequalLite(foo[len], bar[len]))
;
}
return len === -1;
}
if (!ctor || typeof foo === 'object') {
len = 0;
for (ctor in foo) {
if (has.call(foo, ctor) && ++len && !has.call(bar, ctor))
return false;
if (!(ctor in bar) || !dequalLite(foo[ctor], bar[ctor]))
return false;
}
return Object.keys(bar).length === len;
}
}
return foo !== foo && bar !== bar;
}function useContextSelector(context, selector, deepComparison = false) {
const { value, registerListener } = useContext(context);
const selectorRef = useRef(selector);
const [selectedValue, setSelectedValue] = useState(() => selector(value.current));
const _selectedValue = useRef(selectedValue);
_selectedValue.current = selectedValue;
useEffect(() => {
selectorRef.current = selector;
});
useEffect(() => {
const updateValueIfNeeded = (newValue) => {
const newSelectedValue = selectorRef.current(newValue);
const compare = deepComparison ? dequalLite : Object.is;
if (!compare(_selectedValue.current, newSelectedValue)) {
setSelectedValue(() => newSelectedValue);
}
};
const unregisterListener = registerListener(updateValueIfNeeded);
return unregisterListener;
}, [registerListener, value, deepComparison]);
return selectedValue;
}const context = createContext(null);
function NiceformHookProvider({ children, ...props }) {
return (jsx(context.Provider, { value: props, children: children }));
}
function useNiceformHookContext(callback, deepComparison = false) {
return useContextSelector(context, callback, deepComparison);
}function useNiceformContext() {
return useNiceformHookContext(state => {
return state.form;
});
}function useDataRefByContext(callback) {
const fieldMethodsRef = useRef();
return useNiceformHookContext(state => {
fieldMethodsRef.current = callback(state);
return fieldMethodsRef;
});
}function useComputeInputedValueHandler(controller) {
const name = controller.field.name;
const inputRef = useDataRefByContext(state => { var _a; return (_a = state.form.getField(name)) === null || _a === void 0 ? void 0 : _a.input; });
const setValue = useNiceformHookContext(state => state.form.methods.setValue);
const fieldsInputedCalled = useNiceformHookContext(state => state.form.control.fieldsInputedCalled);
const isInputedValueRef = fieldsInputedCalled.has(name);
const computeInputedValue = (callback) => {
const input = inputRef.current;
let value = controller.field.value;
if (!input || isInputedValueRef || controller.fieldState.isDirty)
return;
value = input(value);
if (value === undefined)
return;
fieldsInputedCalled.add(name);
controller.field.value = value;
callback(value);
};
computeInputedValue(value => setTimeout(setValue, 100, name, value));
const value = controller.field.value;
useEffect(() => {
computeInputedValue(value => setValue(name, value));
}, [value]);
}function debounce(fn, ms) {
let timer = 0;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(fn.bind(this, ...args), ms || 0);
};
}
function dynamicDebounce() {
let timer = 0;
return function (fn, ms, ...args) {
clearTimeout(timer);
timer = setTimeout(fn.bind(this, args), ms || 0);
};
}function getDependentFieldsBy(name, fieldsToSearch) {
const allField = [...fieldsToSearch.values()];
const fieldSelected = fieldsToSearch.get(name);
if (!fieldSelected)
return [];
const fieldsToClean = allField.reduce((collection, field) => {
if (fieldSelected.name &&
field.dependsOnToClear &&
[field.dependsOnToClear].flat().includes(fieldSelected.name) &&
field.name !== fieldSelected.name) {
collection.push(field);
}
return collection;
}, []);
return fieldsToClean;
}function flattenJson(obj) {
const flattenedObj = {};
function flatten(innerObj, path) {
var _a;
for (const key in innerObj) {
const newPath = path ? `${path}.${key}` : key;
if (((_a = innerObj[key]) === null || _a === void 0 ? void 0 : _a.constructor) === ({}).constructor || Array.isArray(innerObj[key])) {
flatten(innerObj[key], newPath);
}
else {
flattenedObj[newPath] = innerObj[key];
}
}
}
flatten(obj, "");
return flattenedObj;
}function unflattenJson(obj) {
const unflattenedObj = {};
for (const key in obj) {
const keys = key.split(".");
let innerObj = unflattenedObj;
for (let i = 0; i < keys.length - 1; i++) {
const currentKey = keys[i];
if (!innerObj[currentKey]) {
innerObj[currentKey] = !isNaN(Number(keys[i + 1])) ? [] : {};
}
innerObj = innerObj[currentKey];
}
innerObj[keys.at(-1)] = obj[key];
}
return unflattenedObj;
}function getOutputtedValues({ fields, values }) {
const valuesFlatterned = flattenJson(values);
const result = [...fields.values()].reduce((obj, field) => {
if (!field.name || !field.output || field.active === false)
return obj;
const value = valuesFlatterned[field.name];
obj[field.name] = field.output(value);
return obj;
}, {});
return unflattenJson({ ...valuesFlatterned, ...result });
}const isFieldActive = (arg) => {
if (typeof arg === 'object') {
return arg.active !== false;
}
return arg !== false;
};function normalizeFieldPayload(field, deps) {
if (typeof field !== 'function')
return field;
return field(deps);
}const isDateObject = (value) => value instanceof Date;
const isNullOrUndefined = (value) => value == null;
const isObjectType = (value) => typeof value === 'object';
const isObject = (value) => !isNullOrUndefined(value) &&
!Array.isArray(value) &&
isObjectType(value) &&
!isDateObject(value);const isCheckBoxInput = (element) => element.type === 'checkbox';
const getEventValue = (event) => isObject(event) && event.target
? isCheckBoxInput(event.target)
? event.target.checked
: event.target.value
: event;function useDebounceChangeFieldHandler(controller, time = 400, enabled = true) {
const [value, setValue] = useState(controller.field.value);
const debounceSubmitDefinitions = useNiceformHookContext(state => state.form.control.debounceSubmitDefinitions);
const onChangeDebounce = useMemo(() => debounce(controller.field.onChange, time), [time, controller.field.onChange]);
const newOnChange = useCallback((evt) => {
const value = getEventValue(evt);
setValue(value);
onChangeDebounce(value);
debounceSubmitDefinitions.set(time);
}, [setValue, onChangeDebounce, time]);
if (!enabled)
return;
const controlledValue = controller.field.value;
useEffect(() => {
if (!Object.is(value, controlledValue)) {
setValue(controlledValue);
}
}, [controlledValue]);
controller.field.onChange = newOnChange;
controller.field.value = value;
}function useForm() {
return useNiceformHookContext(state => state.form.methods);
}function useController(props) {
const { control } = useForm();
return useController$1({ control, ...props });
}function useInactiveFieldHandler(controller) {
const isFieldActive$1 = useNiceformHookContext(state => {
const field = state.form.getField(controller.field.name);
return isFieldActive(field === null || field === void 0 ? void 0 : field.active);
});
const [value, setValue] = useState();
if (!isFieldActive$1) {
controller.field = {
...controller.field,
onBlur: () => null,
onChange: (evt) => setValue(evt.target.value),
value,
ref: () => null
};
}
}const obj = {};
function useField(name, config) {
var _a;
if (!config)
config = obj;
const methods = useNiceformHookContext(state => state.form.methods);
const errorsControl = useNiceformHookContext(state => state.form.control.errorsControl);
const fieldsRegistered = useNiceformHookContext(state => state.form.control.fieldsRegistered);
const validationByErrorsControl = (errorsControl || []).reduce((obj, error, i) => {
const field = fieldsRegistered.get(name);
if (!field)
return obj;
obj['errors-control-' + i] = (value, formValues) => error({
value,
formValues,
fieldsRegistered,
methods,
field
});
return obj;
}, {});
const volatileFieldRef = useDataRefByContext(state => {
var _a, _b, _c, _d;
const field = state.form.getField(name);
if (!field)
return {};
return {
input: (_a = field.input) !== null && _a !== void 0 ? _a : config.input,
onBlur: (_b = field.onBlur) !== null && _b !== void 0 ? _b : config.onBlur,
onChange: (_c = field.onChange) !== null && _c !== void 0 ? _c : config.onChange,
validate: (_d = field.validate) !== null && _d !== void 0 ? _d : config.validate
};
});
const { rules, others } = useNiceformHookContext(state => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
const field = state.form.getField(name);
if (!field)
return { rules: {}, others: {} };
const debounceTime = ((_e = (_d = (_b = (_a = field.debounceTime) !== null && _a !== void 0 ? _a : config.debounceTime) !== null && _b !== void 0 ? _b : (_c = state.form.control.parameters) === null || _c === void 0 ? void 0 : _c.debounceTime) !== null && _d !== void 0 ? _d : state.form.control.config.debounceTime) !== null && _e !== void 0 ? _e : 400);
const enableDebounce = ((_k = (_j = (_g = (_f = field.enableDebounce) !== null && _f !== void 0 ? _f : config.enableDebounce) !== null && _g !== void 0 ? _g : (_h = state.form.control.parameters) === null || _h === void 0 ? void 0 : _h.enableDebounce) !== null && _j !== void 0 ? _j : state.form.control.config.enableDebounce) !== null && _k !== void 0 ? _k : false) && debounceTime > 0;
return {
rules: {
min: (_l = field.min) !== null && _l !== void 0 ? _l : config.min,
max: (_m = field.max) !== null && _m !== void 0 ? _m : config.max,
deps: field.deps,
maxLength: (_o = field.maxLength) !== null && _o !== void 0 ? _o : config.maxLength,
minLength: (_p = field.minLength) !== null && _p !== void 0 ? _p : config.minLength,
pattern: (_q = field.pattern) !== null && _q !== void 0 ? _q : config.pattern,
required: (_r = field.required) !== null && _r !== void 0 ? _r : config.required,
value: (_s = field.value) !== null && _s !== void 0 ? _s : config.value,
},
others: {
shouldUnregister: (_t = field.shouldUnregister) !== null && _t !== void 0 ? _t : config.shouldUnregister,
isActive: ((_u = field.active) !== null && _u !== void 0 ? _u : config.active) !== false,
disabled: (_v = field.disabled) !== null && _v !== void 0 ? _v : config.disabled,
debounceTime,
enableDebounce
}
};
}, true);
const controller = useController({
name,
shouldUnregister: others.shouldUnregister,
rules: others.isActive ? {
onBlur: (...args) => { var _a, _b; return (_b = (_a = volatileFieldRef.current) === null || _a === void 0 ? void 0 : _a.onBlur) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
onChange: (...args) => { var _a, _b; return (_b = (_a = volatileFieldRef.current) === null || _a === void 0 ? void 0 : _a.onChange) === null || _b === void 0 ? void 0 : _b.call(_a, ...args); },
shouldUnregister: others.shouldUnregister,
deps: rules.deps,
max: rules.max,
min: rules.min,
maxLength: rules.maxLength,
minLength: rules.minLength,
pattern: rules.pattern,
required: rules.required,
validate: {
...validationByErrorsControl,
...(_a = volatileFieldRef.current) === null || _a === void 0 ? void 0 : _a.validate
},
value: rules.value,
} : {},
disabled: others.disabled
});
useComputeInputedValueHandler(controller);
useDebounceChangeFieldHandler(controller, others.debounceTime, others.enableDebounce);
useInactiveFieldHandler(controller);
return controller;
}const RenderReactElement = memo(function RenderReactElement(props) {
const hook = useField(props.name);
return props.render(hook);
});
const renderReactElement = (props) => jsx(RenderReactElement, { ...props });function useDataRef(data) {
const ref = useRef(data);
ref.current = data;
return ref;
}function useChangeField(props) {
const oldsValuesByName = useRef(null);
const onChangeFieldRef = useDataRef(props.onChangeField);
const watch = props.methods.watch;
const methods = props.methods;
if (!oldsValuesByName.current) {
oldsValuesByName.current = new Map();
}
useEffect(() => {
const subscription = watch((values, { name, type }) => {
if (name === undefined || !onChangeFieldRef.current)
return;
const field = props.fields.get(name);
if (!field)
return;
const value = methods.getValues(name);
if (Object.is(oldsValuesByName.current.get(name), value))
return;
oldsValuesByName.current.set(name, value);
onChangeFieldRef.current(field, value);
});
return () => subscription.unsubscribe();
}, [watch, onChangeFieldRef, methods]);
}function useDependentFieldsToClear(input) {
const { methods, optionsWhenCleaning } = input;
const dataRef = useDataRef({
methods,
optionsWhenCleaning
});
const watch = methods.watch;
useEffect(() => {
const subscription = watch((values, { name, type }) => {
if (name === undefined)
return;
const methods = dataRef.current.methods;
const optionsWhenCleaning = dataRef.current.optionsWhenCleaning;
const fields = input.fields;
const currentValue = methods.getValues(name);
const shouldCleanChain = optionsWhenCleaning === null || optionsWhenCleaning === void 0 ? void 0 : optionsWhenCleaning.shouldCleanChain;
if (currentValue === undefined && !shouldCleanChain)
return;
const fieldsDependents = getDependentFieldsBy(name, fields || []);
fieldsDependents.forEach(field => {
const currentFieldValue = methods.getValues(field.name);
if (!field.name || currentFieldValue === undefined)
return;
methods.setValue(field.name, null, optionsWhenCleaning);
});
});
return () => subscription.unsubscribe();
}, [watch, dataRef]);
}function useInitialValues(props) {
const flatternInitialValues = flattenJson(props.initialValues);
for (const fieldName in flatternInitialValues) {
const value = flatternInitialValues[fieldName];
if (value === undefined)
continue;
if (props.methods.getValues(fieldName) !== undefined)
continue;
props.methods.register(fieldName);
props.methods.setValue(fieldName, value);
}
}/*
* This code is based on an implementation provided by Federico Brigante
* [https://github.com/fregante/many-keys-map]
* Copyright (c) Federico Brigante <me@fregante.com> (https://fregante.com)
* release v2.0.1
*/
const nullKey = Symbol('null'); // `objectHashes` key for null
let keyCounter = 0;
class ManyKeysMap extends Map {
constructor() {
super();
this._objectHashes = new WeakMap();
this._symbolHashes = new Map(); // https://github.com/tc39/ecma262/issues/1194
this._publicKeys = new Map();
const [pairs] = arguments; // Map compat
if (pairs === null || pairs === undefined) {
return;
}
if (typeof pairs[Symbol.iterator] !== 'function') {
throw new TypeError(typeof pairs + ' is not iterable (cannot read property Symbol(Symbol.iterator))');
}
for (const [keys, value] of pairs) {
this.set(keys, value);
}
}
_getPublicKeys(keys, create = false) {
if (!Array.isArray(keys)) {
throw new TypeError('The keys parameter must be an array');
}
const privateKey = this._getPrivateKey(keys, create);
let publicKey;
if (privateKey && this._publicKeys.has(privateKey)) {
publicKey = this._publicKeys.get(privateKey);
}
else if (create) {
publicKey = [...keys]; // Regenerate keys array to avoid external interaction
this._publicKeys.set(privateKey, publicKey);
}
return { privateKey, publicKey };
}
_getPrivateKey(keys, create = false) {
const privateKeys = [];
for (let key of keys) {
if (key === null) {
key = nullKey;
}
const hashes = typeof key === 'object' || typeof key === 'function' ? '_objectHashes' : (typeof key === 'symbol' ? '_symbolHashes' : false);
if (!hashes) {
privateKeys.push(key);
}
else if (this[hashes].has(key)) {
privateKeys.push(this[hashes].get(key));
}
else if (create) {
const privateKey = `@@mkm-ref-${keyCounter++}@@`;
this[hashes].set(key, privateKey);
privateKeys.push(privateKey);
}
else {
return false;
}
}
return JSON.stringify(privateKeys);
}
set(keys, value) {
const { publicKey } = this._getPublicKeys(keys, true);
return super.set(publicKey, value);
}
get(keys) {
const { publicKey } = this._getPublicKeys(keys);
return super.get(publicKey);
}
has(keys) {
const { publicKey } = this._getPublicKeys(keys);
return super.has(publicKey);
}
delete(keys) {
const { publicKey, privateKey } = this._getPublicKeys(keys);
return Boolean(publicKey && super.delete(publicKey) && this._publicKeys.delete(privateKey));
}
clear() {
super.clear();
this._symbolHashes.clear();
this._publicKeys.clear();
}
get [Symbol.toStringTag]() {
return 'ManyKeysMap';
}
get size() {
// @ts-ignore
return super.size;
}
}function useMemoize() {
const cache = useRef(new ManyKeysMap());
const memoize = useCallback(function memoize(callback, dependencies) {
let currentValue = cache.current.get(dependencies);
if (!currentValue) {
if (typeof callback === 'function') {
cache.current.set(dependencies, callback());
}
else {
cache.current.set(dependencies, callback);
}
currentValue = cache.current.get(dependencies);
}
return currentValue;
}, []);
return memoize;
}function useMemoizeCallback() {
const cache = useRef(new ManyKeysMap());
const memoizeCallback = useCallback(function memoizeCallback(callback, dependencies) {
let currentValue = cache.current.get(dependencies);
if (!currentValue) {
cache.current.set(dependencies, callback);
currentValue = cache.current.get(dependencies);
}
return currentValue;
}, []);
return memoizeCallback;
}function useOnErrorDuringSubmit({ getField, methods, onErrorDuringSubmit }) {
useEffect(() => {
const errors = methods.formState.errors;
if (Object.keys(errors).length && methods.formState.submitCount)
onErrorDuringSubmit === null || onErrorDuringSubmit === void 0 ? void 0 : onErrorDuringSubmit(methods.formState.errors, {
methods,
getField
});
}, [methods.formState.submitCount]);
}function create(config) {
const components = new Map();
for (const key in config.components) {
components.set(key, config.components[key]);
}
return function useForm(parameters) {
const repository = useRef({
fieldsRegistered: new Map(),
componentsDefinitions: new Map(components),
errorsControl: config.errorsControl,
debounceSubmitDefinitions: {
debounceRegistry: { registeredAt: 0, time: 0 },
set(time) {
this.debounceRegistry.registeredAt = Date.now();
this.debounceRegistry.time = time;
},
getRemainingTime() {
const timeDiff = Date.now() - this.debounceRegistry.registeredAt;
return Math.max(0, this.debounceRegistry.time - timeDiff + 50);
},
isActiveDebounce() {
const timeDiff = Date.now() - this.debounceRegistry.registeredAt;
return timeDiff < this.debounceRegistry.time;
}
},
fieldsInputedCalled: new Set()
});
const submitDynamicDebounce = useMemo(() => dynamicDebounce(), []);
const memoize = useMemoize();
const memoizeCallback = useMemoizeCallback();
const methods = useForm$1(parameters);
const getField = useCallback((name) => {
return repository.current.fieldsRegistered.get(name);
}, []);
useOnErrorDuringSubmit({
getField,
methods,
onErrorDuringSubmit: config.onErrorDuringSubmit
});
useInitialValues({
methods,
initialValues: (parameters === null || parameters === void 0 ? void 0 : parameters.initialValues) || {}
});
useDependentFieldsToClear({
methods,
get fields() {
return repository.current.fieldsRegistered;
},
});
useChangeField({
methods,
get fields() {
return repository.current.fieldsRegistered;
},
onChangeField: parameters === null || parameters === void 0 ? void 0 : parameters.onChangeField
});
const renderField = useCallback(field => {
const { render, ..._field } = normalizeFieldPayload(field, {
getField,
methods
});
const type = _field.type;
const name = _field.name;
if (!name)
return null;
repository.current.fieldsRegistered.set(name, _field);
if (type === 'node' && render) {
return renderReactElement({ name, render });
}
const component = repository.current.componentsDefinitions.get(type);
const fieldComponent = component === null || component === void 0 ? void 0 : component.render(_field);
return fieldComponent;
}, [
methods,
getField
]);
const renderFields = useCallback(fields => {
return fields.filter(Boolean).map(renderField);
}, [renderField]);
if (!methods.handleSubmit.prototype) {
const handleSubmit = methods.handleSubmit;
methods.handleSubmit = function (onValid, onInvalid) {
const resolver = handleSubmit(async (values, event) => {
const valuesOutputted = getOutputtedValues({
fields: repository.current.fieldsRegistered,
values: values
});
onValid(valuesOutputted, event);
}, onInvalid);
return async function (evt) {
var _a;
const enableDebounce = ((_a = parameters === null || parameters === void 0 ? void 0 : parameters.enableDebounceOnSubmit) !== null && _a !== void 0 ? _a : config.enableDebounceOnSubmit) && repository.current.debounceSubmitDefinitions.isActiveDebounce();
if (!enableDebounce)
return resolver(evt);
const timeDebounce = repository.current.debounceSubmitDefinitions.getRemainingTime();
evt === null || evt === void 0 ? void 0 : evt.preventDefault();
submitDynamicDebounce(() => {
resolver(evt);
}, timeDebounce);
};
};
}
// --------------------------------------------------------------------------------------------
const result = useRef({
methods,
renderField,
renderFields,
getField,
memoize,
memoizeCallback,
control: {
get errorsControl() {
return repository.current.errorsControl;
},
get fieldsRegistered() {
return repository.current.fieldsRegistered;
},
get fieldsInputedCalled() {
return repository.current.fieldsInputedCalled;
},
get debounceSubmitDefinitions() {
return repository.current.debounceSubmitDefinitions;
},
get parameters() {
return parameters;
},
get config() {
return config;
}
}
});
result.current.renderField = renderField;
result.current.renderFields = renderFields;
return result.current;
};
}export{NiceformHookProvider,create,useField,useNiceformContext,useNiceformHookContext};//# sourceMappingURL=index.esm.mjs.map