🚨 Latest Research:Tanstack npm Packages Compromised in Ongoing Mini Shai-Hulud Supply-Chain Attack.Learn More
Socket
Book a DemoSign in
Socket

@conform-to/react

Package Overview
Dependencies
Maintainers
1
Versions
100
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@conform-to/react - npm Package Compare versions

Comparing version
1.16.0
to
1.17.0
+1
-0
dist/context.js

@@ -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':

@@ -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':

@@ -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

@@ -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;

@@ -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;

'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

@@ -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;
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;

@@ -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) {

@@ -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) {

@@ -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": {

@@ -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.