UNPKG

@conform-to/react

Version:

Conform view adapter for react

440 lines (421 loc) 17.6 kB
'use strict'; 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;