@conform-to/react
Advanced tools
+1
-0
@@ -192,2 +192,3 @@ 'use strict'; | ||
| case 'multiple': | ||
| case 'accept': | ||
| return (_state$constraint$nam = state.constraint[name]) === null || _state$constraint$nam === void 0 ? void 0 : _state$constraint$nam[key]; | ||
@@ -194,0 +195,0 @@ case 'getFieldList': |
+1
-0
@@ -188,2 +188,3 @@ import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.mjs'; | ||
| case 'multiple': | ||
| case 'accept': | ||
| return (_state$constraint$nam = state.constraint[name]) === null || _state$constraint$nam === void 0 ? void 0 : _state$constraint$nam[key]; | ||
@@ -190,0 +191,0 @@ case 'getFieldList': |
+10
-0
@@ -37,2 +37,12 @@ import { Serialize } from '@conform-to/dom/future'; | ||
| export declare function createIntentDispatcher<FormShape extends Record<string, any>>(formElement: HTMLFormElement | (() => HTMLFormElement | null), intentName: string): IntentDispatcher<FormShape>; | ||
| /** | ||
| * Restores values from preserved inputs and removes them. | ||
| * Called when PreserveBoundary mounts. | ||
| */ | ||
| export declare function cleanupPreservedInputs(boundary: HTMLElement, form: HTMLFormElement, name?: string): void; | ||
| /** | ||
| * Clones inputs as hidden elements to preserve their values. | ||
| * Called when PreserveBoundary unmounts. | ||
| */ | ||
| export declare function preserveInputs(inputs: Iterable<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>, form: HTMLFormElement, name?: string): void; | ||
| //# sourceMappingURL=dom.d.ts.map |
+126
-0
@@ -240,3 +240,128 @@ 'use strict'; | ||
| } | ||
| 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.createDefaultSnapshot = createDefaultSnapshot; | ||
@@ -252,3 +377,4 @@ exports.createIntentDispatcher = createIntentDispatcher; | ||
| exports.makeInputFocusable = makeInputFocusable; | ||
| exports.preserveInputs = preserveInputs; | ||
| exports.resetFormValue = resetFormValue; | ||
| exports.updateFormValue = updateFormValue; |
+125
-1
@@ -236,3 +236,127 @@ import { change, updateField, isGlobalInstance, isFieldElement, requestIntent, getValueAtPath } from '@conform-to/dom/future'; | ||
| } | ||
| var PERSIST_ATTR = 'data-conform-persist'; | ||
| var containerCache = new WeakMap(); | ||
| export { createDefaultSnapshot, createIntentDispatcher, focusFirstInvalidField, getCheckboxGroupValue, getFormElement, getInputSnapshot, getRadioGroupValue, getSubmitEvent, initializeField, makeInputFocusable, resetFormValue, updateFormValue }; | ||
| /** | ||
| * 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 (!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 (!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); | ||
| } | ||
| } | ||
| export { cleanupPreservedInputs, createDefaultSnapshot, createIntentDispatcher, focusFirstInvalidField, getCheckboxGroupValue, getFormElement, getInputSnapshot, getRadioGroupValue, getSubmitEvent, initializeField, makeInputFocusable, preserveInputs, resetFormValue, updateFormValue }; |
@@ -0,1 +1,2 @@ | ||
| 'use client'; | ||
| 'use strict'; | ||
@@ -2,0 +3,0 @@ |
@@ -0,1 +1,2 @@ | ||
| 'use client'; | ||
| import { objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs'; | ||
@@ -2,0 +3,0 @@ import { isFieldElement } from '@conform-to/dom'; |
@@ -19,2 +19,21 @@ import { type FieldName, type FormValue, type Serialize, type SubmissionResult, createGlobalFormsObserver } from '@conform-to/dom/future'; | ||
| /** | ||
| * Preserves form field values when its contents are unmounted. | ||
| * Useful for multi-step forms and virtualized lists. | ||
| * | ||
| * @see https://conform.guide/api/react/future/PreserveBoundary | ||
| */ | ||
| export declare function PreserveBoundary(props: { | ||
| /** | ||
| * A unique name for the boundary within the form. Used to ensure proper | ||
| * unmount/remount behavior and to isolate preserved inputs between boundaries. | ||
| */ | ||
| name: string; | ||
| /** | ||
| * The id of the form to associate with. Only needed when the boundary | ||
| * is rendered outside the form element. | ||
| */ | ||
| form?: string; | ||
| children: React.ReactNode; | ||
| }): React.ReactElement; | ||
| /** | ||
| * @deprecated Replaced by the `configureForms` factory API. This will be removed in the next minor version. If you are not ready to migrate, please pin to `v1.16.0`. | ||
@@ -21,0 +40,0 @@ */ |
+75
-26
@@ -43,2 +43,43 @@ 'use client'; | ||
| /** | ||
| * Preserves form field values when its contents are unmounted. | ||
| * Useful for multi-step forms and virtualized lists. | ||
| * | ||
| * @see https://conform.guide/api/react/future/PreserveBoundary | ||
| */ | ||
| function PreserveBoundary(props) { | ||
| // name is used as key so React properly unmounts/remounts when switching | ||
| // between boundaries. Without it, both sides of a ternary share | ||
| // key={undefined} and React reuses the instance (useId and key prop | ||
| // can't help here). This is why name is required. | ||
| return /*#__PURE__*/jsxRuntime.jsx(PreserveBoundaryImpl, _rollupPluginBabelHelpers.objectSpread2({}, props), props.name); | ||
| } | ||
| function PreserveBoundaryImpl(props) { | ||
| var fieldsetRef = react.useRef(null); | ||
| // useLayoutEffect to restore values before paint, avoiding flash of default values | ||
| useSafeLayoutEffect(() => { | ||
| var fieldset = fieldsetRef.current; | ||
| if (!fieldset || !fieldset.form) { | ||
| return; | ||
| } | ||
| var form = fieldset.form; | ||
| // On mount: restore values from preserved inputs | ||
| dom.cleanupPreservedInputs(fieldset, form, props.name); | ||
| return () => { | ||
| // On unmount: preserve input values | ||
| dom.preserveInputs(fieldset.querySelectorAll('input,select,textarea'), form, props.name); | ||
| }; | ||
| }, [props.name]); | ||
| return /*#__PURE__*/jsxRuntime.jsx("fieldset", { | ||
| ref: fieldsetRef, | ||
| form: props.form, | ||
| style: { | ||
| display: 'contents' | ||
| }, | ||
| children: props.children | ||
| }); | ||
| } | ||
| /** | ||
| * @deprecated Replaced by the `configureForms` factory API. This will be removed in the next minor version. If you are not ready to migrate, please pin to `v1.16.0`. | ||
@@ -81,7 +122,12 @@ */ | ||
| if (lastResult) { | ||
| state$1 = state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, lastResult), {}, { | ||
| var intent$1 = lastResult.submission.intent ? intent.deserializeIntent(lastResult.submission.intent) : null; | ||
| var result = intent.applyIntent(lastResult, intent$1, { | ||
| handlers: intent.intentHandlers | ||
| }); | ||
| state$1 = state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, { | ||
| type: 'initialize', | ||
| intent: lastResult.submission.intent ? intent.deserializeIntent(lastResult.submission.intent) : null, | ||
| intent: intent$1, | ||
| ctx: { | ||
| handlers: intent.actionHandlers, | ||
| handlers: intent.intentHandlers, | ||
| cancelled: result !== lastResult, | ||
| reset: defaultValue => state.initializeState({ | ||
@@ -104,10 +150,13 @@ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue, | ||
| var handleSubmission = react.useCallback(function (type, result) { | ||
| var _optionsRef$current$o, _optionsRef$current; | ||
| var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : optionsRef.current; | ||
| var intent$1 = result.submission.intent ? intent.deserializeIntent(result.submission.intent) : null; | ||
| setState(state$1 => state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, { | ||
| var finalResult = intent.applyIntent(result, intent$1, { | ||
| handlers: intent.intentHandlers | ||
| }); | ||
| setState(state$1 => state.updateState(state$1, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, finalResult), {}, { | ||
| type, | ||
| intent: intent$1, | ||
| ctx: { | ||
| handlers: intent.actionHandlers, | ||
| handlers: intent.intentHandlers, | ||
| cancelled: finalResult !== result, | ||
| reset(defaultValue) { | ||
@@ -123,10 +172,11 @@ return state.initializeState({ | ||
| var formElement = dom.getFormElement(formRef); | ||
| if (!formElement || !result.error) { | ||
| return; | ||
| if (formElement && result.error) { | ||
| var _optionsRef$current$o, _optionsRef$current; | ||
| (_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, { | ||
| formElement, | ||
| error: result.error, | ||
| intent: intent$1 | ||
| }); | ||
| } | ||
| (_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, { | ||
| formElement, | ||
| error: result.error, | ||
| intent: intent$1 | ||
| }); | ||
| return finalResult; | ||
| }, [formRef, optionsRef]); | ||
@@ -207,3 +257,3 @@ if (options.key !== keyRef.current) { | ||
| } | ||
| var value = intent.applyIntent(submission); | ||
| var value = intent.resolveIntent(submission); | ||
| var submissionResult = future.report(submission, { | ||
@@ -213,8 +263,2 @@ keepFiles: true, | ||
| }); | ||
| // If there is target value, keep track of it as pending value | ||
| if (submission.payload !== value) { | ||
| var _ref; | ||
| pendingValueRef.current = (_ref = value !== null && value !== void 0 ? value : optionsRef.current.defaultValue) !== null && _ref !== void 0 ? _ref : {}; | ||
| } | ||
| var validateResult = | ||
@@ -246,7 +290,7 @@ // Skip validation on form reset | ||
| // Update the form when the validation result is resolved | ||
| asyncResult.then(_ref2 => { | ||
| asyncResult.then(_ref => { | ||
| var { | ||
| error, | ||
| value | ||
| } = _ref2; | ||
| } = _ref; | ||
| // Update the form with the validation result | ||
@@ -259,3 +303,3 @@ // There is no need to flush the update in this case | ||
| // If the form is meant to be submitted and there is no error | ||
| if (error === null && !submission.intent) { | ||
| if (submissionResult.error === null && !submission.intent) { | ||
| var _event = future.createSubmitEvent(submitEvent.submitter); | ||
@@ -275,3 +319,7 @@ | ||
| } | ||
| handleSubmission('client', submissionResult); | ||
| var clientResult = handleSubmission('client', submissionResult); | ||
| if (clientResult.reset || clientResult.targetValue !== undefined) { | ||
| var _ref2, _clientResult$targetV; | ||
| pendingValueRef.current = (_ref2 = (_clientResult$targetV = clientResult.targetValue) !== null && _clientResult$targetV !== void 0 ? _clientResult$targetV : optionsRef.current.defaultValue) !== null && _ref2 !== void 0 ? _ref2 : {}; | ||
| } | ||
| if ( | ||
@@ -281,6 +329,6 @@ // If client validation happens | ||
| // Either the form is not meant to be submitted (i.e. intent is present) or there is an error / pending validation | ||
| submissionResult.submission.intent || submissionResult.error !== null)) { | ||
| clientResult.submission.intent || clientResult.error !== null)) { | ||
| event.preventDefault(); | ||
| } | ||
| result = submissionResult; | ||
| result = clientResult; | ||
| } | ||
@@ -851,2 +899,3 @@ | ||
| exports.INITIAL_KEY = INITIAL_KEY; | ||
| exports.PreserveBoundary = PreserveBoundary; | ||
| exports.useConform = useConform; | ||
@@ -853,0 +902,0 @@ exports.useControl = useControl; |
+79
-31
| 'use client'; | ||
| import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs'; | ||
| import { objectSpread2 as _objectSpread2, objectWithoutProperties as _objectWithoutProperties } from '../_virtual/_rollupPluginBabelHelpers.mjs'; | ||
| import { DEFAULT_INTENT_NAME, createGlobalFormsObserver, serialize, isFieldElement, deepEqual, change, focus, blur, getFormData, parseSubmission, report, createSubmitEvent } from '@conform-to/dom/future'; | ||
| import { createContext, useContext, useMemo, useId, useRef, useEffect, useSyncExternalStore, useCallback, useState, useLayoutEffect } from 'react'; | ||
| import { createContext, useContext, useMemo, useRef, useId, useEffect, useSyncExternalStore, useCallback, useLayoutEffect, useState } from 'react'; | ||
| import { resolveStandardSchemaResult, resolveValidateResult, appendUniqueItem } from './util.mjs'; | ||
| import { isTouched, getFormMetadata, getFieldset, getField, initializeState, updateState } from './state.mjs'; | ||
| import { deserializeIntent, actionHandlers, applyIntent } from './intent.mjs'; | ||
| import { focusFirstInvalidField, getFormElement, createIntentDispatcher, createDefaultSnapshot, getRadioGroupValue, getCheckboxGroupValue, getInputSnapshot, makeInputFocusable, initializeField, resetFormValue, updateFormValue, getSubmitEvent } from './dom.mjs'; | ||
| import { deserializeIntent, applyIntent, intentHandlers, resolveIntent } from './intent.mjs'; | ||
| import { cleanupPreservedInputs, preserveInputs, focusFirstInvalidField, getFormElement, createIntentDispatcher, createDefaultSnapshot, getRadioGroupValue, getCheckboxGroupValue, getInputSnapshot, makeInputFocusable, initializeField, resetFormValue, updateFormValue, getSubmitEvent } from './dom.mjs'; | ||
| import { jsx } from 'react/jsx-runtime'; | ||
@@ -39,2 +39,43 @@ | ||
| /** | ||
| * Preserves form field values when its contents are unmounted. | ||
| * Useful for multi-step forms and virtualized lists. | ||
| * | ||
| * @see https://conform.guide/api/react/future/PreserveBoundary | ||
| */ | ||
| function PreserveBoundary(props) { | ||
| // name is used as key so React properly unmounts/remounts when switching | ||
| // between boundaries. Without it, both sides of a ternary share | ||
| // key={undefined} and React reuses the instance (useId and key prop | ||
| // can't help here). This is why name is required. | ||
| return /*#__PURE__*/jsx(PreserveBoundaryImpl, _objectSpread2({}, props), props.name); | ||
| } | ||
| function PreserveBoundaryImpl(props) { | ||
| var fieldsetRef = useRef(null); | ||
| // useLayoutEffect to restore values before paint, avoiding flash of default values | ||
| useSafeLayoutEffect(() => { | ||
| var fieldset = fieldsetRef.current; | ||
| if (!fieldset || !fieldset.form) { | ||
| return; | ||
| } | ||
| var form = fieldset.form; | ||
| // On mount: restore values from preserved inputs | ||
| cleanupPreservedInputs(fieldset, form, props.name); | ||
| return () => { | ||
| // On unmount: preserve input values | ||
| preserveInputs(fieldset.querySelectorAll('input,select,textarea'), form, props.name); | ||
| }; | ||
| }, [props.name]); | ||
| return /*#__PURE__*/jsx("fieldset", { | ||
| ref: fieldsetRef, | ||
| form: props.form, | ||
| style: { | ||
| display: 'contents' | ||
| }, | ||
| children: props.children | ||
| }); | ||
| } | ||
| /** | ||
| * @deprecated Replaced by the `configureForms` factory API. This will be removed in the next minor version. If you are not ready to migrate, please pin to `v1.16.0`. | ||
@@ -77,7 +118,12 @@ */ | ||
| if (lastResult) { | ||
| state = updateState(state, _objectSpread2(_objectSpread2({}, lastResult), {}, { | ||
| var intent = lastResult.submission.intent ? deserializeIntent(lastResult.submission.intent) : null; | ||
| var result = applyIntent(lastResult, intent, { | ||
| handlers: intentHandlers | ||
| }); | ||
| state = updateState(state, _objectSpread2(_objectSpread2({}, result), {}, { | ||
| type: 'initialize', | ||
| intent: lastResult.submission.intent ? deserializeIntent(lastResult.submission.intent) : null, | ||
| intent, | ||
| ctx: { | ||
| handlers: actionHandlers, | ||
| handlers: intentHandlers, | ||
| cancelled: result !== lastResult, | ||
| reset: defaultValue => initializeState({ | ||
@@ -100,10 +146,13 @@ defaultValue: defaultValue !== null && defaultValue !== void 0 ? defaultValue : options.defaultValue, | ||
| var handleSubmission = useCallback(function (type, result) { | ||
| var _optionsRef$current$o, _optionsRef$current; | ||
| var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : optionsRef.current; | ||
| var intent = result.submission.intent ? deserializeIntent(result.submission.intent) : null; | ||
| setState(state => updateState(state, _objectSpread2(_objectSpread2({}, result), {}, { | ||
| var finalResult = applyIntent(result, intent, { | ||
| handlers: intentHandlers | ||
| }); | ||
| setState(state => updateState(state, _objectSpread2(_objectSpread2({}, finalResult), {}, { | ||
| type, | ||
| intent, | ||
| ctx: { | ||
| handlers: actionHandlers, | ||
| handlers: intentHandlers, | ||
| cancelled: finalResult !== result, | ||
| reset(defaultValue) { | ||
@@ -119,10 +168,11 @@ return initializeState({ | ||
| var formElement = getFormElement(formRef); | ||
| if (!formElement || !result.error) { | ||
| return; | ||
| if (formElement && result.error) { | ||
| var _optionsRef$current$o, _optionsRef$current; | ||
| (_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, { | ||
| formElement, | ||
| error: result.error, | ||
| intent | ||
| }); | ||
| } | ||
| (_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, { | ||
| formElement, | ||
| error: result.error, | ||
| intent | ||
| }); | ||
| return finalResult; | ||
| }, [formRef, optionsRef]); | ||
@@ -203,3 +253,3 @@ if (options.key !== keyRef.current) { | ||
| } | ||
| var value = applyIntent(submission); | ||
| var value = resolveIntent(submission); | ||
| var submissionResult = report(submission, { | ||
@@ -209,8 +259,2 @@ keepFiles: true, | ||
| }); | ||
| // If there is target value, keep track of it as pending value | ||
| if (submission.payload !== value) { | ||
| var _ref; | ||
| pendingValueRef.current = (_ref = value !== null && value !== void 0 ? value : optionsRef.current.defaultValue) !== null && _ref !== void 0 ? _ref : {}; | ||
| } | ||
| var validateResult = | ||
@@ -242,7 +286,7 @@ // Skip validation on form reset | ||
| // Update the form when the validation result is resolved | ||
| asyncResult.then(_ref2 => { | ||
| asyncResult.then(_ref => { | ||
| var { | ||
| error, | ||
| value | ||
| } = _ref2; | ||
| } = _ref; | ||
| // Update the form with the validation result | ||
@@ -255,3 +299,3 @@ // There is no need to flush the update in this case | ||
| // If the form is meant to be submitted and there is no error | ||
| if (error === null && !submission.intent) { | ||
| if (submissionResult.error === null && !submission.intent) { | ||
| var _event = createSubmitEvent(submitEvent.submitter); | ||
@@ -271,3 +315,7 @@ | ||
| } | ||
| handleSubmission('client', submissionResult); | ||
| var clientResult = handleSubmission('client', submissionResult); | ||
| if (clientResult.reset || clientResult.targetValue !== undefined) { | ||
| var _ref2, _clientResult$targetV; | ||
| pendingValueRef.current = (_ref2 = (_clientResult$targetV = clientResult.targetValue) !== null && _clientResult$targetV !== void 0 ? _clientResult$targetV : optionsRef.current.defaultValue) !== null && _ref2 !== void 0 ? _ref2 : {}; | ||
| } | ||
| if ( | ||
@@ -277,6 +325,6 @@ // If client validation happens | ||
| // Either the form is not meant to be submitted (i.e. intent is present) or there is an error / pending validation | ||
| submissionResult.submission.intent || submissionResult.error !== null)) { | ||
| clientResult.submission.intent || clientResult.error !== null)) { | ||
| event.preventDefault(); | ||
| } | ||
| result = submissionResult; | ||
| result = clientResult; | ||
| } | ||
@@ -842,2 +890,2 @@ | ||
| export { FormContextContext, FormOptionsProvider, FormProvider, GlobalFormOptionsContext, INITIAL_KEY, useConform, useControl, useField, useForm, useFormContext, useFormData, useFormMetadata, useIntent, useLatest, useSafeLayoutEffect }; | ||
| export { FormContextContext, FormOptionsProvider, FormProvider, GlobalFormOptionsContext, INITIAL_KEY, PreserveBoundary, useConform, useControl, useField, useForm, useFormContext, useFormData, useFormMetadata, useIntent, useLatest, useSafeLayoutEffect }; |
@@ -5,5 +5,5 @@ export type { FieldName, FormError, FormValue, Submission, SubmissionResult, } from '@conform-to/dom/future'; | ||
| export { configureForms } from './forms'; | ||
| export { FormProvider, FormOptionsProvider, useControl, useField, useForm, useFormData, useFormMetadata, useIntent, } from './hooks'; | ||
| export { PreserveBoundary, FormProvider, FormOptionsProvider, useControl, useField, useForm, useFormData, useFormMetadata, useIntent, } from './hooks'; | ||
| export { shape } from './util'; | ||
| export { memoize } from './memoize'; | ||
| //# sourceMappingURL=index.d.ts.map |
@@ -32,2 +32,3 @@ 'use strict'; | ||
| exports.FormProvider = hooks.FormProvider; | ||
| exports.PreserveBoundary = hooks.PreserveBoundary; | ||
| exports.useControl = hooks.useControl; | ||
@@ -34,0 +35,0 @@ exports.useField = hooks.useField; |
| export { getFieldValue, isDirty, parseSubmission, report } from '@conform-to/dom/future'; | ||
| export { configureForms } from './forms.mjs'; | ||
| export { FormOptionsProvider, FormProvider, useControl, useField, useForm, useFormData, useFormMetadata, useIntent } from './hooks.mjs'; | ||
| export { FormOptionsProvider, FormProvider, PreserveBoundary, useControl, useField, useForm, useFormData, useFormMetadata, useIntent } from './hooks.mjs'; | ||
| export { shape } from './util.mjs'; | ||
| export { memoize } from './memoize.mjs'; |
@@ -1,3 +0,3 @@ | ||
| import type { FormValue, Submission } from '@conform-to/dom/future'; | ||
| import type { ActionHandler, IntentDispatcher, UnknownIntent } from './types'; | ||
| import type { FormValue, Submission, SubmissionResult } from '@conform-to/dom/future'; | ||
| import type { IntentHandler, IntentDispatcher, UnknownIntent } from './types'; | ||
| /** | ||
@@ -15,5 +15,12 @@ * Serializes intent to string format: "type" or "type(payload)". | ||
| */ | ||
| export declare function applyIntent(submission: Submission, options?: { | ||
| handlers?: Record<string, ActionHandler>; | ||
| export declare function resolveIntent(submission: Submission, options?: { | ||
| handlers?: Record<string, IntentHandler>; | ||
| }): Record<string, FormValue> | undefined; | ||
| /** | ||
| * Resolves an intent after validation by calling the handler's onResolve. | ||
| * Mutates the result with updated value/error and returns whether the intent was cancelled. | ||
| */ | ||
| export declare function applyIntent<ErrorShape>(result: SubmissionResult<ErrorShape>, intent: UnknownIntent | null, options?: { | ||
| handlers?: Record<string, IntentHandler>; | ||
| }): SubmissionResult<ErrorShape>; | ||
| export declare function insertItem<Item>(list: Array<Item>, item: Item, index: number): void; | ||
@@ -33,5 +40,5 @@ export declare function removeItem(list: Array<unknown>, index: number): void; | ||
| */ | ||
| export declare const actionHandlers: { | ||
| [Type in keyof IntentDispatcher]: IntentDispatcher[Type] extends (payload: any) => void ? ActionHandler<IntentDispatcher[Type]> : never; | ||
| export declare const intentHandlers: { | ||
| [Type in keyof IntentDispatcher]: IntentDispatcher[Type] extends (payload: any) => void ? IntentHandler<IntentDispatcher[Type]> : never; | ||
| }; | ||
| //# sourceMappingURL=intent.d.ts.map |
+175
-68
@@ -49,4 +49,4 @@ 'use strict'; | ||
| */ | ||
| function applyIntent(submission, options) { | ||
| var _options$handlers, _handler$validatePayl, _handler$validatePayl2; | ||
| function resolveIntent(submission, options) { | ||
| var _options$handlers, _handler$validate, _handler$validate2; | ||
| if (!submission.intent) { | ||
@@ -56,9 +56,25 @@ return submission.payload; | ||
| var intent = deserializeIntent(submission.intent); | ||
| var handlers = (_options$handlers = options === null || options === void 0 ? void 0 : options.handlers) !== null && _options$handlers !== void 0 ? _options$handlers : actionHandlers; | ||
| var handlers = (_options$handlers = options === null || options === void 0 ? void 0 : options.handlers) !== null && _options$handlers !== void 0 ? _options$handlers : intentHandlers; | ||
| var handler = handlers[intent.type]; | ||
| if (handler && handler.onApply && ((_handler$validatePayl = (_handler$validatePayl2 = handler.validatePayload) === null || _handler$validatePayl2 === void 0 ? void 0 : _handler$validatePayl2.call(handler, intent.payload)) !== null && _handler$validatePayl !== void 0 ? _handler$validatePayl : true)) { | ||
| return handler.onApply(submission.payload, intent.payload); | ||
| if (handler !== null && handler !== void 0 && handler.resolve && ((_handler$validate = (_handler$validate2 = handler.validate) === null || _handler$validate2 === void 0 ? void 0 : _handler$validate2.call(handler, intent.payload)) !== null && _handler$validate !== void 0 ? _handler$validate : true)) { | ||
| return handler.resolve(submission.payload, intent.payload); | ||
| } | ||
| return submission.payload; | ||
| } | ||
| /** | ||
| * Resolves an intent after validation by calling the handler's onResolve. | ||
| * Mutates the result with updated value/error and returns whether the intent was cancelled. | ||
| */ | ||
| function applyIntent(result, intent, options) { | ||
| if (intent) { | ||
| var _options$handlers2, _handler$validate3, _handler$validate4; | ||
| var handlers = (_options$handlers2 = options === null || options === void 0 ? void 0 : options.handlers) !== null && _options$handlers2 !== void 0 ? _options$handlers2 : intentHandlers; | ||
| var handler = handlers[intent.type]; | ||
| if (handler !== null && handler !== void 0 && handler.apply && ((_handler$validate3 = (_handler$validate4 = handler.validate) === null || _handler$validate4 === void 0 ? void 0 : _handler$validate4.call(handler, intent.payload)) !== null && _handler$validate3 !== void 0 ? _handler$validate3 : true)) { | ||
| return handler.apply(result, intent.payload); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| function insertItem(list, item, index) { | ||
@@ -95,8 +111,8 @@ list.splice(index, 0, item); | ||
| */ | ||
| var actionHandlers = { | ||
| var intentHandlers = { | ||
| reset: { | ||
| validatePayload(options) { | ||
| validate(options) { | ||
| return util.isOptional(options, future.isPlainObject) && (util.isUndefined(options === null || options === void 0 ? void 0 : options.defaultValue) || util.isNullable(options === null || options === void 0 ? void 0 : options.defaultValue, future.isPlainObject)); | ||
| }, | ||
| onApply(_, options) { | ||
| resolve(_, options) { | ||
| if ((options === null || options === void 0 ? void 0 : options.defaultValue) === null) { | ||
@@ -107,15 +123,13 @@ return {}; | ||
| }, | ||
| onUpdate(_, _ref) { | ||
| var { | ||
| targetValue, | ||
| ctx | ||
| } = _ref; | ||
| return ctx.reset(targetValue); | ||
| apply(result) { | ||
| return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, { | ||
| reset: true | ||
| }); | ||
| } | ||
| }, | ||
| validate: { | ||
| validatePayload(name) { | ||
| validate(name) { | ||
| return util.isOptional(name, util.isString); | ||
| }, | ||
| onUpdate(state, _ref2) { | ||
| update(state, _ref) { | ||
| var _intent$payload; | ||
@@ -126,3 +140,3 @@ var { | ||
| error | ||
| } = _ref2; | ||
| } = _ref; | ||
| var name = (_intent$payload = intent.payload) !== null && _intent$payload !== void 0 ? _intent$payload : ''; | ||
@@ -146,6 +160,6 @@ var basePath = future.getPathSegments(name); | ||
| update: { | ||
| validatePayload(options) { | ||
| validate(options) { | ||
| return future.isPlainObject(options) && util.isOptional(options.name, util.isString) && util.isOptional(options.index, util.isNumber) && !util.isUndefined(options.value); | ||
| }, | ||
| onApply(value, options) { | ||
| resolve(value, options) { | ||
| var _options$value; | ||
@@ -155,3 +169,3 @@ var name = future.appendPathSegment(options.name, options.index); | ||
| }, | ||
| onUpdate(state, _ref3) { | ||
| update(state, _ref2) { | ||
| var { | ||
@@ -161,3 +175,3 @@ type, | ||
| intent | ||
| } = _ref3; | ||
| } = _ref2; | ||
| if (type === 'server') { | ||
@@ -189,12 +203,54 @@ return state; | ||
| insert: { | ||
| validatePayload(options) { | ||
| return future.isPlainObject(options) && util.isString(options.name) && util.isOptional(options.index, util.isNumber); | ||
| validate(options) { | ||
| return future.isPlainObject(options) && util.isString(options.name) && util.isOptional(options.index, util.isNumber) && util.isOptional(options.from, util.isString) && util.isOptional(options.onInvalid, mode => mode === 'revert'); | ||
| }, | ||
| onApply(value, options) { | ||
| resolve(value, options) { | ||
| var _options$index; | ||
| var list = Array.from(util.getArrayAtPath(value, options.name)); | ||
| insertItem(list, options.defaultValue, (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : list.length); | ||
| return util.updateValueAtPath(value, options.name, list); | ||
| var result = value; | ||
| var itemValue = options.defaultValue; | ||
| if (options.from !== undefined) { | ||
| itemValue = future.getValueAtPath(result, options.from); | ||
| result = util.updateValueAtPath(result, options.from, ''); | ||
| } | ||
| var list = Array.from(util.getArrayAtPath(result, options.name)); | ||
| insertItem(list, itemValue, (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : list.length); | ||
| return util.updateValueAtPath(result, options.name, list); | ||
| }, | ||
| onUpdate(state$1, _ref4) { | ||
| apply(result, options) { | ||
| var _result$error; | ||
| // Warn if validation result is not yet available | ||
| if (typeof result.error === 'undefined' && (options.onInvalid || options.from)) { | ||
| // eslint-disable-next-line no-console | ||
| console.warn('intent.insert() with `onInvalid` or `from` requires the validation result to be available synchronously. ' + 'These options are ignored because the error is not yet known.'); | ||
| return result; | ||
| } | ||
| var arrayErrors = (_result$error = result.error) === null || _result$error === void 0 ? void 0 : _result$error.fieldErrors[options.name]; | ||
| if (options.onInvalid === 'revert' && arrayErrors !== null && arrayErrors !== void 0 && arrayErrors.length) { | ||
| return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, { | ||
| targetValue: undefined | ||
| }); | ||
| } | ||
| if (options.from !== undefined) { | ||
| var _options$index2, _result$error2; | ||
| var index = (_options$index2 = options.index) !== null && _options$index2 !== void 0 ? _options$index2 : util.getArrayAtPath(result.submission.payload, options.name).length; | ||
| var insertedItemPath = future.appendPathSegment(options.name, index); | ||
| var insertedItemErrors = (_result$error2 = result.error) === null || _result$error2 === void 0 ? void 0 : _result$error2.fieldErrors[insertedItemPath]; | ||
| if (insertedItemErrors !== null && insertedItemErrors !== void 0 && insertedItemErrors.length) { | ||
| var _result$error$fieldEr, _result$error3, _result$error$formErr, _result$error4, _result$error5; | ||
| var fromErrors = (_result$error$fieldEr = (_result$error3 = result.error) === null || _result$error3 === void 0 ? void 0 : _result$error3.fieldErrors[options.from]) !== null && _result$error$fieldEr !== void 0 ? _result$error$fieldEr : []; | ||
| return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, { | ||
| targetValue: undefined, | ||
| error: { | ||
| formErrors: (_result$error$formErr = (_result$error4 = result.error) === null || _result$error4 === void 0 ? void 0 : _result$error4.formErrors) !== null && _result$error$formErr !== void 0 ? _result$error$formErr : [], | ||
| fieldErrors: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, (_result$error5 = result.error) === null || _result$error5 === void 0 ? void 0 : _result$error5.fieldErrors), {}, { | ||
| [options.from]: [...fromErrors, ...insertedItemErrors], | ||
| [insertedItemPath]: [] | ||
| }) | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| return result; | ||
| }, | ||
| update(state$1, _ref3) { | ||
| var _intent$payload$index; | ||
@@ -204,26 +260,33 @@ var { | ||
| submission, | ||
| intent | ||
| } = _ref4; | ||
| intent, | ||
| ctx | ||
| } = _ref3; | ||
| if (type === 'server') { | ||
| return state$1; | ||
| } | ||
| var currentValue = submission.payload; | ||
| var list = util.getArrayAtPath(currentValue, intent.payload.name); | ||
| var index = (_intent$payload$index = intent.payload.index) !== null && _intent$payload$index !== void 0 ? _intent$payload$index : list.length; | ||
| var from = intent.payload.from; | ||
| var index = (_intent$payload$index = intent.payload.index) !== null && _intent$payload$index !== void 0 ? _intent$payload$index : util.getArrayAtPath(submission.payload, intent.payload.name).length; | ||
| var updateListIndex = util.createPathIndexUpdater(intent.payload.name, currentIndex => index <= currentIndex ? currentIndex + 1 : currentIndex); | ||
| var touchedFields = util.appendUniqueItem(util.compactMap(state$1.touchedFields, updateListIndex), intent.payload.name); | ||
| var keys = state$1.listKeys; | ||
| var touchedFields = state$1.touchedFields; | ||
| var listKeys = state$1.listKeys; | ||
| if (!ctx.cancelled) { | ||
| touchedFields = util.compactMap(state$1.touchedFields, updateListIndex); | ||
| // Update the keys only for client updates to avoid double updates if there is no client validation | ||
| if (type === 'client') { | ||
| var _state$listKeys$inten; | ||
| var listKeys = Array.from((_state$listKeys$inten = state$1.listKeys[intent.payload.name]) !== null && _state$listKeys$inten !== void 0 ? _state$listKeys$inten : state.getDefaultListKey(state$1.resetKey, currentValue, intent.payload.name)); | ||
| insertItem(listKeys, util.generateUniqueKey(), index); | ||
| keys = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, updateListKeys(state$1.listKeys, future.appendPathSegment(intent.payload.name, index), updateListIndex)), {}, { | ||
| // Update existing list keys | ||
| [intent.payload.name]: listKeys | ||
| }); | ||
| // Update the keys only for client updates to avoid double updates if there is no client validation | ||
| if (type === 'client') { | ||
| var _state$listKeys$inten; | ||
| var selectedListKeys = Array.from((_state$listKeys$inten = state$1.listKeys[intent.payload.name]) !== null && _state$listKeys$inten !== void 0 ? _state$listKeys$inten : state.getDefaultListKey(state$1.resetKey, submission.payload, intent.payload.name)); | ||
| insertItem(selectedListKeys, util.generateUniqueKey(), index); | ||
| listKeys = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, updateListKeys(state$1.listKeys, future.appendPathSegment(intent.payload.name, index), updateListIndex)), {}, { | ||
| // Update existing list keys | ||
| [intent.payload.name]: selectedListKeys | ||
| }); | ||
| } | ||
| } | ||
| touchedFields = util.appendUniqueItem(touchedFields, intent.payload.name); | ||
| if (from !== undefined) { | ||
| touchedFields = util.appendUniqueItem(touchedFields, from); | ||
| } | ||
| return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, state$1), {}, { | ||
| listKeys: keys, | ||
| listKeys, | ||
| touchedFields | ||
@@ -234,6 +297,6 @@ }); | ||
| remove: { | ||
| validatePayload(options) { | ||
| return future.isPlainObject(options) && util.isString(options.name) && util.isNumber(options.index); | ||
| validate(options) { | ||
| return future.isPlainObject(options) && util.isString(options.name) && util.isNumber(options.index) && util.isOptional(options.onInvalid, v => v === 'revert' || v === 'insert'); | ||
| }, | ||
| onApply(value, options) { | ||
| resolve(value, options) { | ||
| var list = Array.from(util.getArrayAtPath(value, options.name)); | ||
@@ -243,8 +306,37 @@ removeItem(list, options.index); | ||
| }, | ||
| onUpdate(state$1, _ref5) { | ||
| apply(result, options) { | ||
| var _result$error6; | ||
| // Warn if validation result is not yet available | ||
| if (typeof result.error === 'undefined' && options.onInvalid) { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| // eslint-disable-next-line no-console | ||
| console.warn('intent.remove() with `onInvalid` requires the validation result to be available synchronously. ' + 'This option is ignored because the error is not yet known.'); | ||
| } | ||
| return result; | ||
| } | ||
| if (result.targetValue && (_result$error6 = result.error) !== null && _result$error6 !== void 0 && _result$error6.fieldErrors[options.name]) { | ||
| switch (options.onInvalid) { | ||
| case 'revert': | ||
| return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, { | ||
| targetValue: undefined | ||
| }); | ||
| case 'insert': | ||
| { | ||
| var list = Array.from(util.getArrayAtPath(result.targetValue, options.name)); | ||
| insertItem(list, options.defaultValue, list.length); | ||
| return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, { | ||
| targetValue: util.updateValueAtPath(result.targetValue, options.name, list) | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| }, | ||
| update(state$1, _ref4) { | ||
| var { | ||
| type, | ||
| submission, | ||
| intent | ||
| } = _ref5; | ||
| intent, | ||
| ctx | ||
| } = _ref4; | ||
| if (type === 'server') { | ||
@@ -260,17 +352,31 @@ return state$1; | ||
| }); | ||
| var touchedFields = util.appendUniqueItem(util.compactMap(state$1.touchedFields, updateListIndex), intent.payload.name); | ||
| var keys = state$1.listKeys; | ||
| var touchedFields = state$1.touchedFields; | ||
| var listKeys = state$1.listKeys; | ||
| // Update the keys only for client updates to avoid double updates if there is no client validation | ||
| if (type === 'client') { | ||
| var _state$listKeys$inten2; | ||
| var listKeys = Array.from((_state$listKeys$inten2 = state$1.listKeys[intent.payload.name]) !== null && _state$listKeys$inten2 !== void 0 ? _state$listKeys$inten2 : state.getDefaultListKey(state$1.resetKey, currentValue, intent.payload.name)); | ||
| removeItem(listKeys, intent.payload.index); | ||
| keys = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, updateListKeys(state$1.listKeys, future.appendPathSegment(intent.payload.name, intent.payload.index), updateListIndex)), {}, { | ||
| // Update existing list keys | ||
| [intent.payload.name]: listKeys | ||
| }); | ||
| // If onInvalid is 'insert', we still remove the item and then insert a new item at the end | ||
| if (!ctx.cancelled || intent.payload.onInvalid === 'insert') { | ||
| touchedFields = util.compactMap(touchedFields, updateListIndex); | ||
| // Update the keys only for client updates to avoid double updates if there is no client validation | ||
| if (type === 'client') { | ||
| var _state$listKeys$inten2; | ||
| var selectedListKeys = Array.from((_state$listKeys$inten2 = state$1.listKeys[intent.payload.name]) !== null && _state$listKeys$inten2 !== void 0 ? _state$listKeys$inten2 : state.getDefaultListKey(state$1.resetKey, currentValue, intent.payload.name)); | ||
| removeItem(selectedListKeys, intent.payload.index); | ||
| listKeys = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, updateListKeys(state$1.listKeys, future.appendPathSegment(intent.payload.name, intent.payload.index), updateListIndex)), {}, { | ||
| // Update existing list keys | ||
| [intent.payload.name]: selectedListKeys | ||
| }); | ||
| if (ctx.cancelled) { | ||
| var index = selectedListKeys.length; | ||
| insertItem(selectedListKeys, util.generateUniqueKey(), index); | ||
| listKeys = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, updateListKeys(state$1.listKeys, future.appendPathSegment(intent.payload.name, index), updateListIndex)), {}, { | ||
| // Update existing list keys | ||
| [intent.payload.name]: selectedListKeys | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| touchedFields = util.appendUniqueItem(touchedFields, intent.payload.name); | ||
| return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, state$1), {}, { | ||
| listKeys: keys, | ||
| listKeys: listKeys, | ||
| touchedFields | ||
@@ -281,6 +387,6 @@ }); | ||
| reorder: { | ||
| validatePayload(options) { | ||
| validate(options) { | ||
| return future.isPlainObject(options) && util.isString(options.name) && util.isNumber(options.from) && util.isNumber(options.to); | ||
| }, | ||
| onApply(value, options) { | ||
| resolve(value, options) { | ||
| var list = Array.from(util.getArrayAtPath(value, options.name)); | ||
@@ -290,3 +396,3 @@ reorderItems(list, options.from, options.to); | ||
| }, | ||
| onUpdate(state$1, _ref6) { | ||
| update(state$1, _ref5) { | ||
| var { | ||
@@ -296,3 +402,3 @@ type, | ||
| intent | ||
| } = _ref6; | ||
| } = _ref5; | ||
| if (type === 'server') { | ||
@@ -335,9 +441,10 @@ return state$1; | ||
| exports.actionHandlers = actionHandlers; | ||
| exports.applyIntent = applyIntent; | ||
| exports.deserializeIntent = deserializeIntent; | ||
| exports.insertItem = insertItem; | ||
| exports.intentHandlers = intentHandlers; | ||
| exports.removeItem = removeItem; | ||
| exports.reorderItems = reorderItems; | ||
| exports.resolveIntent = resolveIntent; | ||
| exports.serializeIntent = serializeIntent; | ||
| exports.updateListKeys = updateListKeys; |
+175
-69
| import { objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs'; | ||
| import { getPathSegments, getRelativePath, isPlainObject, appendPathSegment } from '@conform-to/dom/future'; | ||
| import { getPathSegments, getRelativePath, isPlainObject, appendPathSegment, getValueAtPath } from '@conform-to/dom/future'; | ||
| import { isOptional, isUndefined, isNullable, appendUniqueItem, merge, updateValueAtPath, isString, getArrayAtPath, createPathIndexUpdater, compactMap, generateUniqueKey, isNumber, transformKeys } from './util.mjs'; | ||
@@ -45,4 +45,4 @@ import { getDefaultListKey } from './state.mjs'; | ||
| */ | ||
| function applyIntent(submission, options) { | ||
| var _options$handlers, _handler$validatePayl, _handler$validatePayl2; | ||
| function resolveIntent(submission, options) { | ||
| var _options$handlers, _handler$validate, _handler$validate2; | ||
| if (!submission.intent) { | ||
@@ -52,9 +52,25 @@ return submission.payload; | ||
| var intent = deserializeIntent(submission.intent); | ||
| var handlers = (_options$handlers = options === null || options === void 0 ? void 0 : options.handlers) !== null && _options$handlers !== void 0 ? _options$handlers : actionHandlers; | ||
| var handlers = (_options$handlers = options === null || options === void 0 ? void 0 : options.handlers) !== null && _options$handlers !== void 0 ? _options$handlers : intentHandlers; | ||
| var handler = handlers[intent.type]; | ||
| if (handler && handler.onApply && ((_handler$validatePayl = (_handler$validatePayl2 = handler.validatePayload) === null || _handler$validatePayl2 === void 0 ? void 0 : _handler$validatePayl2.call(handler, intent.payload)) !== null && _handler$validatePayl !== void 0 ? _handler$validatePayl : true)) { | ||
| return handler.onApply(submission.payload, intent.payload); | ||
| if (handler !== null && handler !== void 0 && handler.resolve && ((_handler$validate = (_handler$validate2 = handler.validate) === null || _handler$validate2 === void 0 ? void 0 : _handler$validate2.call(handler, intent.payload)) !== null && _handler$validate !== void 0 ? _handler$validate : true)) { | ||
| return handler.resolve(submission.payload, intent.payload); | ||
| } | ||
| return submission.payload; | ||
| } | ||
| /** | ||
| * Resolves an intent after validation by calling the handler's onResolve. | ||
| * Mutates the result with updated value/error and returns whether the intent was cancelled. | ||
| */ | ||
| function applyIntent(result, intent, options) { | ||
| if (intent) { | ||
| var _options$handlers2, _handler$validate3, _handler$validate4; | ||
| var handlers = (_options$handlers2 = options === null || options === void 0 ? void 0 : options.handlers) !== null && _options$handlers2 !== void 0 ? _options$handlers2 : intentHandlers; | ||
| var handler = handlers[intent.type]; | ||
| if (handler !== null && handler !== void 0 && handler.apply && ((_handler$validate3 = (_handler$validate4 = handler.validate) === null || _handler$validate4 === void 0 ? void 0 : _handler$validate4.call(handler, intent.payload)) !== null && _handler$validate3 !== void 0 ? _handler$validate3 : true)) { | ||
| return handler.apply(result, intent.payload); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| function insertItem(list, item, index) { | ||
@@ -91,8 +107,8 @@ list.splice(index, 0, item); | ||
| */ | ||
| var actionHandlers = { | ||
| var intentHandlers = { | ||
| reset: { | ||
| validatePayload(options) { | ||
| validate(options) { | ||
| return isOptional(options, isPlainObject) && (isUndefined(options === null || options === void 0 ? void 0 : options.defaultValue) || isNullable(options === null || options === void 0 ? void 0 : options.defaultValue, isPlainObject)); | ||
| }, | ||
| onApply(_, options) { | ||
| resolve(_, options) { | ||
| if ((options === null || options === void 0 ? void 0 : options.defaultValue) === null) { | ||
@@ -103,15 +119,13 @@ return {}; | ||
| }, | ||
| onUpdate(_, _ref) { | ||
| var { | ||
| targetValue, | ||
| ctx | ||
| } = _ref; | ||
| return ctx.reset(targetValue); | ||
| apply(result) { | ||
| return _objectSpread2(_objectSpread2({}, result), {}, { | ||
| reset: true | ||
| }); | ||
| } | ||
| }, | ||
| validate: { | ||
| validatePayload(name) { | ||
| validate(name) { | ||
| return isOptional(name, isString); | ||
| }, | ||
| onUpdate(state, _ref2) { | ||
| update(state, _ref) { | ||
| var _intent$payload; | ||
@@ -122,3 +136,3 @@ var { | ||
| error | ||
| } = _ref2; | ||
| } = _ref; | ||
| var name = (_intent$payload = intent.payload) !== null && _intent$payload !== void 0 ? _intent$payload : ''; | ||
@@ -142,6 +156,6 @@ var basePath = getPathSegments(name); | ||
| update: { | ||
| validatePayload(options) { | ||
| validate(options) { | ||
| return isPlainObject(options) && isOptional(options.name, isString) && isOptional(options.index, isNumber) && !isUndefined(options.value); | ||
| }, | ||
| onApply(value, options) { | ||
| resolve(value, options) { | ||
| var _options$value; | ||
@@ -151,3 +165,3 @@ var name = appendPathSegment(options.name, options.index); | ||
| }, | ||
| onUpdate(state, _ref3) { | ||
| update(state, _ref2) { | ||
| var { | ||
@@ -157,3 +171,3 @@ type, | ||
| intent | ||
| } = _ref3; | ||
| } = _ref2; | ||
| if (type === 'server') { | ||
@@ -185,12 +199,54 @@ return state; | ||
| insert: { | ||
| validatePayload(options) { | ||
| return isPlainObject(options) && isString(options.name) && isOptional(options.index, isNumber); | ||
| validate(options) { | ||
| return isPlainObject(options) && isString(options.name) && isOptional(options.index, isNumber) && isOptional(options.from, isString) && isOptional(options.onInvalid, mode => mode === 'revert'); | ||
| }, | ||
| onApply(value, options) { | ||
| resolve(value, options) { | ||
| var _options$index; | ||
| var list = Array.from(getArrayAtPath(value, options.name)); | ||
| insertItem(list, options.defaultValue, (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : list.length); | ||
| return updateValueAtPath(value, options.name, list); | ||
| var result = value; | ||
| var itemValue = options.defaultValue; | ||
| if (options.from !== undefined) { | ||
| itemValue = getValueAtPath(result, options.from); | ||
| result = updateValueAtPath(result, options.from, ''); | ||
| } | ||
| var list = Array.from(getArrayAtPath(result, options.name)); | ||
| insertItem(list, itemValue, (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : list.length); | ||
| return updateValueAtPath(result, options.name, list); | ||
| }, | ||
| onUpdate(state, _ref4) { | ||
| apply(result, options) { | ||
| var _result$error; | ||
| // Warn if validation result is not yet available | ||
| if (typeof result.error === 'undefined' && (options.onInvalid || options.from)) { | ||
| // eslint-disable-next-line no-console | ||
| console.warn('intent.insert() with `onInvalid` or `from` requires the validation result to be available synchronously. ' + 'These options are ignored because the error is not yet known.'); | ||
| return result; | ||
| } | ||
| var arrayErrors = (_result$error = result.error) === null || _result$error === void 0 ? void 0 : _result$error.fieldErrors[options.name]; | ||
| if (options.onInvalid === 'revert' && arrayErrors !== null && arrayErrors !== void 0 && arrayErrors.length) { | ||
| return _objectSpread2(_objectSpread2({}, result), {}, { | ||
| targetValue: undefined | ||
| }); | ||
| } | ||
| if (options.from !== undefined) { | ||
| var _options$index2, _result$error2; | ||
| var index = (_options$index2 = options.index) !== null && _options$index2 !== void 0 ? _options$index2 : getArrayAtPath(result.submission.payload, options.name).length; | ||
| var insertedItemPath = appendPathSegment(options.name, index); | ||
| var insertedItemErrors = (_result$error2 = result.error) === null || _result$error2 === void 0 ? void 0 : _result$error2.fieldErrors[insertedItemPath]; | ||
| if (insertedItemErrors !== null && insertedItemErrors !== void 0 && insertedItemErrors.length) { | ||
| var _result$error$fieldEr, _result$error3, _result$error$formErr, _result$error4, _result$error5; | ||
| var fromErrors = (_result$error$fieldEr = (_result$error3 = result.error) === null || _result$error3 === void 0 ? void 0 : _result$error3.fieldErrors[options.from]) !== null && _result$error$fieldEr !== void 0 ? _result$error$fieldEr : []; | ||
| return _objectSpread2(_objectSpread2({}, result), {}, { | ||
| targetValue: undefined, | ||
| error: { | ||
| formErrors: (_result$error$formErr = (_result$error4 = result.error) === null || _result$error4 === void 0 ? void 0 : _result$error4.formErrors) !== null && _result$error$formErr !== void 0 ? _result$error$formErr : [], | ||
| fieldErrors: _objectSpread2(_objectSpread2({}, (_result$error5 = result.error) === null || _result$error5 === void 0 ? void 0 : _result$error5.fieldErrors), {}, { | ||
| [options.from]: [...fromErrors, ...insertedItemErrors], | ||
| [insertedItemPath]: [] | ||
| }) | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| return result; | ||
| }, | ||
| update(state, _ref3) { | ||
| var _intent$payload$index; | ||
@@ -200,26 +256,33 @@ var { | ||
| submission, | ||
| intent | ||
| } = _ref4; | ||
| intent, | ||
| ctx | ||
| } = _ref3; | ||
| if (type === 'server') { | ||
| return state; | ||
| } | ||
| var currentValue = submission.payload; | ||
| var list = getArrayAtPath(currentValue, intent.payload.name); | ||
| var index = (_intent$payload$index = intent.payload.index) !== null && _intent$payload$index !== void 0 ? _intent$payload$index : list.length; | ||
| var from = intent.payload.from; | ||
| var index = (_intent$payload$index = intent.payload.index) !== null && _intent$payload$index !== void 0 ? _intent$payload$index : getArrayAtPath(submission.payload, intent.payload.name).length; | ||
| var updateListIndex = createPathIndexUpdater(intent.payload.name, currentIndex => index <= currentIndex ? currentIndex + 1 : currentIndex); | ||
| var touchedFields = appendUniqueItem(compactMap(state.touchedFields, updateListIndex), intent.payload.name); | ||
| var keys = state.listKeys; | ||
| var touchedFields = state.touchedFields; | ||
| var listKeys = state.listKeys; | ||
| if (!ctx.cancelled) { | ||
| touchedFields = compactMap(state.touchedFields, updateListIndex); | ||
| // Update the keys only for client updates to avoid double updates if there is no client validation | ||
| if (type === 'client') { | ||
| var _state$listKeys$inten; | ||
| var listKeys = Array.from((_state$listKeys$inten = state.listKeys[intent.payload.name]) !== null && _state$listKeys$inten !== void 0 ? _state$listKeys$inten : getDefaultListKey(state.resetKey, currentValue, intent.payload.name)); | ||
| insertItem(listKeys, generateUniqueKey(), index); | ||
| keys = _objectSpread2(_objectSpread2({}, updateListKeys(state.listKeys, appendPathSegment(intent.payload.name, index), updateListIndex)), {}, { | ||
| // Update existing list keys | ||
| [intent.payload.name]: listKeys | ||
| }); | ||
| // Update the keys only for client updates to avoid double updates if there is no client validation | ||
| if (type === 'client') { | ||
| var _state$listKeys$inten; | ||
| var selectedListKeys = Array.from((_state$listKeys$inten = state.listKeys[intent.payload.name]) !== null && _state$listKeys$inten !== void 0 ? _state$listKeys$inten : getDefaultListKey(state.resetKey, submission.payload, intent.payload.name)); | ||
| insertItem(selectedListKeys, generateUniqueKey(), index); | ||
| listKeys = _objectSpread2(_objectSpread2({}, updateListKeys(state.listKeys, appendPathSegment(intent.payload.name, index), updateListIndex)), {}, { | ||
| // Update existing list keys | ||
| [intent.payload.name]: selectedListKeys | ||
| }); | ||
| } | ||
| } | ||
| touchedFields = appendUniqueItem(touchedFields, intent.payload.name); | ||
| if (from !== undefined) { | ||
| touchedFields = appendUniqueItem(touchedFields, from); | ||
| } | ||
| return _objectSpread2(_objectSpread2({}, state), {}, { | ||
| listKeys: keys, | ||
| listKeys, | ||
| touchedFields | ||
@@ -230,6 +293,6 @@ }); | ||
| remove: { | ||
| validatePayload(options) { | ||
| return isPlainObject(options) && isString(options.name) && isNumber(options.index); | ||
| validate(options) { | ||
| return isPlainObject(options) && isString(options.name) && isNumber(options.index) && isOptional(options.onInvalid, v => v === 'revert' || v === 'insert'); | ||
| }, | ||
| onApply(value, options) { | ||
| resolve(value, options) { | ||
| var list = Array.from(getArrayAtPath(value, options.name)); | ||
@@ -239,8 +302,37 @@ removeItem(list, options.index); | ||
| }, | ||
| onUpdate(state, _ref5) { | ||
| apply(result, options) { | ||
| var _result$error6; | ||
| // Warn if validation result is not yet available | ||
| if (typeof result.error === 'undefined' && options.onInvalid) { | ||
| if (process.env.NODE_ENV !== 'production') { | ||
| // eslint-disable-next-line no-console | ||
| console.warn('intent.remove() with `onInvalid` requires the validation result to be available synchronously. ' + 'This option is ignored because the error is not yet known.'); | ||
| } | ||
| return result; | ||
| } | ||
| if (result.targetValue && (_result$error6 = result.error) !== null && _result$error6 !== void 0 && _result$error6.fieldErrors[options.name]) { | ||
| switch (options.onInvalid) { | ||
| case 'revert': | ||
| return _objectSpread2(_objectSpread2({}, result), {}, { | ||
| targetValue: undefined | ||
| }); | ||
| case 'insert': | ||
| { | ||
| var list = Array.from(getArrayAtPath(result.targetValue, options.name)); | ||
| insertItem(list, options.defaultValue, list.length); | ||
| return _objectSpread2(_objectSpread2({}, result), {}, { | ||
| targetValue: updateValueAtPath(result.targetValue, options.name, list) | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| }, | ||
| update(state, _ref4) { | ||
| var { | ||
| type, | ||
| submission, | ||
| intent | ||
| } = _ref5; | ||
| intent, | ||
| ctx | ||
| } = _ref4; | ||
| if (type === 'server') { | ||
@@ -256,17 +348,31 @@ return state; | ||
| }); | ||
| var touchedFields = appendUniqueItem(compactMap(state.touchedFields, updateListIndex), intent.payload.name); | ||
| var keys = state.listKeys; | ||
| var touchedFields = state.touchedFields; | ||
| var listKeys = state.listKeys; | ||
| // Update the keys only for client updates to avoid double updates if there is no client validation | ||
| if (type === 'client') { | ||
| var _state$listKeys$inten2; | ||
| var listKeys = Array.from((_state$listKeys$inten2 = state.listKeys[intent.payload.name]) !== null && _state$listKeys$inten2 !== void 0 ? _state$listKeys$inten2 : getDefaultListKey(state.resetKey, currentValue, intent.payload.name)); | ||
| removeItem(listKeys, intent.payload.index); | ||
| keys = _objectSpread2(_objectSpread2({}, updateListKeys(state.listKeys, appendPathSegment(intent.payload.name, intent.payload.index), updateListIndex)), {}, { | ||
| // Update existing list keys | ||
| [intent.payload.name]: listKeys | ||
| }); | ||
| // If onInvalid is 'insert', we still remove the item and then insert a new item at the end | ||
| if (!ctx.cancelled || intent.payload.onInvalid === 'insert') { | ||
| touchedFields = compactMap(touchedFields, updateListIndex); | ||
| // Update the keys only for client updates to avoid double updates if there is no client validation | ||
| if (type === 'client') { | ||
| var _state$listKeys$inten2; | ||
| var selectedListKeys = Array.from((_state$listKeys$inten2 = state.listKeys[intent.payload.name]) !== null && _state$listKeys$inten2 !== void 0 ? _state$listKeys$inten2 : getDefaultListKey(state.resetKey, currentValue, intent.payload.name)); | ||
| removeItem(selectedListKeys, intent.payload.index); | ||
| listKeys = _objectSpread2(_objectSpread2({}, updateListKeys(state.listKeys, appendPathSegment(intent.payload.name, intent.payload.index), updateListIndex)), {}, { | ||
| // Update existing list keys | ||
| [intent.payload.name]: selectedListKeys | ||
| }); | ||
| if (ctx.cancelled) { | ||
| var index = selectedListKeys.length; | ||
| insertItem(selectedListKeys, generateUniqueKey(), index); | ||
| listKeys = _objectSpread2(_objectSpread2({}, updateListKeys(state.listKeys, appendPathSegment(intent.payload.name, index), updateListIndex)), {}, { | ||
| // Update existing list keys | ||
| [intent.payload.name]: selectedListKeys | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| touchedFields = appendUniqueItem(touchedFields, intent.payload.name); | ||
| return _objectSpread2(_objectSpread2({}, state), {}, { | ||
| listKeys: keys, | ||
| listKeys: listKeys, | ||
| touchedFields | ||
@@ -277,6 +383,6 @@ }); | ||
| reorder: { | ||
| validatePayload(options) { | ||
| validate(options) { | ||
| return isPlainObject(options) && isString(options.name) && isNumber(options.from) && isNumber(options.to); | ||
| }, | ||
| onApply(value, options) { | ||
| resolve(value, options) { | ||
| var list = Array.from(getArrayAtPath(value, options.name)); | ||
@@ -286,3 +392,3 @@ reorderItems(list, options.from, options.to); | ||
| }, | ||
| onUpdate(state, _ref6) { | ||
| update(state, _ref5) { | ||
| var { | ||
@@ -292,3 +398,3 @@ type, | ||
| intent | ||
| } = _ref6; | ||
| } = _ref5; | ||
| if (type === 'server') { | ||
@@ -331,2 +437,2 @@ return state; | ||
| export { actionHandlers, applyIntent, deserializeIntent, insertItem, removeItem, reorderItems, serializeIntent, updateListKeys }; | ||
| export { applyIntent, deserializeIntent, insertItem, intentHandlers, removeItem, reorderItems, resolveIntent, serializeIntent, updateListKeys }; |
| import { type FieldName, type ValidationAttributes, type Serialize } from '@conform-to/dom/future'; | ||
| import type { FieldMetadata, Fieldset, FormContext, FormMetadata, FormState, FormAction, UnknownIntent, ActionHandler, BaseFieldMetadata, BaseFormMetadata, DefineConditionalField } from './types'; | ||
| import type { FieldMetadata, Fieldset, FormContext, FormMetadata, FormState, FormAction, UnknownIntent, IntentHandler, BaseFieldMetadata, BaseFormMetadata, DefineConditionalField } from './types'; | ||
| export declare function initializeState<ErrorShape>(options?: { | ||
@@ -14,3 +14,4 @@ defaultValue?: Record<string, unknown> | null | undefined; | ||
| export declare function updateState<ErrorShape>(state: FormState<ErrorShape>, action: FormAction<ErrorShape, UnknownIntent | null, { | ||
| handlers: Record<string, ActionHandler>; | ||
| handlers: Record<string, IntentHandler>; | ||
| cancelled: boolean; | ||
| reset: (defaultValue?: Record<string, unknown> | null | undefined) => FormState<ErrorShape>; | ||
@@ -17,0 +18,0 @@ }>): FormState<ErrorShape>; |
@@ -60,8 +60,9 @@ 'use strict'; | ||
| var handler = (_action$ctx$handlers = action.ctx.handlers) === null || _action$ctx$handlers === void 0 ? void 0 : _action$ctx$handlers[intent.type]; | ||
| if (typeof (handler === null || handler === void 0 ? void 0 : handler.onUpdate) === 'function') { | ||
| var _handler$validatePayl, _handler$validatePayl2; | ||
| if ((_handler$validatePayl = (_handler$validatePayl2 = handler.validatePayload) === null || _handler$validatePayl2 === void 0 ? void 0 : _handler$validatePayl2.call(handler, intent.payload)) !== null && _handler$validatePayl !== void 0 ? _handler$validatePayl : true) { | ||
| return handler.onUpdate(state, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, action), {}, { | ||
| if (typeof (handler === null || handler === void 0 ? void 0 : handler.update) === 'function') { | ||
| var _handler$validate, _handler$validate2; | ||
| if ((_handler$validate = (_handler$validate2 = handler.validate) === null || _handler$validate2 === void 0 ? void 0 : _handler$validate2.call(handler, intent.payload)) !== null && _handler$validate !== void 0 ? _handler$validate : true) { | ||
| return handler.update(state, _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, action), {}, { | ||
| ctx: { | ||
| reset: action.ctx.reset | ||
| reset: action.ctx.reset, | ||
| cancelled: action.ctx.cancelled | ||
| }, | ||
@@ -332,2 +333,3 @@ intent: { | ||
| multiple: constraint === null || constraint === void 0 ? void 0 : constraint.multiple, | ||
| accept: constraint === null || constraint === void 0 ? void 0 : constraint.accept, | ||
| get defaultValue() { | ||
@@ -334,0 +336,0 @@ return getDefaultValue(context, name, serialize); |
@@ -56,8 +56,9 @@ import { objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs'; | ||
| var handler = (_action$ctx$handlers = action.ctx.handlers) === null || _action$ctx$handlers === void 0 ? void 0 : _action$ctx$handlers[intent.type]; | ||
| if (typeof (handler === null || handler === void 0 ? void 0 : handler.onUpdate) === 'function') { | ||
| var _handler$validatePayl, _handler$validatePayl2; | ||
| if ((_handler$validatePayl = (_handler$validatePayl2 = handler.validatePayload) === null || _handler$validatePayl2 === void 0 ? void 0 : _handler$validatePayl2.call(handler, intent.payload)) !== null && _handler$validatePayl !== void 0 ? _handler$validatePayl : true) { | ||
| return handler.onUpdate(state, _objectSpread2(_objectSpread2({}, action), {}, { | ||
| if (typeof (handler === null || handler === void 0 ? void 0 : handler.update) === 'function') { | ||
| var _handler$validate, _handler$validate2; | ||
| if ((_handler$validate = (_handler$validate2 = handler.validate) === null || _handler$validate2 === void 0 ? void 0 : _handler$validate2.call(handler, intent.payload)) !== null && _handler$validate !== void 0 ? _handler$validate : true) { | ||
| return handler.update(state, _objectSpread2(_objectSpread2({}, action), {}, { | ||
| ctx: { | ||
| reset: action.ctx.reset | ||
| reset: action.ctx.reset, | ||
| cancelled: action.ctx.cancelled | ||
| }, | ||
@@ -328,2 +329,3 @@ intent: { | ||
| multiple: constraint === null || constraint === void 0 ? void 0 : constraint.multiple, | ||
| accept: constraint === null || constraint === void 0 ? void 0 : constraint.accept, | ||
| get defaultValue() { | ||
@@ -330,0 +332,0 @@ return getDefaultValue(context, name, serialize$1); |
@@ -425,3 +425,3 @@ import type { FieldName, FormError, FormValue, Serialize, SubmissionResult, ValidationAttributes } from '@conform-to/dom/future'; | ||
| */ | ||
| insert<FieldShape extends Array<any>>(options: { | ||
| insert<FieldShape extends Array<any> | null | undefined>(options: { | ||
| /** | ||
@@ -439,3 +439,17 @@ * The name of the array field to insert into. | ||
| */ | ||
| defaultValue?: FieldShape extends Array<infer ItemShape> ? DefaultValue<ItemShape> : never; | ||
| defaultValue?: NonNullable<FieldShape> extends Array<infer ItemShape> ? DefaultValue<ItemShape> : never; | ||
| /** | ||
| * The name of a field to read the value from. | ||
| * When specified, the value is read from this field, validated, | ||
| * and if valid, inserted into the array and the source field is cleared. | ||
| * If validation fails, the error is shown on the source field instead. | ||
| * Requires the validation error to be available synchronously. | ||
| */ | ||
| from?: string; | ||
| /** | ||
| * What to do when the insert causes a validation error on the array. | ||
| * - 'revert': Don't insert, keep original array state. | ||
| * Requires the validation error to be available synchronously. | ||
| */ | ||
| onInvalid?: 'revert'; | ||
| }): void; | ||
@@ -445,7 +459,7 @@ /** | ||
| */ | ||
| remove(options: { | ||
| remove<FieldShape extends Array<any> | null | undefined>(options: { | ||
| /** | ||
| * The name of the array field to remove from. | ||
| */ | ||
| name: FieldName<Array<any>>; | ||
| name: FieldName<FieldShape>; | ||
| /** | ||
@@ -455,2 +469,13 @@ * The index of the item to remove. | ||
| index: number; | ||
| /** | ||
| * What to do when the remove causes a validation error on the array. | ||
| * - 'revert': Don't remove, keep original item as-is. | ||
| * - 'insert': Remove the item but insert a new blank item at the end. | ||
| * Requires the validation error to be available synchronously. | ||
| */ | ||
| onInvalid?: 'revert' | 'insert'; | ||
| /** | ||
| * The default value for the new item when onInvalid is 'insert'. | ||
| */ | ||
| defaultValue?: NonNullable<FieldShape> extends Array<infer ItemShape> ? DefaultValue<ItemShape> : never; | ||
| }): void; | ||
@@ -472,6 +497,7 @@ /** | ||
| }[keyof Dispatcher]; | ||
| export type ActionHandler<Signature extends (payload: any) => void = (payload: any) => void> = { | ||
| validatePayload?(...args: UnknownArgs<Parameters<Signature>>): boolean; | ||
| onApply?(value: Record<string, FormValue>, ...args: Parameters<Signature>): Record<string, FormValue> | undefined; | ||
| onUpdate?<ErrorShape extends BaseErrorShape>(state: FormState<ErrorShape>, action: FormAction<ErrorShape, { | ||
| export type IntentHandler<Signature extends (payload: any) => void = (payload: any) => void> = { | ||
| validate?(...args: UnknownArgs<Parameters<Signature>>): boolean; | ||
| resolve?(value: Record<string, FormValue>, ...args: Parameters<Signature>): Record<string, FormValue> | undefined; | ||
| apply?<ErrorShape extends BaseErrorShape>(result: SubmissionResult<ErrorShape>, ...args: Parameters<Signature>): SubmissionResult<ErrorShape>; | ||
| update?<ErrorShape extends BaseErrorShape>(state: FormState<ErrorShape>, action: FormAction<ErrorShape, { | ||
| type: string; | ||
@@ -481,2 +507,3 @@ payload: Signature extends (payload: infer Payload) => void ? Payload : undefined; | ||
| reset: (defaultValue?: Record<string, unknown> | null) => FormState<ErrorShape>; | ||
| cancelled?: boolean; | ||
| }>): FormState<ErrorShape>; | ||
@@ -483,0 +510,0 @@ }; |
@@ -37,2 +37,3 @@ import type { FormMetadata, FieldMetadata, Metadata, Pretty } from './context'; | ||
| multiple?: boolean; | ||
| accept?: string; | ||
| value?: string; | ||
@@ -39,0 +40,0 @@ defaultChecked?: boolean; |
+2
-1
@@ -112,3 +112,4 @@ 'use strict'; | ||
| pattern: metadata.pattern, | ||
| multiple: metadata.multiple | ||
| multiple: metadata.multiple, | ||
| accept: metadata.accept | ||
| }); | ||
@@ -115,0 +116,0 @@ if (typeof options.value === 'undefined' || options.value) { |
+2
-1
@@ -108,3 +108,4 @@ import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.mjs'; | ||
| pattern: metadata.pattern, | ||
| multiple: metadata.multiple | ||
| multiple: metadata.multiple, | ||
| accept: metadata.accept | ||
| }); | ||
@@ -111,0 +112,0 @@ if (typeof options.value === 'undefined' || options.value) { |
+2
-2
@@ -6,3 +6,3 @@ { | ||
| "license": "MIT", | ||
| "version": "1.16.0", | ||
| "version": "1.17.0", | ||
| "main": "./dist/index.js", | ||
@@ -45,3 +45,3 @@ "module": "./dist/index.mjs", | ||
| "dependencies": { | ||
| "@conform-to/dom": "1.16.0" | ||
| "@conform-to/dom": "1.17.0" | ||
| }, | ||
@@ -48,0 +48,0 @@ "devDependencies": { |
+1
-1
@@ -10,3 +10,3 @@ ``` | ||
| Version 1.16.0 / License MIT / Copyright (c) 2025 Edmund Hung | ||
| Version 1.17.0 / License MIT / Copyright (c) 2025 Edmund Hung | ||
@@ -13,0 +13,0 @@ Progressively enhance HTML forms with React. Build resilient, type-safe forms with no hassle using web standards. |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
391832
8.19%9151
7.12%36
111.76%+ Added
- Removed
Updated