UNPKG

@altiore/form

Version:

Form helper for building powerful forms

341 lines (340 loc) 17.1 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Form = void 0; var react_1 = __importStar(require("react")); var cloneDeep_1 = __importDefault(require("lodash/cloneDeep")); var get_1 = __importDefault(require("lodash/get")); var isEqual_1 = __importDefault(require("lodash/isEqual")); var set_1 = __importDefault(require("lodash/set")); var unset_1 = __importDefault(require("lodash/unset")); var consts_1 = require("../@common/consts"); var form_context_1 = require("../@common/form-context"); var types_1 = require("../@common/types"); var utils_1 = require("../@common/utils"); var form_utils_1 = require("./form.utils"); var getItemsFromDefVal = function (_, i) { return i; }; /** * Форма - элемент взаимодействия пользователя с сайтом или приложением */ var Form = function (_a) { var children = _a.children, defaultValues = _a.defaultValues, _b = _a.hideErrorsInXSeconds, hideErrorsInXSeconds = _b === void 0 ? consts_1.DEF_HIDE_ERROR_IN_X_SEC : _b, html5Validation = _a.html5Validation, onSubmit = _a.onSubmit, props = __rest(_a, ["children", "defaultValues", "hideErrorsInXSeconds", "html5Validation", "onSubmit"]); var formRef = (0, react_1.useRef)(null); var _c = (0, react_1.useState)({}), fields = _c[0], setFields = _c[1]; var _d = (0, react_1.useState)(false), isSubmitting = _d[0], setIsSubmitting = _d[1]; var timeout = (0, react_1.useRef)(); var clearErrors = (0, react_1.useCallback)(function () { setFields(function (curFields) { var newFields = __assign({}, curFields); var shouldUpdate = false; Object.entries(newFields).forEach(function (_a) { var curFieldName = _a[0], curFieldVal = _a[1]; if (curFieldVal.isInvalid) { shouldUpdate = true; newFields[curFieldName] = __assign(__assign({}, newFields[curFieldName]), { error: undefined, errors: [], isInvalid: false }); } }); if (shouldUpdate) { return newFields; } return curFields; }); }, [setFields]); var setErrors = (0, react_1.useCallback)(function (fieldName, errors, force, isWarning) { setFields(function (s) { var _a; if (!s[fieldName]) { // этот код, похоже, никогда не выполняется и здесь лишь для совместимости return s; } var fieldData; if (isWarning) { fieldData = __assign(__assign({}, s[fieldName]), { isUntouched: false, warning: errors === null || errors === void 0 ? void 0 : errors[0], warnings: errors }); } else { fieldData = __assign(__assign({}, s[fieldName]), { error: errors === null || errors === void 0 ? void 0 : errors[0], errors: errors, isInvalid: Boolean(errors === null || errors === void 0 ? void 0 : errors.length), isUntouched: false }); } // улучает производительность, избегая рендера, если ошибки не изменились if ((0, isEqual_1.default)(s[fieldName], fieldData) && !force) { return s; } if (errors === null || errors === void 0 ? void 0 : errors.length) { if (timeout.current) { clearTimeout(timeout.current); } if (typeof hideErrorsInXSeconds === 'number') { timeout.current = setTimeout(clearErrors, hideErrorsInXSeconds * 1000); } } return __assign(__assign({}, s), (_a = {}, _a[fieldName] = fieldData, _a)); }); }, [clearErrors, hideErrorsInXSeconds, setFields]); var setNestedErrors = (0, react_1.useCallback)(function (errors) { (0, form_utils_1.toFlatErrors)(errors, setErrors); }, [setErrors]); // К сожалению, нормализация индексов не работает, т.к. механизмы React конфликтуют с изменениями сырого кода // const normalizeItems = useCallback( // ( // fieldPattern: string, // s: Record<string, FieldMeta>, // fixedItems: Array<number>, // ) => { // if (Array.isArray(s[fieldPattern].items)) { // let newState = s; // const it_len = fixedItems.length; // for (let i = 0; i < it_len; i++) { // Object.entries(s).forEach(([oldFieldName, fieldMeta]) => { // const matched = oldFieldName.match( // new RegExp(`^${fieldPattern}\.${fixedItems[i]}\.(.+)`), // ); // if (fixedItems[i] !== i && matched && document) { // const newFieldName = `${fieldPattern}.${i}.${matched[1]}`; // // const elemOld = document.querySelector( // `[name="${oldFieldName}"]`, // ); // const elemNew = document.querySelector( // `[name="${newFieldName}"]`, // ); // // if (elemOld && matched[1]) { // elemOld.setAttribute('name', newFieldName); // delete newState[oldFieldName]; // newState = { // ...newState, // [newFieldName]: fieldMeta, // }; // // // TODO: точно непонятно, почему это работает // // Вероятно, это связано с тем, как работают ключи React // // Пока этот код срабатывал всегда, но это не значит, что это будет действительно всегда работать // // Возможно, для очень больших форм этот код может создавать ухудшение производительности // if (elemNew && matched[1]) { // (elemNew as any).value = (elemOld as any).value; // } // } // } // }); // } // // return newState; // } // return s; // }, // [], // ); var setItems = (0, react_1.useCallback)(function (fieldName, setItemsArg, getErrors, defaultValue) { setFields(function (s) { var _a; var itemsPrev = __spreadArray([], s[fieldName].items, true); var items = setItemsArg(s[fieldName].items); var errors = getErrors(items); var newState = __assign(__assign({}, s), (_a = {}, _a[fieldName] = __assign(__assign({}, s[fieldName]), { defaultValue: defaultValue, error: errors === null || errors === void 0 ? void 0 : errors[0], errors: errors, isInvalid: Boolean(errors === null || errors === void 0 ? void 0 : errors.length), isUntouched: false, items: items, itemsPrev: itemsPrev }), _a)); // Если происходит удаление элемента массива, то нужно отфильтровать все неиспользуемые поля if (itemsPrev.length > items.length) { var removedItems_1 = itemsPrev.filter(function (el) { return !items.includes(el); }); var allRemovedFields = Object.keys(newState).filter(function (oldFieldName) { return removedItems_1.some(function (removedItem) { return oldFieldName.match(new RegExp("^" + fieldName + "." + removedItem + "((.(.+))|$)")); }); }); allRemovedFields.forEach(function (removedField) { delete newState[removedField]; }); } // К сожалению, нормализация индексов не работает, т.к. механизмы React конфликтуют с изменениями сырого кода // if (itemsPrev.length !== items.length) { // return { // ...normalizeItems(fieldName, s, items), // [fieldName]: { // ...s[fieldName], // defaultValue, // error: errors?.[0], // errors, // isInvalid: Boolean(errors?.length), // isUntouched: false, // items: Array.from({length: items.length}, (_, index) => index), // itemsPrev, // }, // }; // } return newState; }); }, [setFields]); var registerField = (0, react_1.useCallback)(function (fieldName, fieldType, fieldDefaultValue, validators) { setFields(function (s) { var _a; var _b; var defaultValue = (_b = (0, get_1.default)(defaultValues, fieldName)) !== null && _b !== void 0 ? _b : fieldDefaultValue; var items = fieldType === types_1.FieldType.ARRAY ? Array.isArray(defaultValue) ? defaultValue.map(getItemsFromDefVal) : [] : undefined; return __assign(__assign({}, s), (_a = {}, _a[fieldName] = { defaultValue: defaultValue, error: undefined, errors: [], fieldType: fieldType, isInvalid: false, // Этот флаг работает только для полей у которых есть валидаторы isUntouched: true, items: items, itemsPrev: items, name: fieldName, setErrors: setErrors.bind({}, fieldName), validators: validators ? validators : [], warning: undefined, warnings: [], }, _a)); }); return function () { setFields(function (s) { var _a; var newState = __assign(__assign({}, s), (_a = {}, _a[fieldName] = undefined, _a)); delete newState[fieldName]; return newState; }); }; }, [defaultValues, setFields]); var getFormValueByName = (0, react_1.useCallback)(function (name) { if (!formRef) { throw new Error('Форма недоступна'); } return (0, utils_1.getValueByNodeName)(name, formRef); }, [formRef]); var handleSubmit = (0, react_1.useCallback)(function (formEventOrCustomHandler) { var evt = 'not a submit event'; if (formEventOrCustomHandler === null || formEventOrCustomHandler === void 0 ? void 0 : formEventOrCustomHandler.preventDefault) { formEventOrCustomHandler.preventDefault(); evt = formEventOrCustomHandler; } // 1. Преобразовываем данные в правильный формат var values = (0, form_utils_1.getFormValues)(formRef.current, fields); // создаём клон данных, т.к. похоже, что валидация может менять данные. Чтоб избежать изменения данных валидацией, создаём копию var valuesClone = (0, cloneDeep_1.default)(values); // 2. Проверка данных на соответствие правилам валидации var isFormInvalid = false; Object.entries(fields).forEach(function (_a) { var fieldName = _a[0], fieldMeta = _a[1]; if (fieldMeta.validators) { var fieldType = fieldMeta.fieldType || types_1.FieldType.TEXT; var value_1; if (fieldType === types_1.FieldType.ARRAY) { value_1 = (0, form_utils_1.getArrayValue)(fieldName, values, fieldMeta.items); } else { value_1 = (0, get_1.default)(values, fieldName); } var errors_1 = []; fieldMeta.validators.forEach(function (validate) { var error = validate(value_1, fieldName, getFormValueByName); if (error) { isFormInvalid = true; errors_1.push(error); } }); setErrors(fieldName, errors_1); } }); if (isFormInvalid) { setIsSubmitting(false); return Promise.resolve(); } Object.entries(fields).forEach(function (_a) { var fieldName = _a[0], fieldMeta = _a[1]; // Преобразовать массивы к массивам с непустыми данными // (должно быть в самом конце для правильной работы!) // мы не можем выполнить это действие во время проверки валидации, // т.к. не можем менять результирующие данные до их проверки // создание новой переменной с результирующими данными усложняет // понимание кода if (fieldMeta.fieldType === types_1.FieldType.ARRAY) { var value = (0, form_utils_1.getArrayValue)(fieldName, valuesClone, fieldMeta.items); (0, unset_1.default)(valuesClone, fieldName); (0, set_1.default)(valuesClone, fieldName, value); } }); setIsSubmitting(true); var localSubmitFunc = typeof formEventOrCustomHandler === 'function' ? formEventOrCustomHandler : onSubmit; Promise.resolve( // если было событие submit, то передаем это событие как 3-ий параметр, // чтоб можно было что-то с ним сделать localSubmitFunc(valuesClone, setNestedErrors, evt)) .then(function () { setIsSubmitting(false); }) .catch(console.error); }, [ fields, getFormValueByName, onSubmit, setIsSubmitting, setErrors, setNestedErrors, ]); return (react_1.default.createElement("form", __assign({ noValidate: !html5Validation }, props, { onSubmit: handleSubmit, ref: formRef }), react_1.default.createElement(form_context_1.FormContext.Provider, { value: { fields: fields, formRef: formRef, isSubmitting: isSubmitting, onSubmit: handleSubmit, registerField: registerField, setItems: setItems, } }, children))); }; exports.Form = Form;