@conform-to/react
Version:
Conform view adapter for react
446 lines (431 loc) • 20.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');
var state = require('./state.js');
/**
* Serializes intent to string format: "type" or "type(payload)".
*/
function serializeIntent(intent) {
if (!intent.payload) {
return intent.type;
}
return "".concat(intent.type, "(").concat(JSON.stringify(intent.payload), ")");
}
/**
* Parses serialized intent string back to intent object.
*/
function deserializeIntent(value) {
var type = value;
var payload;
var serializedPayload;
var openParenIndex = value.indexOf('(');
if (openParenIndex > 0 && value[value.length - 1] === ')') {
type = value.slice(0, openParenIndex);
serializedPayload = value.slice(openParenIndex + 1, -1);
}
if (serializedPayload) {
try {
payload = JSON.parse(serializedPayload);
} catch (_unused) {
// Ignore the error
}
}
return {
type,
payload
};
}
/**
* Applies intent transformation to submission payload.
* Returns modified payload or null for reset intent.
*/
function resolveIntent(submission, options) {
var _options$handlers, _handler$validate, _handler$validate2;
if (!submission.intent) {
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 : intentHandlers;
var handler = handlers[intent.type];
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) {
list.splice(index, 0, item);
}
function removeItem(list, index) {
list.splice(index, 1);
}
function reorderItems(list, fromIndex, toIndex) {
list.splice(toIndex, 0, ...list.splice(fromIndex, 1));
}
/**
* Updates list keys by removing child keys and optionally transforming remaining keys.
*/
function updateListKeys() {
var keys = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var keyToBeRemoved = arguments.length > 1 ? arguments[1] : undefined;
var updateKey = arguments.length > 2 ? arguments[2] : undefined;
var basePath = future.parsePath(keyToBeRemoved);
return util.transformKeys(keys, field => {
var _updateKey;
return future.getRelativePath(field, basePath) !== null ? null : (_updateKey = updateKey === null || updateKey === void 0 ? void 0 : updateKey(field)) !== null && _updateKey !== void 0 ? _updateKey : field;
});
}
/**
* Built-in action handlers for form intents:
* - reset: clears form data
* - validate: marks fields as touched for validation display
* - update: updates specific field values
* - insert/remove/reorder: manages array field operations
*/
var intentHandlers = {
reset: {
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));
},
resolve(_, options) {
if ((options === null || options === void 0 ? void 0 : options.defaultValue) === null) {
return {};
}
return options === null || options === void 0 ? void 0 : options.defaultValue;
},
apply(result) {
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, {
reset: true
});
}
},
validate: {
validate(name) {
return util.isOptional(name, util.isString);
},
update(state, _ref) {
var _intent$payload;
var {
submission,
intent,
error
} = _ref;
var name = (_intent$payload = intent.payload) !== null && _intent$payload !== void 0 ? _intent$payload : '';
var basePath = future.parsePath(name);
var allFields = error ?
// Consider fields / fieldset with errors as touched too
submission.fields.concat(Object.keys(error.fieldErrors)) : submission.fields;
var touchedFields = util.appendUniqueItem(state.touchedFields, name);
for (var field of allFields) {
// Add all child fields to the touched fields too
if (future.getRelativePath(field, basePath) !== null) {
touchedFields = util.appendUniqueItem(touchedFields, field);
}
}
return util.merge(state, {
touchedFields
});
}
},
update: {
validate(options) {
return future.isPlainObject(options) && util.isOptional(options.name, util.isString) && util.isOptional(options.index, util.isNumber) && !util.isUndefined(options.value);
},
resolve(value, options) {
var _options$value;
var name = future.appendPath(options.name, options.index);
return util.updatePathValue(value, name, (_options$value = options.value) !== null && _options$value !== void 0 ? _options$value : name === '' ? {} : null);
},
update(state, _ref2) {
var {
type,
submission,
intent
} = _ref2;
if (type === 'server') {
return state;
}
var listKeys = state.listKeys;
// Update the keys only for client updates to avoid double updates if there is no client validation
if (type === 'client') {
// TODO: Do we really need to update the keys here?
var name = future.appendPath(intent.payload.name, intent.payload.index);
// Remove all child keys
listKeys = name === '' ? {} : updateListKeys(state.listKeys, name);
}
var basePath = future.parsePath(intent.payload.name);
var touchedFields = state.touchedFields;
for (var field of submission.fields) {
if (basePath.length === 0 || future.getRelativePath(field, basePath) !== null) {
touchedFields = util.appendUniqueItem(touchedFields, field);
}
}
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, state), {}, {
listKeys,
touchedFields
});
}
},
insert: {
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');
},
resolve(value, options) {
var _options$index;
var result = value;
var itemValue = options.defaultValue;
if (options.from !== undefined) {
itemValue = future.getPathValue(result, options.from);
result = util.updatePathValue(result, options.from, '');
}
var list = Array.from(util.getPathArray(result, options.name));
insertItem(list, itemValue, (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : list.length);
return util.updatePathValue(result, options.name, list);
},
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 listError = (_result$error = result.error) === null || _result$error === void 0 ? void 0 : _result$error.fieldErrors[options.name];
if (options.onInvalid === 'revert' && listError != null) {
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, {
targetValue: undefined
});
}
if (options.from !== undefined) {
var _options$index2, _result$error2, _result$error3;
var index = (_options$index2 = options.index) !== null && _options$index2 !== void 0 ? _options$index2 : util.getPathArray(result.submission.payload, options.name).length;
var insertedItemPath = future.appendPath(options.name, index);
var insertedItemError = (_result$error2 = result.error) === null || _result$error2 === void 0 ? void 0 : _result$error2.fieldErrors[insertedItemPath];
var fromFieldError = (_result$error3 = result.error) === null || _result$error3 === void 0 ? void 0 : _result$error3.fieldErrors[options.from];
if (fromFieldError != null) {
var _result$error$formErr, _result$error4, _result$error5;
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 : null,
fieldErrors: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, (_result$error5 = result.error) === null || _result$error5 === void 0 ? void 0 : _result$error5.fieldErrors), {}, {
[insertedItemPath]: null
})
}
});
}
if (insertedItemError != null) {
var _result$error$formErr2, _result$error6, _result$error7;
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, {
targetValue: undefined,
error: {
formErrors: (_result$error$formErr2 = (_result$error6 = result.error) === null || _result$error6 === void 0 ? void 0 : _result$error6.formErrors) !== null && _result$error$formErr2 !== void 0 ? _result$error$formErr2 : null,
fieldErrors: _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, (_result$error7 = result.error) === null || _result$error7 === void 0 ? void 0 : _result$error7.fieldErrors), {}, {
[options.from]: insertedItemError,
[insertedItemPath]: null
})
}
});
}
}
return result;
},
update(state$1, _ref3) {
var _intent$payload$index;
var {
type,
submission,
intent,
ctx
} = _ref3;
if (type === 'server') {
return state$1;
}
var from = intent.payload.from;
var index = (_intent$payload$index = intent.payload.index) !== null && _intent$payload$index !== void 0 ? _intent$payload$index : util.getPathArray(submission.payload, intent.payload.name).length;
var updateListIndex = util.createPathIndexUpdater(intent.payload.name, currentIndex => index <= currentIndex ? currentIndex + 1 : currentIndex);
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 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.appendPath(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,
touchedFields
});
}
},
remove: {
validate(options) {
return future.isPlainObject(options) && util.isString(options.name) && util.isNumber(options.index) && util.isOptional(options.onInvalid, v => v === 'revert' || v === 'insert');
},
resolve(value, options) {
var list = Array.from(util.getPathArray(value, options.name));
removeItem(list, options.index);
return util.updatePathValue(value, options.name, list);
},
apply(result, options) {
var _result$error8;
// 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$error8 = result.error) !== null && _result$error8 !== void 0 && _result$error8.fieldErrors[options.name]) {
switch (options.onInvalid) {
case 'revert':
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, {
targetValue: undefined
});
case 'insert':
{
var list = Array.from(util.getPathArray(result.targetValue, options.name));
insertItem(list, options.defaultValue, list.length);
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, result), {}, {
targetValue: util.updatePathValue(result.targetValue, options.name, list)
});
}
}
}
return result;
},
update(state$1, _ref4) {
var {
type,
submission,
intent,
ctx
} = _ref4;
if (type === 'server') {
return state$1;
}
var currentValue = submission.payload;
var updateListIndex = util.createPathIndexUpdater(intent.payload.name, currentIndex => {
if (intent.payload.index === currentIndex) {
return null;
}
return intent.payload.index < currentIndex ? currentIndex - 1 : currentIndex;
});
var touchedFields = state$1.touchedFields;
var listKeys = state$1.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.appendPath(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.appendPath(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: listKeys,
touchedFields
});
}
},
reorder: {
validate(options) {
return future.isPlainObject(options) && util.isString(options.name) && util.isNumber(options.from) && util.isNumber(options.to);
},
resolve(value, options) {
var list = Array.from(util.getPathArray(value, options.name));
reorderItems(list, options.from, options.to);
return util.updatePathValue(value, options.name, list);
},
update(state$1, _ref5) {
var {
type,
submission,
intent
} = _ref5;
if (type === 'server') {
return state$1;
}
var currentValue = submission.payload;
var updateListIndex = util.createPathIndexUpdater(intent.payload.name, currentIndex => {
if (intent.payload.from === intent.payload.to) {
return currentIndex;
}
if (currentIndex === intent.payload.from) {
return intent.payload.to;
}
if (intent.payload.from < intent.payload.to) {
return currentIndex > intent.payload.from && currentIndex <= intent.payload.to ? currentIndex - 1 : currentIndex;
}
return currentIndex >= intent.payload.to && currentIndex < intent.payload.from ? currentIndex + 1 : currentIndex;
});
var touchedFields = util.appendUniqueItem(util.compactMap(state$1.touchedFields, updateListIndex), intent.payload.name);
var keys = 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$inten3;
var listKeys = Array.from((_state$listKeys$inten3 = state$1.listKeys[intent.payload.name]) !== null && _state$listKeys$inten3 !== void 0 ? _state$listKeys$inten3 : state.getDefaultListKey(state$1.resetKey, currentValue, intent.payload.name));
reorderItems(listKeys, intent.payload.from, intent.payload.to);
keys = _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, updateListKeys(state$1.listKeys, future.appendPath(intent.payload.name, intent.payload.from), updateListIndex)), {}, {
// Update existing list keys
[intent.payload.name]: listKeys
});
}
return _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, state$1), {}, {
listKeys: keys,
touchedFields
});
}
}
};
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;