UNPKG

@conform-to/react

Version:

Conform view adapter for react

286 lines (279 loc) 9.58 kB
import { unstable_updateField } from '@conform-to/dom'; import { useRef, useEffect, useMemo, useState } from 'react'; function getFormElement(formId) { return document.forms.namedItem(formId); } function getFieldElements(form, name) { var field = form === null || form === void 0 ? void 0 : form.elements.namedItem(name); var elements = !field ? [] : field instanceof Element ? [field] : Array.from(field.values()); return elements.filter(element => element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement); } function getEventTarget(form, name, value) { var _elements$; var elements = getFieldElements(form, name); if (elements.length > 1) { var options = typeof value === 'string' ? [value] : value; for (var element of elements) { if (typeof options !== 'undefined' && element instanceof HTMLInputElement && element.type === 'checkbox' && (element.checked ? options.includes(element.value) : !options.includes(element.value))) { continue; } return element; } } return (_elements$ = elements[0]) !== null && _elements$ !== void 0 ? _elements$ : null; } function createDummySelect(form, name, value) { var select = document.createElement('select'); var options = typeof value === 'string' ? [value] : value !== null && value !== void 0 ? value : []; select.name = name; select.multiple = Array.isArray(value); select.dataset.conform = 'true'; // To make sure the input is hidden but still focusable select.setAttribute('aria-hidden', 'true'); select.tabIndex = -1; select.style.position = 'absolute'; select.style.width = '1px'; select.style.height = '1px'; select.style.padding = '0'; select.style.margin = '-1px'; select.style.overflow = 'hidden'; select.style.clip = 'rect(0,0,0,0)'; select.style.whiteSpace = 'nowrap'; select.style.border = '0'; for (var option of options) { select.options.add(new Option(option, option, true, true)); } form.appendChild(select); return select; } function isDummySelect(element) { return element.dataset.conform === 'true'; } function getInputValue(element) { if (element instanceof HTMLSelectElement) { var _value$; var _value = Array.from(element.selectedOptions).map(option => option.value); return element.multiple ? _value : (_value$ = _value[0]) !== null && _value$ !== void 0 ? _value$ : null; } if (element instanceof HTMLInputElement && (element.type === 'radio' || element.type === 'checkbox')) { return element.checked ? element.value : null; } return element.value; } function useInputEvent(onUpdate) { var ref = useRef(null); var observerRef = useRef(null); var eventDispatched = useRef({ change: false, focus: false, blur: false }); useEffect(() => { var createEventListener = listener => { return event => { var element = ref.current; if (element && event.target === element) { eventDispatched.current[listener] = true; } }; }; 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); }; }, [ref]); return useMemo(() => { return { change(value) { if (!eventDispatched.current.change) { eventDispatched.current.change = true; var element = ref.current; if (element) { unstable_updateField(element, { value }); // Dispatch input event with the updated input value element.dispatchEvent(new InputEvent('input', { bubbles: true })); // Dispatch change event (necessary for select to update the selected option) element.dispatchEvent(new Event('change', { bubbles: true })); } } eventDispatched.current.change = false; }, focus() { if (!eventDispatched.current.focus) { eventDispatched.current.focus = true; var element = ref.current; if (element) { element.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); element.dispatchEvent(new FocusEvent('focus')); } } eventDispatched.current.focus = false; }, blur() { if (!eventDispatched.current.blur) { eventDispatched.current.blur = true; var element = ref.current; if (element) { element.dispatchEvent(new FocusEvent('focusout', { bubbles: true })); element.dispatchEvent(new FocusEvent('blur')); } } eventDispatched.current.blur = false; }, register(element) { ref.current = element; if (observerRef.current) { observerRef.current.disconnect(); observerRef.current = null; } if (!element) { return; } observerRef.current = new MutationObserver(mutations => { var _loop = function _loop() { if (mutation.type === 'attributes') { var _getInputValue; var nextValue = (_getInputValue = getInputValue(element)) !== null && _getInputValue !== void 0 ? _getInputValue : undefined; onUpdate(prevValue => { if (nextValue === prevValue || // If the value is an array, check if the current value is the same as the new value JSON.stringify(prevValue) === JSON.stringify(nextValue)) { return prevValue; } return nextValue; }); } }; for (var mutation of mutations) { _loop(); } }); observerRef.current.observe(element, { attributes: true, attributeFilter: ['data-conform'] }); } }; }, [onUpdate]); } function useInputValue(options) { var initializeValue = () => { var _options$initialValue; if (typeof options.initialValue === 'string') { // @ts-expect-error FIXME: To ensure that the type of value is also `string | undefined` if initialValue is not an array return options.initialValue; } // @ts-expect-error Same as above return (_options$initialValue = options.initialValue) === null || _options$initialValue === void 0 ? void 0 : _options$initialValue.map(value => value !== null && value !== void 0 ? value : ''); }; var [key, setKey] = useState(options.key); var [value, setValue] = useState(initializeValue); if (key !== options.key) { setValue(initializeValue); setKey(options.key); } return [value, setValue]; } function useControl(meta) { var [value, setValue] = useInputValue(meta); var { register, change, focus, blur } = useInputEvent( // @ts-expect-error We will fix the type when stabilizing the API setValue); var handleChange = value => { setValue(value); change(value); }; var refCallback = element => { register(element); if (!element) { return; } // We were trying to sync the value based on key previously // This is now handled mostly by the side effect // But we still need to set the initial value for backward compatibility if (!element.dataset.conform) { unstable_updateField(element, { value }); } }; return { register: refCallback, value, change: handleChange, focus, blur }; } function useInputControl(meta) { var [value, setValue] = useInputValue(meta); var initializedRef = useRef(false); var { register, change, focus, blur } = useInputEvent( // @ts-expect-error We will fix the type when stabilizing the API setValue); useEffect(() => { var form = getFormElement(meta.formId); if (!form) { // eslint-disable-next-line no-console console.warn("useInputControl is unable to find form#".concat(meta.formId, " and identify if a dummy input is required")); return; } var element = getEventTarget(form, meta.name); if (!element && typeof value !== 'undefined' && (!Array.isArray(value) || value.length > 0)) { element = createDummySelect(form, meta.name, value); } register(element); if (!initializedRef.current) { initializedRef.current = true; } else { change(value !== null && value !== void 0 ? value : ''); } return () => { register(null); var elements = getFieldElements(form, meta.name); for (var _element of elements) { if (isDummySelect(_element)) { _element.remove(); } } }; }, [meta.formId, meta.name, value, change, register]); return { value, change: setValue, focus, blur }; } function Control(props) { var control = useControl(props.meta); return props.render(control); } export { Control, createDummySelect, getEventTarget, getFieldElements, getFormElement, getInputValue, isDummySelect, useControl, useInputControl, useInputEvent, useInputValue };