UNPKG

@open-formulieren/formio-builder

Version:

An opinionated Formio webform builder for Open Forms

124 lines (123 loc) 5.24 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { useFormikContext } from 'formik'; /** * Provides tools to introspect validation errors for Formik-based forms. */ export function useErrorChecker() { const { errors } = useFormikContext(); const pathsWithErrors = getErrorNames(errors); /** * Check if there currently are any validation errors for the provided (partial) paths. * * Example usage: * * hasAnyError(errors, 'key', 'defaultValue', 'validate.maxLength') * * @param errors - The Formik validation errors structure. */ function hasAnyError(...paths) { // if no paths are provided, there is no filter, so return if there is *any* // validation error. if (!paths.length) { return pathsWithErrors.length > 0; } // otherwise, check if there is overlap between the array of errored paths and // specified paths. // In the future, we should be able to use Set.prototype.intersection for this. return paths.some(p => pathsWithErrors.includes(p)); } return { hasAnyError, }; } /** * Recursively look up the names of fields that have errors, and join them using * path-syntax. * * Example: * * getErrorNames({foo: {bar: 'an error!'}}) * // ['foo', ['foo.bar']] * * @private */ export function getErrorNames(errors) { const names = []; // Iterating over objects is surprisingly hard/annoying in TS due to JS' quirks, see // https://effectivetypescript.com/2020/05/26/iterate-objects/ let k; for (k in errors) { const error = errors[k]; // if there is nothing to display (error is undefined or empty string), then the // field may not be marked as having errors. This function is primarily used when // determining which edit form tabs/groups of fields have errors to draw the // attention of users. if (!error) continue; switch (typeof error) { case 'string': { names.push(k); break; } case 'object': { let nestedNames = []; if (Array.isArray(error)) { error.forEach((item, index) => { // same condition as earlier if (!item) return; switch (typeof item) { // a single error, for the entire object in the array. Could be because of // validation after each field was individually validated. case 'string': { nestedNames.push(index.toString()); break; } case 'object': { const deepNested = getErrorNames(item).map(n => `${index}.${n}`); if (deepNested.length) { nestedNames.push(index.toString(), ...deepNested); } break; } default: throw new Error('Unexpected item type'); } }); } else { nestedNames = getErrorNames(error); } if (nestedNames.length) { // include the parent to get all nodes from root -> error leaf node in the // list of errored fields const prefixed = nestedNames.map(n => `${k}.${n}`); names.push(k, ...prefixed); } break; } default: throw new Error('Unexpected error type'); } } // Satisfying typescript seems to be quite a challenge here. There's definitely some // blame to JS quirks (see https://effectivetypescript.com/2020/05/26/iterate-objects/ // for more background), but the combination of our Paths enum with (inferred) // string keys (from keyof Object + FormikErrors) doesn't place nice together either. // // There is also an aspect on our side - the Paths<> generic currently is *not* able // to figure out a naming pattern for arrays/lists (e.g. something like `foo[].bar`), // while we *do* return error names like `foo.1.bar` in this implementation. return names; } export const useValidationErrors = (name) => { const { getFieldMeta } = useFormikContext(); // FIXME: inferred type says string, but for nested objects this can be an object itself! const { error } = name ? getFieldMeta(name) : { error: '' }; const errors = name && error && typeof error === 'string' ? [error] : []; return { hasErrors: errors.length > 0, errors, }; }; export const ErrorList = ({ errors }) => (_jsx("div", Object.assign({ className: "formio-errors invalid-feedback" }, { children: errors.map((error, index) => (_jsx("div", Object.assign({ className: "form-text error" }, { children: error }), index))) })));