@de100/form-echo
Version:
A form state management for fields validations and errors
883 lines (876 loc) • 28.7 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
createFormStoreBuilder: () => createFormStoreBuilder,
errorFormatter: () => errorFormatter,
fvh: () => fieldValue_default,
handleCreateFormStore: () => handleCreateFormStore,
isZodError: () => isZodError,
isZodValidator: () => isZodValidator,
useCreateFormStore: () => useCreateFormStore
});
module.exports = __toCommonJS(src_exports);
// src/utils/FormStoreField.js
var FormStoreField = class {
/** @type {string} */
id;
/** @type {FieldsValues[Key]} */
value;
/** @type {FieldMetadata<Key, FieldsValues[Key]>} */
metadata;
/** @type {((fieldValue: unknown) => Exclude<FieldsValues[Key], (value: FieldsValues[Key]) => FieldsValues[Key]>) | undefined} */
valueFromFieldToStore;
/** @type {(storeValue: FieldsValues[Key]) => string | ReadonlyArray<string> | number | undefined} */
valueFromStoreToField;
/**
* @param {{
* id: string;
* value: FieldsValues[Key];
* metadata: FieldMetadata<Key, FieldsValues[Key]>;
* valueFromFieldToStore?: (fieldValue: unknown) => Exclude<FieldsValues[Key], (value: FieldsValues[Key]) => FieldsValues[Key]>;
* valueFromStoreToField?: (StoreValue: FieldsValues[Key]) => string | ReadonlyArray<string> | number | undefined;
* }} params
*/
constructor(params) {
this.id = params.id;
this.value = params.value;
this.metadata = params.metadata;
this.valueFromFieldToStore = params.valueFromFieldToStore;
this.valueFromStoreToField = params.valueFromStoreToField ?? /**
* @param {FieldsValues[Key]} StoreValue
* @returns string | ReadonlyArray<string> | number | undefined
*/
((value) => value ?? "");
}
/**
* @description Gets the field value converted _(using the passed `valueFromStoreToField` if not it will just return the original value)_ from the store value.
*
* @type {string | ReadonlyArray<string> | number | undefined}
* */
get storeToFieldValue() {
return this.valueFromStoreToField(this.value);
}
};
// src/utils/zod.ts
function isZodValidator(validator) {
return !!(validator instanceof Object && "parseAsync" in validator && typeof validator.parseAsync === "function");
}
function isZodError(error) {
return error instanceof Object && "errors" in error;
}
function errorFormatter(error) {
if (isZodError(error))
return error.format()._errors.join(", ");
if (error instanceof Error)
return error.message;
return "Something went wrong!";
}
// src/utils/zustand.ts
var import_zustand = require("zustand");
var import_react = require("react");
var handleCreateFormStore = (params) => (0, import_zustand.createStore)(createFormStoreBuilder(params));
var useCreateFormStore = (props) => {
const baseId = (0, import_react.useId)();
const formStore = (0, import_react.useState)(
handleCreateFormStore({ ...props, baseId: props.baseId || baseId })
);
return formStore[0];
};
// src/utils/helpers/inputDate.js
function formatDate(date, type) {
let formattedDate = "";
switch (type) {
case "date":
formattedDate = date.toISOString().slice(0, 10);
break;
case "time":
formattedDate = date.toTimeString().slice(0, 8);
break;
case "datetime-local":
formattedDate = `${date.getFullYear()}-${`${date.getMonth() + 1}`.padStart(2, "0")}-${`${date.getDate()}`.padStart(
2,
"0"
)}T${`${date.getHours()}`.padStart(
2,
"0"
)}:${`${date.getMinutes()}`.padStart(2, "0")}`;
break;
case "week":
const year = date.getFullYear();
const weekNumber = getWeekNumber(date);
formattedDate = `${year}-W${weekNumber.toString().length < 2 ? "0" + weekNumber.toString() : weekNumber.toString()}`;
break;
case "month":
formattedDate = date.toISOString().slice(0, 7);
break;
default:
break;
}
return formattedDate;
}
function parseDate(dateString, type) {
let parsedDate;
switch (type) {
case "date":
parsedDate = new Date(dateString);
break;
case "time":
const [hours, minutes, seconds] = dateString.toString().split(":");
parsedDate = /* @__PURE__ */ new Date();
parsedDate.setHours(Number(hours || 0));
parsedDate.setMinutes(Number(minutes || 0));
parsedDate.setSeconds(Number(seconds || 0));
break;
case "datetime-local":
parsedDate = new Date(dateString.toString().replace(" ", "T"));
break;
case "week":
const [yearString, weekString] = dateString.toString().split("-W");
const year = Number(yearString);
const week = Number(weekString);
parsedDate = getFirstDateOfWeek(year, week);
break;
case "month":
parsedDate = /* @__PURE__ */ new Date(`${dateString}-01`);
break;
default:
parsedDate = /* @__PURE__ */ new Date();
break;
}
return parsedDate;
}
function getWeekNumber(date) {
const yearStart = new Date(date.getFullYear(), 0, 1);
const daysSinceYearStart = (date.valueOf() - yearStart.valueOf()) / (1e3 * 60 * 60 * 24);
const weekNumber = Math.floor(daysSinceYearStart / 7) + 1;
return weekNumber;
}
function getFirstDateOfWeek(year, week) {
const januaryFirst = new Date(year, 0, 1);
const daysToFirstMonday = (8 - januaryFirst.getDay()) % 7;
const firstMonday = new Date(januaryFirst);
firstMonday.setDate(januaryFirst.getDate() + daysToFirstMonday);
const daysToTargetMonday = (week - 1) * 7;
const targetMonday = new Date(firstMonday);
targetMonday.setDate(firstMonday.getDate() + daysToTargetMonday);
return targetMonday;
}
var inputDateHelpers = {
/**
* Formats a date object to the desired string format based on the type.
* @param {Date} date - The Date object to be formatted.
* @param {string} type - The format type ('date', 'time', 'datetime-local', 'week', or 'month').
* @returns {string} A formatted string based on the specified format.
*/
formatDate,
/**
* Parses a string in the specified format and returns a Date object.
* @param {string} dateString - The string to be parsed.
* @param {string} type - The format type ('date', 'time', 'datetime-local', 'week', or 'month').
* @returns {Date} - The parsed Date object.
*/
parseDate,
/**
* Returns the week number of the year for a given date.
* @param {Date} date - The date object for which to calculate the week number.
* @returns {number} - The week number.
*/
getWeekNumber,
/**
* Returns the first date (Monday) of a given week in a year.
* @param {number} year - The year of the target week.
* @param {number} week - The week number (1-53) of the desired week.
* @returns {Date} - The first date (Monday) of the specified week.
*/
getFirstDateOfWeek
};
// src/utils/helpers/fieldValue.js
var dateInput = {
/**
* @param {import("../..").InputDateTypes} type
* @description used to handle parsing ("date", "time", "datetime-local", "week", "month") and the cases of falsy values results to `null` like when clearing the input
*/
parse: function(type) {
return function(dateString) {
return !dateString ? null : inputDateHelpers.parseDate(dateString, type);
};
},
/**
* @param {import("../..").InputDateTypes} type
* @description used to handle formatting ("date", "time", "datetime-local", "week", "month") and the cases of falsy values results to '' like when clearing the input
*/
format: function(type) {
return function(dateString) {
return !dateString ? null : inputDateHelpers.formatDate(dateString, type);
};
}
};
function onNotNullableTo(defaultValue) {
return function(value) {
const symbol = Symbol();
const isNullable = value ?? symbol;
if (isNullable !== symbol) {
return (
/** @type {OnNullableDefaultReturn<Value, DefaultValue>} */
defaultValue
);
}
return (
/** @type {OnNullableDefaultReturn<Value, DefaultValue>} */
value
);
};
}
var onNullable = {
/**
* @template Value
* @param {Value} value
*/
toEmptyString: function(value) {
return (
/** @type {OnNullableDefaultReturn<Value, "">} */
value ?? ""
);
},
/**
* @template Value
* @param {Value} value
*/
toUndefined: function(value) {
return (
/** @type {OnNullableDefaultReturn<Value, undefined>} */
value ?? void 0
);
},
/**
* @template Value
* @param {Value} value
*/
toNull: function(value) {
return (
/** @type {OnNullableDefaultReturn<Value, null>} */
value ?? null
);
},
/**
* @template DefaultValue
* @param {DefaultValue} defaultValue
*/
to: function(defaultValue) {
return function(value) {
const symbol = Symbol();
const isNullable = value ?? symbol;
if (isNullable === symbol) {
return (
/** @type {OnNullableDefaultReturn<Value, DefaultValue>} */
defaultValue
);
}
return (
/** @type {OnNullableDefaultReturn<Value, DefaultValue>} */
value
);
};
},
falsy: {
/**
* @template Value
* @param {Value} value
*/
toEmptyString: function(value) {
return onNotNullableTo(
/** @type {""} */
""
)(value);
},
/**
* @template Value
* @param {Value} value
*/
toUndefined: function(value) {
return onNotNullableTo(void 0)(value);
},
/**
* @template Value
* @param {Value} value
*/
toNull: function(value) {
return onNotNullableTo(null)(value);
},
/**
* @template DefaultValue
* @param {DefaultValue} defaultValue
*/
to: onNotNullableTo
}
};
function onFalsyTo(defaultValue) {
return function(value) {
return (
/** @type {OnFalsyDefaultReturn<Value, DefaultValue>} */
!value ? defaultValue : value
);
};
}
var onFalsy = {
toEmptyString: onFalsyTo(
/** @type {""} */
""
),
toUndefined: onFalsyTo(void 0),
toNull: onFalsyTo(null),
to: onFalsyTo
};
function onTruthyTo(defaultValue) {
return function(value) {
return (
/** @type {OnTruthyDefaultReturn<Value, DefaultValue>} */
!value ? value : defaultValue
);
};
}
var onTruthy = {
toEmptyString: onTruthyTo(
/** @type {""} */
""
),
toUndefined: onTruthyTo(void 0),
toNull: onTruthyTo(null),
to: onTruthyTo
};
var formFieldValueHelpers = {
onDateInput: dateInput,
onNullable,
onFalsy,
onTruthy
};
var fieldValue_default = formFieldValueHelpers;
// src/utils/index.ts
function createFormStoreMetadata(params, baseId) {
if (!params.initialValues || typeof params.initialValues !== "object")
throw new Error("");
const metadata = {
baseId,
formId: `${baseId}-form`,
fieldsNames: {},
fieldsNamesMap: {},
//
validatedFieldsNames: [],
validatedFieldsNamesMap: {},
// //
manualValidatedFields: [],
manualValidatedFieldsMap: [],
// //
referencedValidatedFields: [],
referencedValidatedFieldsMap: []
};
metadata.fieldsNames = Object.keys(
params.initialValues
);
for (const fieldName of metadata.fieldsNames) {
metadata.fieldsNamesMap[fieldName] = true;
}
for (const key in params.validationsHandlers) {
metadata.validatedFieldsNames.push(key);
metadata.validatedFieldsNamesMap[key] = true;
if (key in metadata.fieldsNamesMap) {
metadata.referencedValidatedFields.push(
key
);
metadata.referencedValidatedFieldsMap[key] = true;
continue;
}
metadata.manualValidatedFields.push(
key
);
metadata.manualValidatedFieldsMap[
key
// as unknown as (typeof metadata)['manualValidatedFieldsMap'][number]
] = true;
}
return metadata;
}
function createFormStoreValidations(params, metadata) {
let fieldValidationEvents = {
submit: true,
blur: true
};
let isFieldHavingPassedValidations = false;
let fieldValidationEventKey;
const validations = {};
for (const fieldName of metadata.validatedFieldsNames) {
const fieldValidationsHandler = params.validationsHandlers?.[fieldName];
validations[fieldName] = {
handler: !fieldValidationsHandler ? void 0 : isZodValidator(fieldValidationsHandler) ? (value) => fieldValidationsHandler.parse(value) : fieldValidationsHandler,
currentDirtyEventsCounter: 0,
failedAttempts: 0,
passedAttempts: 0,
events: {
// mount: { },
blur: {
failedAttempts: 0,
passedAttempts: 0,
isActive: params.validationEvents?.blur ?? true,
isDirty: false,
error: null
},
change: {
failedAttempts: 0,
passedAttempts: 0,
isActive: params.validationEvents?.change ?? false,
isDirty: false,
error: null
},
submit: {
failedAttempts: 0,
passedAttempts: 0,
isActive: params.validationEvents?.submit ?? true,
isDirty: false,
error: null
}
},
isDirty: false,
metadata: { name: fieldName }
};
if (params.validationEvents) {
isFieldHavingPassedValidations = true;
fieldValidationEvents = {
...fieldValidationEvents,
...params.validationEvents
};
}
if (isFieldHavingPassedValidations) {
for (fieldValidationEventKey in fieldValidationEvents) {
validations[fieldName].events[fieldValidationEventKey].isActive = !!typeof fieldValidationEvents[fieldValidationEventKey];
}
}
}
return validations;
}
function createFormStoreFields(params, baseId, metadata) {
const fields = {};
for (const fieldName of metadata.fieldsNames) {
fields[fieldName] = new FormStoreField({
value: params.initialValues[fieldName],
valueFromFieldToStore: params.valuesFromFieldsToStore?.[fieldName] ? params.valuesFromFieldsToStore[fieldName] : void 0,
valueFromStoreToField: params.valuesFromStoreToFields?.[fieldName] ? params.valuesFromStoreToFields[fieldName] : void 0,
id: `${baseId}field-${String(fieldName)}`,
metadata: {
name: fieldName,
initialValue: params.initialValues[fieldName]
}
});
}
return fields;
}
function _setFieldError(params) {
return function(currentState) {
if (!currentState.validations[params.name].events[params.validationEvent].isActive)
return currentState;
let currentDirtyFieldsCounter = currentState.currentDirtyFieldsCounter;
const validation = {
...currentState.validations[params.name]
};
if (params.message) {
validation.failedAttempts++;
validation.events[params.validationEvent].failedAttempts++;
if (!validation.isDirty) {
validation.currentDirtyEventsCounter++;
if (validation.currentDirtyEventsCounter > 0) {
currentDirtyFieldsCounter++;
}
}
validation.events[params.validationEvent].error = {
message: params.message
};
validation.error = { message: params.message };
validation.events[params.validationEvent].isDirty = true;
validation.isDirty = true;
} else {
validation.passedAttempts++;
validation.events[params.validationEvent].passedAttempts++;
if (validation.isDirty) {
validation.currentDirtyEventsCounter--;
if (validation.currentDirtyEventsCounter === 0) {
currentDirtyFieldsCounter--;
}
}
validation.events[params.validationEvent].error = null;
validation.error = null;
validation.events[params.validationEvent].isDirty = false;
validation.isDirty = false;
}
currentState.currentDirtyFieldsCounter = currentDirtyFieldsCounter;
currentState.isDirty = currentDirtyFieldsCounter > 0;
currentState.validations = {
...currentState.validations,
[params.name]: validation
};
return currentState;
};
}
function _setFieldValue(name, valueOrUpdater) {
return function(currentState) {
const field = currentState.fields[name];
field.value = typeof valueOrUpdater === "function" ? valueOrUpdater(field.value) : valueOrUpdater;
return {
...currentState,
fields: {
...currentState.fields,
[name]: field
}
};
};
}
var itemsToResetDefaults = {
fields: true,
validations: true,
submit: false,
focus: true
};
function createFormStoreBuilder(params) {
const baseId = params.baseId ? `${params.baseId}-` : "";
const metadata = createFormStoreMetadata(params, baseId);
const fields = createFormStoreFields(params, baseId, metadata);
const validations = createFormStoreValidations(params, metadata);
return (set, get) => {
return {
baseId,
metadata,
validations,
fields,
id: `${baseId}form`,
isDirty: false,
submit: {
counter: 0,
passedAttempts: 0,
failedAttempts: 0,
errorMessage: null,
isActive: false
},
focus: { isActive: false, field: null },
currentDirtyFieldsCounter: 0,
getFieldValues() {
const currentState = get();
const fieldsValues = {};
let fieldName;
for (fieldName in currentState.fields) {
fieldsValues[fieldName] = currentState.fields[fieldName].value;
}
return fieldsValues;
},
setSubmitState(valueOrUpdater) {
set(function(currentState) {
return {
// ...currentState,
submit: {
...currentState.submit,
...typeof valueOrUpdater === "function" ? valueOrUpdater(currentState.submit) : valueOrUpdater
}
};
});
},
setFocusState(fieldName, validationName, isActive) {
set(function(currentState) {
let _currentState = currentState;
if (!isActive && _currentState.validations[validationName].events.blur.isActive) {
try {
_currentState.validations[validationName].handler(
validationName && fieldName !== validationName ? _currentState.getFieldValues() : _currentState.fields[fieldName].value,
"blur"
);
_currentState = _setFieldError(
{
name: validationName,
message: null,
validationEvent: "blur"
}
)(_currentState);
} catch (error) {
const message = _currentState.errorFormatter(error, "blur");
_currentState = _setFieldError(
{
name: validationName,
message,
validationEvent: "blur"
}
)(_currentState);
}
if (_currentState.focus.isActive && _currentState.focus.field.name !== fieldName)
return _currentState;
}
return {
..._currentState,
focus: isActive ? {
isActive: true,
field: {
name: fieldName,
id: _currentState.fields[fieldName].id
}
} : { isActive: false, field: null }
};
});
},
resetFormStore: function(itemsToReset = itemsToResetDefaults) {
return set(function(currentState) {
const fields2 = currentState.fields;
const validations2 = currentState.validations;
let isDirty = currentState.isDirty;
let submit = currentState.submit;
let focus = currentState.focus;
if (itemsToReset.fields) {
let fieldName;
for (fieldName in fields2) {
fields2[fieldName].value = fields2[fieldName].metadata.initialValue;
}
}
if (itemsToReset.validations) {
for (const key in validations2) {
validations2[key].failedAttempts = 0;
validations2[key].passedAttempts = 0;
validations2[key].isDirty = false;
validations2[key].error = null;
let eventKey;
for (eventKey in validations2[key].events) {
validations2[key].events[eventKey].failedAttempts = 0;
validations2[key].events[eventKey].passedAttempts = 0;
validations2[key].events[eventKey].isDirty = false;
validations2[key].events[eventKey].error = null;
}
}
isDirty = false;
}
if (itemsToReset.submit) {
submit = {
counter: 0,
passedAttempts: 0,
failedAttempts: 0,
errorMessage: null,
isActive: false
};
}
if (itemsToReset.focus) {
focus = {
isActive: false,
field: null
};
}
return {
// ...currentState,
fields: fields2,
validations: validations2,
isDirty,
submit,
focus
};
});
},
setFieldValue(name, value) {
return set(_setFieldValue(name, value));
},
setFieldError(params2) {
set(_setFieldError(params2));
},
errorFormatter: params.errorFormatter ?? errorFormatter,
handleInputChange(name, valueOrUpdater, validationName) {
let currentState = get();
const field = currentState.fields[name];
const _value = typeof valueOrUpdater === "function" ? valueOrUpdater(field.value) : valueOrUpdater;
const value = field.valueFromFieldToStore ? field.valueFromFieldToStore(_value) : _value;
const _validationName = validationName ? validationName : (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
currentState.metadata.referencedValidatedFieldsMap[name] ? name : void 0
);
const setFieldValue = _setFieldValue;
const setFieldError = _setFieldError;
if (_validationName && currentState.validations[_validationName].events["change"].isActive) {
try {
currentState = setFieldValue(
name,
currentState.validations[_validationName].handler(
validationName && // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
validationName !== name ? currentState.getFieldValues() : value,
"change"
)
)(currentState);
currentState = setFieldError({
name: _validationName,
message: null,
validationEvent: "change"
})(currentState);
} catch (error) {
currentState = setFieldError({
name: _validationName,
message: currentState.errorFormatter(error, "change"),
validationEvent: "change"
})(currentState);
currentState = setFieldValue(name, value)(currentState);
}
} else {
currentState = setFieldValue(name, value)(currentState);
}
set(currentState);
},
getFieldEventsListeners(name, validationName) {
const currentState = get();
const _validationName = validationName ?? name;
return {
onChange: (event) => {
currentState.handleInputChange(name, event.target.value);
},
onFocus: () => {
currentState.setFocusState(
name,
_validationName,
true
);
},
onBlur: () => {
currentState.setFocusState(
name,
_validationName,
false
);
}
};
},
handleSubmit(cb) {
return async function(event) {
event.preventDefault();
const currentState = get();
currentState.setSubmitState({ isActive: true });
const metadata2 = currentState.metadata;
const fields2 = currentState.fields;
const validations2 = currentState.validations;
const values = {};
const validatedValues = {};
const errors = {};
let hasError = false;
let fieldName;
for (fieldName in fields2) {
values[fieldName] = fields2[fieldName].value;
try {
const validationSchema = fieldName in metadata2.referencedValidatedFieldsMap && validations2[fieldName].handler;
if (typeof validationSchema !== "function" || !validations2[fieldName].events.submit.isActive) {
continue;
}
validatedValues[fieldName] = validationSchema(
fields2[fieldName].value,
"submit"
);
errors[fieldName] = {
name: fieldName,
message: null,
validationEvent: "submit"
};
} catch (error) {
errors[fieldName] = {
name: fieldName,
message: currentState.errorFormatter(error, "submit"),
validationEvent: "submit"
};
}
}
let manualFieldName;
for (manualFieldName of metadata2.manualValidatedFields) {
try {
const validationSchema = currentState.validations[manualFieldName].handler;
if (typeof validationSchema !== "function") {
continue;
}
validatedValues[manualFieldName] = validationSchema(
values,
"submit"
);
errors[manualFieldName] = {
name: manualFieldName,
message: null,
validationEvent: "submit"
};
} catch (error) {
errors[manualFieldName] = {
name: manualFieldName,
message: currentState.errorFormatter(error, "submit"),
validationEvent: "submit"
};
}
}
let _currentState = get();
let errorKey;
for (errorKey in errors) {
const errorObj = errors[errorKey];
_currentState = _setFieldError(
errors[errorKey]
)(_currentState);
if (typeof errorObj.message !== "string")
continue;
hasError = true;
}
if (!hasError) {
try {
await cb({
event,
values,
validatedValues,
hasError,
errors
});
currentState.setSubmitState((prev) => ({
isActive: false,
counter: prev.counter + 1,
passedAttempts: prev.counter + 1,
errorMessage: null
}));
} catch (error) {
currentState.setSubmitState((prev) => ({
isActive: false,
counter: prev.counter + 1,
failedAttempts: prev.counter + 1,
errorMessage: currentState.errorFormatter(error, "submit")
}));
}
} else {
set(_currentState);
currentState.setSubmitState((prev) => ({
isActive: false,
counter: prev.counter + 1,
failedAttempts: prev.counter + 1,
errorMessage: null
}));
}
};
}
};
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createFormStoreBuilder,
errorFormatter,
fvh,
handleCreateFormStore,
isZodError,
isZodValidator,
useCreateFormStore
});
//# sourceMappingURL=index.js.map