@conform-to/react
Version:
Conform view adapter for react
300 lines (291 loc) • 9.93 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var dom = require('@conform-to/dom');
var react = require('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 = react.useRef(null);
var observerRef = react.useRef(null);
var eventDispatched = react.useRef({
change: false,
focus: false,
blur: false
});
react.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 react.useMemo(() => {
return {
change(value) {
if (!eventDispatched.current.change) {
eventDispatched.current.change = true;
var element = ref.current;
if (element) {
dom.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] = react.useState(options.key);
var [value, setValue] = react.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) {
dom.unstable_updateField(element, {
value
});
}
};
return {
register: refCallback,
value,
change: handleChange,
focus,
blur
};
}
function useInputControl(meta) {
var [value, setValue] = useInputValue(meta);
var initializedRef = react.useRef(false);
var {
register,
change,
focus,
blur
} = useInputEvent(
// @ts-expect-error We will fix the type when stabilizing the API
setValue);
react.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);
}
exports.Control = Control;
exports.createDummySelect = createDummySelect;
exports.getEventTarget = getEventTarget;
exports.getFieldElements = getFieldElements;
exports.getFormElement = getFormElement;
exports.getInputValue = getInputValue;
exports.isDummySelect = isDummySelect;
exports.useControl = useControl;
exports.useInputControl = useInputControl;
exports.useInputEvent = useInputEvent;
exports.useInputValue = useInputValue;