UNPKG

@conform-to/dom

Version:

A set of opinionated helpers built on top of the Constraint Validation API

427 lines (401 loc) 13.2 kB
'use strict'; 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;