@conform-to/react
Version:
Conform view adapter for react
232 lines (224 loc) • 10.2 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var dom = require('@conform-to/dom');
var react = require('react');
var util = require('./util.js');
var context = require('./context.js');
/**
* A React hook that lets you sync the state of an input and dispatch native form events from it.
* This is useful when emulating native input behavior — typically by rendering a hidden base input
* and syncing it with a custom input.
*
* @example
* ```ts
* const control = useControl(options);
* ```
*/
function useControl(options) {
var {
observer
} = react.useContext(context.FormContext);
var inputRef = react.useRef(null);
var eventDispatched = react.useRef({});
var defaultSnapshot = util.getDefaultSnapshot(options === null || options === void 0 ? void 0 : options.defaultValue, options === null || options === void 0 ? void 0 : options.defaultChecked, options === null || options === void 0 ? void 0 : options.value);
var snapshotRef = react.useRef(defaultSnapshot);
var optionsRef = react.useRef(options);
react.useEffect(() => {
optionsRef.current = options;
});
// This is necessary to ensure that input is re-registered
// if the onFocus handler changes
var shouldHandleFocus = typeof (options === null || options === void 0 ? void 0 : options.onFocus) === 'function';
var snapshot = react.useSyncExternalStore(react.useCallback(callback => observer.onFieldUpdate(event => {
var input = event.target;
if (Array.isArray(inputRef.current) ? inputRef.current.some(item => item === input) : inputRef.current === input) {
callback();
}
}), [observer]), () => {
var input = inputRef.current;
var prev = snapshotRef.current;
var next = !input ? defaultSnapshot : Array.isArray(input) ? {
value: util.getRadioGroupValue(input),
options: util.getCheckboxGroupValue(input)
} : util.getInputSnapshot(input);
if (dom.unstable_deepEqual(prev, next)) {
return prev;
}
snapshotRef.current = next;
return next;
}, () => snapshotRef.current);
react.useEffect(() => {
var createEventListener = listener => {
return event => {
if (Array.isArray(inputRef.current) ? inputRef.current.some(item => item === event.target) : inputRef.current === event.target) {
var timer = eventDispatched.current[listener];
if (timer) {
clearTimeout(timer);
}
eventDispatched.current[listener] = window.setTimeout(() => {
eventDispatched.current[listener] = undefined;
});
if (listener === 'focus') {
var _optionsRef$current, _optionsRef$current$o;
(_optionsRef$current = optionsRef.current) === null || _optionsRef$current === void 0 || (_optionsRef$current$o = _optionsRef$current.onFocus) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current);
}
}
};
};
var inputHandler = createEventListener('change');
var focusHandler = createEventListener('focus');
var blurHandler = createEventListener('blur');
document.addEventListener('input', inputHandler, true);
document.addEventListener('focusin', focusHandler, true);
document.addEventListener('focusout', blurHandler, true);
return () => {
document.removeEventListener('input', inputHandler, true);
document.removeEventListener('focusin', focusHandler, true);
document.removeEventListener('focusout', blurHandler, true);
};
}, []);
return {
value: snapshot.value,
checked: snapshot.checked,
options: snapshot.options,
files: snapshot.files,
register: react.useCallback(element => {
if (!element) {
inputRef.current = null;
} else if (dom.isFieldElement(element)) {
inputRef.current = element;
if (shouldHandleFocus) {
util.focusable(element);
}
if (element.type === 'checkbox' || element.type === 'radio') {
var _optionsRef$current$v, _optionsRef$current2;
// React set the value as empty string incorrectly when the value is undefined
// This make sure the checkbox value falls back to the default value "on" properly
// @see https://github.com/facebook/react/issues/17590
element.value = (_optionsRef$current$v = (_optionsRef$current2 = optionsRef.current) === null || _optionsRef$current2 === void 0 ? void 0 : _optionsRef$current2.value) !== null && _optionsRef$current$v !== void 0 ? _optionsRef$current$v : 'on';
}
util.initializeField(element, optionsRef.current);
} else {
var _inputs$0$name, _inputs$, _inputs$0$type, _inputs$2;
var inputs = Array.from(element);
var name = (_inputs$0$name = (_inputs$ = inputs[0]) === null || _inputs$ === void 0 ? void 0 : _inputs$.name) !== null && _inputs$0$name !== void 0 ? _inputs$0$name : '';
var type = (_inputs$0$type = (_inputs$2 = inputs[0]) === null || _inputs$2 === void 0 ? void 0 : _inputs$2.type) !== null && _inputs$0$type !== void 0 ? _inputs$0$type : '';
if (!name || !(type === 'checkbox' || type === 'radio') || !inputs.every(input => input.name === name && input.type === type)) {
throw new Error('You can only register a checkbox or radio group with the same name');
}
inputRef.current = inputs;
for (var input of inputs) {
var _optionsRef$current3;
if (shouldHandleFocus) {
util.focusable(input);
}
util.initializeField(input, {
// We will not be uitlizing defaultChecked / value on checkbox / radio group
defaultValue: (_optionsRef$current3 = optionsRef.current) === null || _optionsRef$current3 === void 0 ? void 0 : _optionsRef$current3.defaultValue
});
}
}
}, [shouldHandleFocus]),
change: react.useCallback(value => {
if (!eventDispatched.current.change) {
var _inputRef$current;
var _element = Array.isArray(inputRef.current) ? (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 ? void 0 : _inputRef$current.find(input => {
var wasChecked = input.checked;
var isChecked = Array.isArray(value) ? value.some(item => item === input.value) : input.value === value;
switch (input.type) {
case 'checkbox':
// We assume that only one checkbox can be checked at a time
// So we will pick the first element with checked state changed
return wasChecked !== isChecked;
case 'radio':
// We cannot uncheck a radio button
// So we will pick the first element that should be checked
return isChecked;
default:
return false;
}
}) : inputRef.current;
if (_element) {
dom.unstable_change(_element, typeof value === 'boolean' ? value ? _element.value : null : value);
}
}
if (eventDispatched.current.change) {
clearTimeout(eventDispatched.current.change);
}
eventDispatched.current.change = undefined;
}, []),
focus: react.useCallback(() => {
if (!eventDispatched.current.focus) {
var _element2 = Array.isArray(inputRef.current) ? inputRef.current[0] : inputRef.current;
if (_element2) {
dom.unstable_focus(_element2);
}
}
if (eventDispatched.current.focus) {
clearTimeout(eventDispatched.current.focus);
}
eventDispatched.current.focus = undefined;
}, []),
blur: react.useCallback(() => {
if (!eventDispatched.current.blur) {
var _element3 = Array.isArray(inputRef.current) ? inputRef.current[0] : inputRef.current;
if (_element3) {
dom.unstable_blur(_element3);
}
}
if (eventDispatched.current.blur) {
clearTimeout(eventDispatched.current.blur);
}
eventDispatched.current.blur = undefined;
}, [])
};
}
/**
* A React hook that lets you subscribe to the current `FormData` of a form and derive a custom value from it.
* The selector runs whenever the form's structure or data changes, and the hook re-renders only when the result is deeply different.
*
* @see https://conform.guide/api/react/future/useFormData
* @example
* ```ts
* const value = useFormData(formRef, formData => formData?.get('fieldName').toString() ?? '');
* ```
*/
function useFormData(formRef, select, options) {
var {
observer
} = react.useContext(context.FormContext);
var valueRef = react.useRef();
var formDataRef = react.useRef(null);
var value = react.useSyncExternalStore(react.useCallback(callback => {
var formElement = util.getFormElement(formRef);
if (formElement) {
var _formData = dom.getFormData(formElement);
formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ? _formData : new URLSearchParams(Array.from(_formData).map(_ref => {
var [key, value] = _ref;
return [key, value.toString()];
}));
}
var unsubscribe = observer.onFormUpdate(event => {
if (event.target === util.getFormElement(formRef)) {
var _formData2 = dom.getFormData(event.target, event.submitter);
formDataRef.current = options !== null && options !== void 0 && options.acceptFiles ? _formData2 : new URLSearchParams(Array.from(_formData2).map(_ref2 => {
var [key, value] = _ref2;
return [key, value.toString()];
}));
callback();
}
});
return unsubscribe;
}, [observer, formRef, options === null || options === void 0 ? void 0 : options.acceptFiles]), () => {
// @ts-expect-error FIXME
var result = select(formDataRef.current, valueRef.current);
if (typeof valueRef.current !== 'undefined' && dom.unstable_deepEqual(result, valueRef.current)) {
return valueRef.current;
}
valueRef.current = result;
return result;
}, () => select(null, undefined));
return value;
}
exports.useControl = useControl;
exports.useFormData = useFormData;