@conform-to/dom
Version:
A set of opinionated helpers built on top of the Constraint Validation API
427 lines (401 loc) • 13.2 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var submission = require('./submission.js');
/**
* Construct a form data with the submitter value.
* It utilizes the submitter argument on the FormData constructor from modern browsers
* with fallback to append the submitter value in case it is not unsupported.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData#parameters
*/
function getFormData(form, submitter) {
var payload = new FormData(form, submitter);
if (submitter && submitter.type === 'submit' && submitter.name !== '') {
var entries = payload.getAll(submitter.name);
// This assumes the submitter value to be always unique, which should be fine in most cases
if (!entries.includes(submitter.value)) {
payload.append(submitter.name, submitter.value);
}
}
return payload;
}
/**
* Returns the paths from a name based on the JS syntax convention
* @example
* ```js
* const paths = getPaths('todos[0].content'); // ['todos', 0, 'content']
* ```
*/
function getPaths(name) {
if (!name) {
return [];
}
return name.split(/\.|(\[\d*\])/).reduce((result, segment) => {
if (typeof segment !== 'undefined' && segment !== '' && segment !== '__proto__' && segment !== 'constructor' && segment !== 'prototype') {
if (segment.startsWith('[') && segment.endsWith(']')) {
var index = segment.slice(1, -1);
result.push(Number(index));
} else {
result.push(segment);
}
}
return result;
}, []);
}
/**
* Returns a formatted name from the paths based on the JS syntax convention
* @example
* ```js
* const name = formatPaths(['todos', 0, 'content']); // "todos[0].content"
* ```
*/
function formatPaths(paths) {
return paths.reduce((name, path) => {
if (typeof path === 'number') {
return "".concat(name, "[").concat(Number.isNaN(path) ? '' : path, "]");
}
if (name === '' || path === '') {
return [name, path].join('');
}
return [name, path].join('.');
}, '');
}
/**
* Format based on a prefix and a path
*/
function formatName(prefix, path) {
return typeof path !== 'undefined' ? formatPaths([...getPaths(prefix), path]) : prefix !== null && prefix !== void 0 ? prefix : '';
}
/**
* Check if a name match the prefix paths
*/
function isPrefix(name, prefix) {
var paths = getPaths(name);
var prefixPaths = getPaths(prefix);
return paths.length >= prefixPaths.length && prefixPaths.every((path, index) => paths[index] === path);
}
/**
* Compare the parent and child paths to get the relative paths
* Returns null if the child paths do not start with the parent paths
*/
function getChildPaths(parentNameOrPaths, childName) {
var parentPaths = typeof parentNameOrPaths === 'string' ? getPaths(parentNameOrPaths) : parentNameOrPaths;
var childPaths = getPaths(childName);
if (childPaths.length >= parentPaths.length && parentPaths.every((path, index) => childPaths[index] === path)) {
return childPaths.slice(parentPaths.length);
}
return null;
}
/**
* Assign a value to a target object by following the paths
*/
function setValue(target, name, valueFn) {
var paths = getPaths(name);
var length = paths.length;
var lastIndex = length - 1;
var index = -1;
var pointer = target;
while (pointer != null && ++index < length) {
var _key = paths[index];
var nextKey = paths[index + 1];
var newValue = index != lastIndex ? Object.prototype.hasOwnProperty.call(pointer, _key) && pointer[_key] !== null ? pointer[_key] : typeof nextKey === 'number' ? [] : {} : valueFn(pointer[_key]);
pointer[_key] = newValue;
pointer = pointer[_key];
}
}
/**
* Retrive the value from a target object by following the paths
*/
function getValue(target, name) {
var pointer = target;
for (var path of getPaths(name)) {
if (typeof pointer === 'undefined' || pointer == null) {
break;
}
if (!Object.prototype.hasOwnProperty.call(pointer, path)) {
return;
}
if (isPlainObject(pointer) && typeof path === 'string') {
pointer = pointer[path];
} else if (Array.isArray(pointer) && typeof path === 'number') {
pointer = pointer[path];
} else {
return;
}
}
return pointer;
}
/**
* Check if the value is a plain object
*/
function isPlainObject(obj) {
return !!obj && obj.constructor === Object && Object.getPrototypeOf(obj) === Object.prototype;
}
function isGlobalInstance(obj, className) {
var Ctor = globalThis[className];
return typeof Ctor === 'function' && obj instanceof Ctor;
}
/**
* Normalize value by removing empty object or array, empty string and null values
*/
function normalize(value) {
var acceptFile = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
if (isPlainObject(value)) {
var obj = Object.keys(value).sort().reduce((result, key) => {
var data = normalize(value[key], acceptFile);
if (typeof data !== 'undefined') {
result[key] = data;
}
return result;
}, {});
if (Object.keys(obj).length === 0) {
return;
}
return obj;
}
if (Array.isArray(value)) {
if (value.length === 0) {
return undefined;
}
return value.map(item => normalize(item, acceptFile));
}
if (typeof value === 'string' && value === '' || value === null || isGlobalInstance(value, 'File') && (!acceptFile || value.size === 0)) {
return;
}
return value;
}
/**
* Flatten a tree into a dictionary
*/
function flatten(data) {
var _options$resolve;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var result = {};
var resolve = (_options$resolve = options.resolve) !== null && _options$resolve !== void 0 ? _options$resolve : data => data;
function process(data, prefix) {
var value = normalize(resolve(data));
if (typeof value !== 'undefined') {
result[prefix] = value;
}
if (Array.isArray(data)) {
for (var i = 0; i < data.length; i++) {
process(data[i], "".concat(prefix, "[").concat(i, "]"));
}
} else if (isPlainObject(data)) {
for (var [_key2, _value] of Object.entries(data)) {
process(_value, prefix ? "".concat(prefix, ".").concat(_key2) : _key2);
}
}
}
if (data) {
var _options$prefix;
process(data, (_options$prefix = options.prefix) !== null && _options$prefix !== void 0 ? _options$prefix : '');
}
return result;
}
function deepEqual(left, right) {
if (Object.is(left, right)) {
return true;
}
if (left == null || right == null) {
return false;
}
// Compare plain objects
if (isPlainObject(left) && isPlainObject(right)) {
var prevKeys = Object.keys(left);
var nextKeys = Object.keys(right);
if (prevKeys.length !== nextKeys.length) {
return false;
}
for (var _key3 of prevKeys) {
if (!Object.prototype.hasOwnProperty.call(right, _key3) || !deepEqual(left[_key3], right[_key3])) {
return false;
}
}
return true;
}
// Compare arrays
if (Array.isArray(left) && Array.isArray(right)) {
if (left.length !== right.length) {
return false;
}
for (var i = 0; i < left.length; i++) {
if (!deepEqual(left[i], right[i])) {
return false;
}
}
return true;
}
return false;
}
/**
* The form value of a submission. This is usually constructed from a FormData or URLSearchParams.
* It may contains JSON primitives if the value is updated based on a form intent.
*/
/**
* The data of a form submission.
*/
/**
* Parse `FormData` or `URLSearchParams` into a submission object.
* This function structures the form values based on the naming convention.
* It also includes all the field names and the intent if the `intentName` option is provided.
*
* @example
* ```ts
* const formData = new FormData();
*
* formData.append('email', 'test@example.com');
* formData.append('password', 'secret');
*
* parseSubmission(formData)
* // {
* // value: { email: 'test@example.com', password: 'secret' },
* // fields: ['email', 'password'],
* // intent: null,
* // }
*
* // If you have an intent field
* formData.append('intent', 'login');
* parseSubmission(formData, { intentName: 'intent' })
* // {
* // value: { email: 'test@example.com', password: 'secret' },
* // fields: ['email', 'password'],
* // intent: 'login',
* // }
* ```
*/
function parseSubmission(formData, options) {
var _options$intentName;
var intentName = (_options$intentName = options === null || options === void 0 ? void 0 : options.intentName) !== null && _options$intentName !== void 0 ? _options$intentName : submission.INTENT;
var submission$1 = {
value: {},
fields: [],
intent: null
};
var _loop = function _loop() {
var _options$skipEntry;
if (_name !== intentName && !(options !== null && options !== void 0 && (_options$skipEntry = options.skipEntry) !== null && _options$skipEntry !== void 0 && _options$skipEntry.call(options, _name))) {
var _value2 = formData.getAll(_name);
setValue(submission$1.value, _name, () => _value2.length > 1 ? _value2 : _value2[0]);
submission$1.fields.push(_name);
}
};
for (var _name of new Set(formData.keys())) {
_loop();
}
if (intentName) {
// We take the first value of the intent field if it exists.
var intent = formData.get(intentName);
if (typeof intent === 'string') {
submission$1.intent = intent;
}
}
return submission$1;
}
function defaultSerialize(value) {
if (typeof value === 'string' || isGlobalInstance(value, 'File')) {
return value;
}
if (typeof value === 'boolean') {
return value ? 'on' : undefined;
}
if (value instanceof Date) {
return value.toISOString();
}
return value === null || value === void 0 ? void 0 : value.toString();
}
/**
* A utility function that checks whether the current form data differs from the default values.
*
* @see https://conform.guide/api/react/future/isDirty
* @example Enable a submit button only if the form is dirty
*
* ```tsx
* const dirty = useFormData(
* formRef,
* (formData) => isDirty(formData, { defaultValue }) ?? false,
* );
*
* return (
* <button type="submit" disabled={!dirty}>
* Save changes
* </button>
* );
* ```
*/
function isDirty(
/**
* The current form data to compare. It can be:
*
* - A `FormData` object
* - A `URLSearchParams` object
* - A plain object that was parsed from form data (i.e. `submission.payload`)
*/
formData, options) {
var _options$serialize;
if (!formData) {
return;
}
var formValue = formData instanceof FormData || formData instanceof URLSearchParams ? parseSubmission(formData, {
intentName: options === null || options === void 0 ? void 0 : options.intentName,
skipEntry: options === null || options === void 0 ? void 0 : options.skipEntry
}).value : formData;
var defaultValue = options === null || options === void 0 ? void 0 : options.defaultValue;
var serialize = (_options$serialize = options === null || options === void 0 ? void 0 : options.serialize) !== null && _options$serialize !== void 0 ? _options$serialize : defaultSerialize;
function normalize(value) {
if (Array.isArray(value)) {
if (value.length === 0) {
return undefined;
}
var array = value.map(normalize);
if (array.length === 1 && (typeof array[0] === 'string' || array[0] === undefined)) {
return array[0];
}
return array;
}
if (isPlainObject(value)) {
var entries = Object.entries(value).reduce((list, _ref) => {
var [key, value] = _ref;
var normalizedValue = normalize(value);
if (typeof normalizedValue !== 'undefined') {
list.push([key, normalizedValue]);
}
return list;
}, []);
if (entries.length === 0) {
return undefined;
}
return Object.fromEntries(entries);
}
// If the value is null or undefined, treat it as undefined
if (value == null) {
return undefined;
}
// Removes empty strings, so that bpth empty string and undefined are treated as the same
if (typeof value === 'string' && value === '') {
return undefined;
}
// Remove empty File as well, which happens if no File was selected
if (isGlobalInstance(value, 'File') && value.name === '' && value.size === 0) {
return undefined;
}
return serialize(value, defaultSerialize);
}
return !deepEqual(normalize(formValue), normalize(defaultValue));
}
exports.deepEqual = deepEqual;
exports.defaultSerialize = defaultSerialize;
exports.flatten = flatten;
exports.formatName = formatName;
exports.formatPaths = formatPaths;
exports.getChildPaths = getChildPaths;
exports.getFormData = getFormData;
exports.getPaths = getPaths;
exports.getValue = getValue;
exports.isDirty = isDirty;
exports.isGlobalInstance = isGlobalInstance;
exports.isPlainObject = isPlainObject;
exports.isPrefix = isPrefix;
exports.normalize = normalize;
exports.parseSubmission = parseSubmission;
exports.setValue = setValue;