@conform-to/react
Version:
Conform view adapter for react
440 lines (421 loc) • 17.6 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var _rollupPluginBabelHelpers = require('../_virtual/_rollupPluginBabelHelpers.js');
var future = require('@conform-to/dom/future');
var util = require('./util.js');
function initializeState(options) {
var _options$resetKey, _options$defaultValue;
return {
resetKey: (_options$resetKey = options === null || options === void 0 ? void 0 : options.resetKey) !== null && _options$resetKey !== void 0 ? _options$resetKey : util.generateUniqueKey(),
listKeys: {},
defaultValue: (_options$defaultValue = options === null || options === void 0 ? void 0 : options.defaultValue) !== null && _options$defaultValue !== void 0 ? _options$defaultValue : {},
targetValue: null,
serverValue: null,
serverError: null,
clientError: null,
touchedFields: []
};
}
/**
* Updates form state based on action type:
* - Client actions: update target value and client errors
* - Server actions: update server errors and clear client errors, with optional target value
* - Initialize: set initial server value
*/
function updateState(state, action) {
var _action$targetValue, _action$targetValue2, _action$intent, _action$ctx$handlers;
if (action.reset) {
return action.ctx.reset(action.targetValue);
}
var value = (_action$targetValue = action.targetValue) !== null && _action$targetValue !== void 0 ? _action$targetValue : action.submission.payload;
// Apply the form error and target value from the result first
state = action.type === 'client' ? util.merge(state, {
targetValue: (_action$targetValue2 = action.targetValue) !== null && _action$targetValue2 !== void 0 ? _action$targetValue2 : state.targetValue,
serverValue: action.targetValue ? null : state.serverValue,
// Update client error only if the error is different from the previous one to minimize unnecessary re-renders
clientError: typeof action.error !== 'undefined' && !future.deepEqual(state.clientError, action.error) ? action.error : state.clientError,
// Reset server error if form value is changed
serverError: typeof action.error !== 'undefined' && !future.deepEqual(state.serverValue, value) ? null : state.serverError
}) : util.merge(state, {
// Clear client error to avoid showing stale errors
clientError: null,
// Update server error if the error is defined.
// There is no need to check if the error is different as we are updating other states as well
serverError: typeof action.error !== 'undefined' ? action.error : state.serverError,
listKeys: action.type === 'server' && action.targetValue ? pruneListKeys(state.listKeys, action.targetValue) : state.listKeys,
targetValue: action.type === 'server' && action.targetValue ? action.targetValue : state.targetValue,
// Keep track of the value that the serverError is based on
serverValue: !future.deepEqual(state.serverValue, value) ? value : state.serverValue
});
// Validate the whole form if no intent is provided (default submission)
var intent = (_action$intent = action.intent) !== null && _action$intent !== void 0 ? _action$intent : {
type: 'validate'
};
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), {}, {
ctx: {
reset: action.ctx.reset
},
intent: {
type: intent.type,
payload: intent.payload
}
}));
}
}
return state;
}
/**
* Removes list keys where array length has changed to force regeneration.
* Minimizes UI state loss by only invalidating keys when necessary.
*/
function pruneListKeys(listKeys, targetValue) {
var result = listKeys;
for (var [name, keys] of Object.entries(listKeys)) {
var list = util.getArrayAtPath(targetValue, name);
// Reset list keys only if the length has changed
// to minimize potential UI state loss due to key changes
if (keys.length !== list.length) {
// Create a shallow copy to avoid mutating the original object
if (result === listKeys) {
result = _rollupPluginBabelHelpers.objectSpread2({}, result);
}
// Remove the list key to force regeneration
delete result[name];
}
}
return result;
}
function getDefaultValue(context, name) {
var _ref, _context$state$server;
var serialize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : future.serialize;
var value = future.getValueAtPath((_ref = (_context$state$server = context.state.serverValue) !== null && _context$state$server !== void 0 ? _context$state$server : context.state.targetValue) !== null && _ref !== void 0 ? _ref : context.state.defaultValue, name);
var serializedValue = serialize(value);
if (typeof serializedValue === 'string') {
return serializedValue;
}
return '';
}
function getDefaultOptions(context, name) {
var _ref2, _context$state$server2;
var serialize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : future.serialize;
var value = future.getValueAtPath((_ref2 = (_context$state$server2 = context.state.serverValue) !== null && _context$state$server2 !== void 0 ? _context$state$server2 : context.state.targetValue) !== null && _ref2 !== void 0 ? _ref2 : context.state.defaultValue, name);
var serializedValue = serialize(value);
if (Array.isArray(serializedValue) && serializedValue.every(item => typeof item === 'string')) {
return serializedValue;
}
if (typeof serializedValue === 'string') {
return [serializedValue];
}
return [];
}
function isDefaultChecked(context, name) {
var _ref3, _context$state$server3;
var serialize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : future.serialize;
var value = future.getValueAtPath((_ref3 = (_context$state$server3 = context.state.serverValue) !== null && _context$state$server3 !== void 0 ? _context$state$server3 : context.state.targetValue) !== null && _ref3 !== void 0 ? _ref3 : context.state.defaultValue, name);
var serializedValue = serialize(value);
if (typeof serializedValue === 'string') {
return serializedValue === 'on';
}
return false;
}
/**
* Determine if the field is touched
*
* This checks if the field is in the list of touched fields,
* or if there is any child field that is touched, i.e. form / fieldset
*/
function isTouched(state) {
var name = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
if (state.touchedFields.includes(name)) {
return true;
}
var paths = future.getPathSegments(name);
return state.touchedFields.some(field => field !== name && future.getRelativePath(field, paths) !== null);
}
function getDefaultListKey(prefix, initialValue, name) {
return util.getArrayAtPath(initialValue, name).map((_, index) => "".concat(prefix, "-").concat(future.appendPathSegment(name, index)));
}
function getListKey(context, name) {
var _context$state$listKe, _context$state$listKe2, _ref4, _context$state$server4;
return (_context$state$listKe = (_context$state$listKe2 = context.state.listKeys) === null || _context$state$listKe2 === void 0 ? void 0 : _context$state$listKe2[name]) !== null && _context$state$listKe !== void 0 ? _context$state$listKe : getDefaultListKey(context.state.resetKey, (_ref4 = (_context$state$server4 = context.state.serverValue) !== null && _context$state$server4 !== void 0 ? _context$state$server4 : context.state.targetValue) !== null && _ref4 !== void 0 ? _ref4 : context.state.defaultValue, name);
}
function getErrors(state, name) {
var _state$serverError;
var error = (_state$serverError = state.serverError) !== null && _state$serverError !== void 0 ? _state$serverError : state.clientError;
if (!error || !isTouched(state, name)) {
return;
}
var errors = name ? error.fieldErrors[name] : error.formErrors;
if (errors && errors.length > 0) {
return errors;
}
}
function getFieldErrors(state, name) {
var _state$serverError2;
var result = {};
var error = (_state$serverError2 = state.serverError) !== null && _state$serverError2 !== void 0 ? _state$serverError2 : state.clientError;
if (error) {
var basePath = future.getPathSegments(name);
for (var field of Object.keys(error.fieldErrors)) {
var relativePath = future.getRelativePath(field, basePath);
// Only include errors for specified field's children
if (!relativePath || relativePath.length === 0) {
continue;
}
var _error = getErrors(state, field);
if (typeof _error !== 'undefined') {
result[future.formatPathSegments(relativePath)] = _error;
}
}
}
return result;
}
function isValid(state, name) {
var _state$serverError3;
var error = (_state$serverError3 = state.serverError) !== null && _state$serverError3 !== void 0 ? _state$serverError3 : state.clientError;
// If there is no error, it must be valid
if (!error) {
return true;
}
var basePath = future.getPathSegments(name);
for (var field of Object.keys(error.fieldErrors)) {
// When checking a specific field, only check that field and its children
if (name && !future.getRelativePath(field, basePath)) {
continue;
}
// If the field is not touched, we don't consider its error
var _error2 = getErrors(state, field);
if (_error2) {
return false;
}
}
// Make sure there is no form error when checking the whole form
if (!name) {
return !getErrors(state);
}
return true;
}
/**
* Gets validation constraint for a field, with fallback to parent array patterns.
* e.g. "array[0].key" falls back to "array[].key" if specific constraint not found.
*/
function getConstraint(context, name) {
var _context$constraint;
var constraint = (_context$constraint = context.constraint) === null || _context$constraint === void 0 ? void 0 : _context$constraint[name];
if (!constraint) {
var path = future.getPathSegments(name);
for (var i = path.length - 1; i >= 0; i--) {
var segment = path[i];
// Try searching a less specific path for the constraint
// e.g. `array[0].anotherArray[1].key` -> `array[0].anotherArray[].key` -> `array[].anotherArray[].key`
if (typeof segment === 'number') {
// This overrides the current number segment with an empty string
// which will be treated as an empty bracket
path[i] = '';
break;
}
}
var alternative = future.formatPathSegments(path);
if (name !== alternative) {
constraint = getConstraint(context, alternative);
}
}
return constraint;
}
function getFormMetadata(context, options) {
return {
key: context.state.resetKey,
id: context.formId,
errorId: "".concat(context.formId, "-form-error"),
descriptionId: "".concat(context.formId, "-form-description"),
defaultValue: context.state.defaultValue,
get errors() {
return getErrors(context.state);
},
get fieldErrors() {
return getFieldErrors(context.state);
},
get touched() {
return isTouched(context.state);
},
get valid() {
return isValid(context.state);
},
get invalid() {
return !this.valid;
},
props: {
id: context.formId,
onSubmit: context.handleSubmit,
onInput: context.handleInput,
onBlur: context.handleBlur,
noValidate: true
},
context,
getField(name) {
return getField(context, {
name,
serialize: options === null || options === void 0 ? void 0 : options.serialize,
customize: options === null || options === void 0 ? void 0 : options.customize
});
},
getFieldset(name) {
return getFieldset(context, {
name,
serialize: options === null || options === void 0 ? void 0 : options.serialize,
customize: options === null || options === void 0 ? void 0 : options.customize
});
},
getFieldList(name) {
return getFieldList(context, {
name,
serialize: options === null || options === void 0 ? void 0 : options.serialize,
customize: options === null || options === void 0 ? void 0 : options.customize
});
}
};
}
function getField(context, options) {
var {
key,
name,
serialize = future.serialize,
customize
} = options;
var id = "".concat(context.formId, "-field-").concat(name.replace(/[^a-zA-Z0-9._-]/g, '_'));
var constraint = getConstraint(context, name);
var metadata = {
key,
name,
id,
descriptionId: "".concat(id, "-description"),
errorId: "".concat(id, "-error"),
formId: context.formId,
required: constraint === null || constraint === void 0 ? void 0 : constraint.required,
minLength: constraint === null || constraint === void 0 ? void 0 : constraint.minLength,
maxLength: constraint === null || constraint === void 0 ? void 0 : constraint.maxLength,
pattern: constraint === null || constraint === void 0 ? void 0 : constraint.pattern,
min: constraint === null || constraint === void 0 ? void 0 : constraint.min,
max: constraint === null || constraint === void 0 ? void 0 : constraint.max,
step: constraint === null || constraint === void 0 ? void 0 : constraint.step,
multiple: constraint === null || constraint === void 0 ? void 0 : constraint.multiple,
get defaultValue() {
return getDefaultValue(context, name, serialize);
},
get defaultOptions() {
return getDefaultOptions(context, name, serialize);
},
get defaultChecked() {
return isDefaultChecked(context, name, serialize);
},
get touched() {
return isTouched(context.state, name);
},
get valid() {
return isValid(context.state, name);
},
get invalid() {
return !this.valid;
},
get errors() {
return getErrors(context.state, name);
},
get fieldErrors() {
return getFieldErrors(context.state, name);
},
get ariaInvalid() {
return !this.valid ? true : undefined;
},
get ariaDescribedBy() {
return !this.valid ? this.errorId : undefined;
},
getFieldset() {
return getFieldset(context, {
name: name,
serialize,
customize
});
},
getFieldList() {
return getFieldList(context, {
name: name,
serialize,
customize
});
}
};
if (typeof customize !== 'function') {
return metadata;
}
var customMetadata = null;
return new Proxy(metadata, {
get(target, prop, receiver) {
var _customMetadata;
if (Reflect.has(target, prop)) {
return Reflect.get(target, prop, receiver);
}
(_customMetadata = customMetadata) !== null && _customMetadata !== void 0 ? _customMetadata : customMetadata = customize(metadata);
if (Reflect.has(customMetadata, prop)) {
return Reflect.get(customMetadata, prop, receiver);
}
// Allow React DevTools to inspect the object
// without throwing errors for internal properties
if (typeof prop === 'symbol' || prop === '$$typeof') {
return undefined;
}
throw new Error("Property \"".concat(String(prop), "\" does not exist on field metadata. ") + "If you have defined the CustomMetadata interface to include \"".concat(String(prop), "\", make sure to also implement it through the \"defineCustomMetadata\" property on <FormOptionsProvider />."));
}
});
}
/**
* Creates a proxy that dynamically generates field objects when properties are accessed.
*/
function getFieldset(context, options) {
return new Proxy({}, {
get(target, name, receiver) {
if (typeof name === 'string') {
return getField(context, {
name: future.appendPathSegment(options === null || options === void 0 ? void 0 : options.name, name),
serialize: options.serialize,
customize: options.customize
});
}
return Reflect.get(target, name, receiver);
}
});
}
/**
* Creates an array of field objects for list/array inputs
*/
function getFieldList(context, options) {
var keys = getListKey(context, options.name);
return keys.map((key, index) => {
return getField(context, {
name: future.appendPathSegment(options.name, index),
serialize: options.serialize,
customize: options.customize,
key
});
});
}
exports.getConstraint = getConstraint;
exports.getDefaultListKey = getDefaultListKey;
exports.getDefaultOptions = getDefaultOptions;
exports.getDefaultValue = getDefaultValue;
exports.getErrors = getErrors;
exports.getField = getField;
exports.getFieldErrors = getFieldErrors;
exports.getFieldList = getFieldList;
exports.getFieldset = getFieldset;
exports.getFormMetadata = getFormMetadata;
exports.getListKey = getListKey;
exports.initializeState = initializeState;
exports.isDefaultChecked = isDefaultChecked;
exports.isTouched = isTouched;
exports.isValid = isValid;
exports.pruneListKeys = pruneListKeys;
exports.updateState = updateState;