UNPKG

@conform-to/react

Version:

Conform view adapter for react

336 lines (313 loc) 12.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var future = require('@conform-to/dom/future'); var intent = require('./intent.js'); var state = require('./state.js'); function getFormElement(formRef) { if (typeof formRef === 'undefined') { return null; } if (typeof formRef !== 'string') { var element = formRef.current; if (!element) { return null; } return future.isGlobalInstance(element, 'HTMLFormElement') ? element : element.form; } return document.forms.namedItem(formRef); } function getSubmitEvent(event) { if (event.type !== 'submit') { throw new Error('The event is not a submit event'); } return event.nativeEvent; } function initializeField(element, options) { var _options$value; if (element.dataset.conform) { return; } var defaultValue = typeof (options === null || options === void 0 ? void 0 : options.value) === 'string' || typeof (options === null || options === void 0 ? void 0 : options.defaultChecked) === 'boolean' ? options.defaultChecked ? (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : 'on' : null : options === null || options === void 0 ? void 0 : options.defaultValue; // Use change helper to set value and dispatch events // This syncs React's internal value tracker so subsequent // programmatic changes will properly trigger onChange if (defaultValue !== undefined) { future.change(element, defaultValue, { // To avoid triggering validation on initialization preventDefault: true }); } // Set the default value after change to preserve it for form reset future.updateField(element, { defaultValue }); element.dataset.conform = 'initialized'; } function resolveControlPayload(input) { if (Array.isArray(input)) { var options; for (var element of input) { if (element.type === 'radio' && element.checked) { return element.value; } if (element.type === 'checkbox') { var _options; (_options = options) !== null && _options !== void 0 ? _options : options = []; if (element.checked) { options.push(element.value); } } } return options; } if (input instanceof HTMLInputElement) { switch (input.type) { case 'file': { return input.files ? Array.from(input.files) : []; } case 'radio': case 'checkbox': return input.checked ? input.value : null; } } else if (input instanceof HTMLSelectElement && input.multiple) { return Array.from(input.selectedOptions).map(option => option.value); } else if (input instanceof HTMLFieldSetElement) { if (input.elements.length === 0) { return null; } var result = {}; var entries = new Map(); for (var _element of input.elements) { if (future.isFieldElement(_element)) { var payload = resolveControlPayload(_element); var value = entries.get(_element.name); if (_element.type === 'checkbox') { entries.set(_element.name, value === undefined ? payload : (Array.isArray(value) ? [...value, payload] : [value, payload]).filter(v => v !== null)); } else if (_element.type === 'radio') { entries.set(_element.name, value == null ? payload : payload === null ? value : payload); } else { entries.set(_element.name, value === undefined ? payload : Array.isArray(value) ? [...value, payload] : [value, payload]); } } } for (var [name, _value] of entries) { future.setPathValue(result, name, _value); } return future.getPathValue(result, input.name); } return input.value; } function deriveDefaultPayload(options) { if ('defaultChecked' in options && typeof options.defaultChecked === 'boolean') { var _options$value2; return options.defaultChecked ? (_options$value2 = options.value) !== null && _options$value2 !== void 0 ? _options$value2 : 'on' : null; } if ('defaultValue' in options) { return options.defaultValue; } } /** * Focuses the first field with validation errors on default form submission. * Does nothing if the submission was triggered with a specific intent (e.g. validate / insert) */ function focusFirstInvalidField(ctx) { if (ctx.intent) { return; } for (var element of ctx.formElement.elements) { if (!(future.isFieldElement(element) || element instanceof HTMLFieldSetElement) || element.name === '' || !state.hasFieldError(ctx.error, element.name)) { continue; } // Treat fieldset as a focusable field only if it is hidden if (element.type === 'fieldset' && !element.hidden) { continue; } if (element.hidden || element.type === 'hidden' || element.type === 'fieldset') { future.focus(element); } else { element.focus(); } break; } } function updateFormValue(form, targetValue, serialize) { for (var element of form.elements) { if (future.isFieldElement(element) && element.name && element.type !== 'hidden') { var fieldValue = future.getPathValue(targetValue, element.name); if (element.type === 'file' && fieldValue === undefined) { // Do not update file inputs unless there's a target value continue; } var value = serialize(fieldValue, { name: element.name }); // Treat undefined as null to clear the field value future.change(element, value !== undefined ? value : null, { preventDefault: true }); } } } function resetFormValue(form, defaultValue, serialize) { for (var element of form.elements) { if (future.isFieldElement(element) && element.name && element.type !== 'hidden' && element.type !== 'file') { var fieldValue = future.getPathValue(defaultValue, element.name); var value = serialize(fieldValue, { name: element.name }); future.updateField(element, { defaultValue: value !== undefined ? value : null }); } } form.reset(); } /** * Creates a proxy that dynamically generates intent dispatch functions. * Each property access returns a function that submits the intent to the form. */ function createIntentDispatcher(formElement, intentName) { return new Proxy({}, { get(target, type, receiver) { if (typeof type === 'string') { var _target$type; // @ts-expect-error (_target$type = target[type]) !== null && _target$type !== void 0 ? _target$type : target[type] = payload => { var form = typeof formElement === 'function' ? formElement() : formElement; if (!form) { throw new Error("Dispatching \"".concat(type, "\" intent failed; No form element found.")); } future.requestIntent(form, intentName, intent.serializeIntent({ type, payload })); }; } return Reflect.get(target, type, receiver); } }); } var PERSIST_ATTR = 'data-conform-persist'; var containerCache = new WeakMap(); /** * Gets or creates a hidden container for persisted inputs. * Using a container div instead of appending directly to <form> provides ~10x * better performance (form.elements bookkeeping is expensive at scale). */ function getPersistContainer(form) { var container = containerCache.get(form); // Verify container is still attached to the form if (container && container.parentNode !== form) { container = undefined; } if (!container) { container = form.ownerDocument.createElement('div'); container.setAttribute(PERSIST_ATTR, ''); container.hidden = true; form.appendChild(container); containerCache.set(form, container); } return container; } /** * Restores values from preserved inputs and removes them. * Called when PreserveBoundary mounts. */ function cleanupPreservedInputs(boundary, form, name) { var inputs = boundary.querySelectorAll('input,select,textarea'); var container = getPersistContainer(form); for (var input of inputs) { if (!future.isFieldElement(input) || !input.name) { continue; } // For checkbox/radio, match by field name + value (+ boundary name if provided) // For other inputs, match by field name only (+ boundary name if provided) var isCheckboxOrRadio = input.type === 'checkbox' || input.type === 'radio'; // Query the persist container, not the whole form var boundarySelector = name ? "[".concat(PERSIST_ATTR, "=\"").concat(name, "\"]") : ''; var selector = isCheckboxOrRadio ? "".concat(boundarySelector, "[name=\"").concat(input.name, "\"][value=\"").concat(input.value, "\"]") : "".concat(boundarySelector, "[name=\"").concat(input.name, "\"]"); var persisted = container.querySelector(selector); if (persisted) { if (input instanceof HTMLInputElement && persisted instanceof HTMLInputElement) { if (isCheckboxOrRadio) { input.checked = persisted.checked; } else if (input.type === 'file') { // Restore files from the persisted input (may be empty) input.files = persisted.files; } else { input.value = persisted.value; } } else if (input instanceof HTMLSelectElement && persisted instanceof HTMLSelectElement) { var _loop = function _loop(option) { var _persistedOption$sele; var persistedOption = Array.from(persisted.options).find(o => o.value === option.value); option.selected = (_persistedOption$sele = persistedOption === null || persistedOption === void 0 ? void 0 : persistedOption.selected) !== null && _persistedOption$sele !== void 0 ? _persistedOption$sele : false; }; for (var option of input.options) { _loop(option); } } else if (input instanceof HTMLTextAreaElement && persisted instanceof HTMLTextAreaElement) { input.value = persisted.value; } persisted.remove(); } } // If name is provided, remove any remaining persisted inputs with this name // (handles the case where inputs were removed from the boundary) if (name) { var remainingPersisted = container.querySelectorAll("[".concat(PERSIST_ATTR, "=\"").concat(name, "\"]")); remainingPersisted.forEach(el => el.remove()); } } /** * Clones inputs as hidden elements to preserve their values. * Called when PreserveBoundary unmounts. */ function preserveInputs(inputs, form, name) { // Get the persist container once, outside the loop var container = getPersistContainer(form); for (var input of inputs) { if (!future.isFieldElement(input) || !input.name) { continue; } // Skip unchecked checkbox/radio (they don't contribute to FormData) if (input instanceof HTMLInputElement && (input.type === 'checkbox' || input.type === 'radio') && !input.checked) { continue; } // Clone the input element var clone = input.cloneNode(true); // Mark with name if provided, and hide it if (name) { clone.setAttribute(PERSIST_ATTR, name); } clone.hidden = true; // Copy dynamic state that cloneNode doesn't preserve if (input instanceof HTMLSelectElement) { // cloneNode doesn't copy selected state for options for (var i = 0; i < input.options.length; i++) { var inputOption = input.options[i]; var cloneOption = clone.options[i]; if (inputOption && cloneOption) { cloneOption.selected = inputOption.selected; } } } else if (input instanceof HTMLInputElement && input.type === 'file') { // cloneNode doesn't copy files clone.files = input.files; } // Append to persist container (faster than appending directly to form) container.appendChild(clone); } } exports.cleanupPreservedInputs = cleanupPreservedInputs; exports.createIntentDispatcher = createIntentDispatcher; exports.deriveDefaultPayload = deriveDefaultPayload; exports.focusFirstInvalidField = focusFirstInvalidField; exports.getFormElement = getFormElement; exports.getSubmitEvent = getSubmitEvent; exports.initializeField = initializeField; exports.preserveInputs = preserveInputs; exports.resetFormValue = resetFormValue; exports.resolveControlPayload = resolveControlPayload; exports.updateFormValue = updateFormValue;