@altiore/form
Version:
Form helper for building powerful forms
341 lines (340 loc) • 17.1 kB
JavaScript
;
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;