UNPKG

@conform-to/react

Version:

Conform view adapter for react

232 lines (224 loc) 10.2 kB
'use strict'; 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;