@jay-js/system
Version:
A powerful and flexible TypeScript library for UI, state management, lazy loading, routing and managing draggable elements in modern web applications.
454 lines • 16.9 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { state } from "../../state/index.js";
/**
* Creates a form management system with validation, state tracking, and DOM integration.
*
* @template T - The type of form values being managed
* @param {TUseFormOptions<T>} options - Form configuration options
* @param {T} options.defaultValues - Initial values for the form fields
* @param {TResolver<T>} [options.resolver] - Optional validation resolver function
* @param {number} [options.debounceMs=300] - Debounce time for validation in milliseconds
* @returns {TUseForm<T>} Form management interface with methods for registration, state access, and event handling
*
* @example
* // Basic form with Zod validation and custom debounce
* const form = handleForm({
* defaultValues: { email: '', password: '', remember: false },
* resolver: zodResolver(loginSchema),
* debounceMs: 500 // Custom debounce time
* });
*
* // Register an input element
* const emailInput = document.querySelector('#email');
* Object.assign(emailInput, form.register('email'));
*
* // Register a checkbox element
* const rememberInput = document.querySelector('#remember');
* Object.assign(rememberInput, form.register('remember', { type: 'checkbox' }));
*
* // Show validation errors
* const errorElement = document.querySelector('#email-error');
* errorElement.appendChild(form.formState.errors('email'));
*
* // Handle form submission
* form.onSubmit((data, event) => {
* console.log('Form submitted:', data);
* });
*
* // Reset form
* form.formState.reset();
*
* // Cleanup when component unmounts
* form.destroy();
*/
export function handleForm({ defaultValues, resolver, debounceMs = 300 }) {
const formErrors = state({ errors: [] });
const formValues = state(defaultValues);
// Cache for DOM elements using WeakMap for automatic garbage collection
const elementCache = new Map();
// Debounce timers for validation
const debounceTimers = new Map();
// Event listeners cleanup registry
const eventListeners = new Set();
// MutationObserver for automatic cleanup when elements are removed
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.removedNodes.forEach((node) => {
if (node instanceof HTMLElement) {
const name = node.getAttribute("name");
if (name && elementCache.has(name)) {
elementCache.delete(name);
// Clear debounce timer for removed element
const timer = debounceTimers.get(name);
if (timer) {
clearTimeout(timer);
debounceTimers.delete(name);
}
}
}
});
});
});
// Start observing the DOM
if (typeof document !== "undefined") {
observer.observe(document.body, { childList: true, subtree: true });
}
const formState = {
errors: (path) => {
const errorText = document.createTextNode("");
formErrors.sub(path, (error) => {
const errorFound = error.errors.find((err) => err.path === path);
errorText.textContent = "";
if (errorFound === null || errorFound === void 0 ? void 0 : errorFound.message) {
errorText.textContent = errorFound.message;
}
});
return errorText;
},
setValue: (path, value) => {
formValues.set((prev) => {
return Object.assign(Object.assign({}, prev), { [path]: value });
});
// Use cache or search for element and store in cache
const pathStr = String(path);
let fieldElement = elementCache.get(pathStr);
if (!fieldElement || !document.contains(fieldElement)) {
fieldElement = document.querySelector(`[name="${pathStr}"]`);
if (fieldElement) {
elementCache.set(pathStr, fieldElement);
}
}
if (!fieldElement)
return;
if (fieldElement instanceof HTMLInputElement) {
if (fieldElement.type === "checkbox") {
fieldElement.checked = Boolean(value);
}
else if (fieldElement.type === "radio") {
const radioGroup = document.querySelectorAll(`input[type="radio"][name="${String(path)}"]`);
radioGroup.forEach((radio) => {
if (radio instanceof HTMLInputElement) {
radio.checked = radio.value === String(value);
}
});
}
else {
fieldElement.value = String(value);
}
}
else if (fieldElement instanceof HTMLTextAreaElement) {
fieldElement.value = String(value);
}
else if (fieldElement instanceof HTMLSelectElement) {
if (fieldElement.multiple && Array.isArray(value)) {
Array.from(fieldElement.options).forEach((option) => {
option.selected = value.includes(option.value);
});
}
else {
fieldElement.value = String(value);
}
}
},
setValues: (values) => {
formValues.set((prev) => {
return Object.assign(Object.assign({}, prev), values);
});
for (const field in values) {
formState.setValue(field, values[field]);
}
},
getValue: (path) => {
const values = formValues.get();
return values[path];
},
getValues: () => {
return formValues.get();
},
isValid: (path) => __awaiter(this, void 0, void 0, function* () {
try {
if (!resolver) {
return true;
}
const formValuesData = formValues.get();
const result = yield resolver(formValuesData, path);
return checkValidate(result);
}
catch (error) {
return checkValidate(error);
}
}),
getErrors: () => formErrors.get(),
setError: (field, message) => {
formErrors.set((prev) => {
return {
errors: [
...prev.errors,
{
path: field,
message,
},
],
};
});
},
setErrors: (errors) => {
formErrors.set(errors);
},
/**
* Resets the form to default values and clears all errors
*/
reset: () => {
// Restaura valores padrão
formValues.set(defaultValues);
// Limpa erros
formErrors.set({ errors: [] });
// Atualiza elementos DOM com valores padrão
for (const field in defaultValues) {
formState.setValue(field, defaultValues[field]);
}
},
};
/**
* Updates a form field value in the internal state
*
* @param {string} field - The field name to update
* @param {any} value - The new value for the field
* @private
*/
function privateSetValue(field, value) {
formValues.set((prev) => {
return Object.assign(Object.assign({}, prev), { [field]: value });
});
}
/**
* Creates a debounced validation function for a specific field
*
* @param {string} field - The field name to validate
* @returns {Function} Debounced validation function
* @private
*/
function createDebouncedValidation(field) {
return () => {
const timer = debounceTimers.get(field);
if (timer) {
clearTimeout(timer);
}
const newTimer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
try {
if (!resolver) {
return;
}
const formValuesData = formValues.get();
const result = yield resolver(formValuesData, field);
validateResult(result);
}
catch (error) {
validateResult(error);
}
finally {
debounceTimers.delete(field);
}
}), debounceMs);
debounceTimers.set(field, newTimer);
};
}
/**
* Subscribes to error state changes
*
* @param {Function} callback - Function called when validation errors change
*/
function onErrors(callback) {
formErrors.sub("onErrors", callback);
// Store callback reference for cleanup
const cleanup = () => {
// The State system will handle cleanup automatically
// when the form is destroyed
};
eventListeners.add(cleanup);
return cleanup;
}
/**
* Extracts value from form element based on its type
*
* @param {HTMLElement} element - The DOM element to extract value from
* @returns {any} The value in the appropriate type for the element
*/
function getElementValue(element) {
if (element instanceof HTMLInputElement) {
if (element.type === "checkbox") {
return element.checked;
}
else if (element.type === "radio") {
if (element.checked) {
return element.value;
}
return undefined;
}
else if (element.type === "file") {
return element.files;
}
return element.value;
}
else if (element instanceof HTMLSelectElement) {
if (element.multiple) {
return Array.from(element.selectedOptions).map((option) => option.value);
}
return element.value;
}
else if (element instanceof HTMLTextAreaElement) {
return element.value;
}
return undefined;
}
/**
* Handles input/change events from form elements
*
* @param {Event} ev - The DOM event
* @param {TRegisterOptions} options - Optional processing options
* @private
*/
function onChangeValue(ev_1) {
return __awaiter(this, arguments, void 0, function* (ev, options = {}) {
const element = ev.target;
if (element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element instanceof HTMLSelectElement) {
const field = element.getAttribute("name");
let value = getElementValue(element);
if (value === undefined)
return;
if (options.beforeChange && typeof value === "string") {
value = options.beforeChange(value, ev);
if (value === undefined) {
return;
}
if (element instanceof HTMLInputElement && element.type !== "checkbox" && element.type !== "radio") {
element.value = value;
}
else if (element instanceof HTMLTextAreaElement) {
element.value = value;
}
}
privateSetValue(field, value);
// Use debounced validation instead of immediate validation
if (resolver) {
const debouncedValidate = createDebouncedValidation(field);
debouncedValidate();
}
}
});
}
/**
* Process validation results and update error state
*
* @param {TFormValidateResult} result - The validation result
* @returns {boolean} True if validation passed, false otherwise
* @private
*/
function validateResult(result) {
if (result.errors && result.errors.length > 0) {
formErrors.set({ errors: result.errors });
return false;
}
formErrors.set({ errors: [] });
return true;
}
/**
* Checks if a validation result contains errors
*
* @param {TFormValidateResult} result - The validation result to check
* @returns {boolean} True if no errors, false otherwise
* @private
*/
function checkValidate(result) {
if (result.errors && result.errors.length > 0) {
return false;
}
return true;
}
/**
* Registers a form field to connect it with the form management system
*
* @param {keyof T} path - The field name/path in the form values object
* @param {TRegisterOptions} options - Optional field registration options
* @returns {TRegister} Props to apply to the HTML element
*/
function register(path, options = {}) {
const value = options.value ? String(options.value) : defaultValues[path];
if (typeof value === "boolean") {
return {
name: path,
onchange: (ev) => onChangeValue(ev, options),
checked: Boolean(value),
};
}
return {
name: path,
onchange: (ev) => onChangeValue(ev, options),
oninput: (ev) => onChangeValue(ev, options),
value: String(value),
};
}
/**
* Subscribes to form value changes
*
* @param {Function} callback - Function called when form values change
*/
function onChange(callback) {
formValues.sub("onChange", (data) => {
callback(data, formErrors.get());
});
// Store callback reference for cleanup
const cleanup = () => {
// The State system will handle cleanup automatically
// when the form is destroyed
};
eventListeners.add(cleanup);
return cleanup;
}
/**
* Creates a form submission handler that validates before calling the callback
*
* @param {Function} callback - Function called on successful form submission
* @returns {Function} Event handler for the form's submit event
*/
function onSubmit(callback) {
return (ev) => __awaiter(this, void 0, void 0, function* () {
ev.preventDefault();
const formValuesData = formValues.get();
try {
if (!resolver) {
callback(formValuesData, ev);
return;
}
const result = yield resolver(formValuesData);
const validated = validateResult(result);
if (validated) {
callback(formValuesData, ev);
}
}
catch (error) {
validateResult(error);
}
});
}
/**
* Cleans up resources and stops DOM observation
*/
function destroy() {
// Disconnect DOM observer
observer.disconnect();
// Clear element cache
elementCache.clear();
// Clear all debounce timers
for (const timer of debounceTimers.values()) {
clearTimeout(timer);
}
debounceTimers.clear();
// Cleanup all event listeners
eventListeners.forEach((unsubscribe) => {
if (typeof unsubscribe === "function") {
unsubscribe();
}
});
eventListeners.clear();
}
return {
formState,
register,
onChange,
onSubmit,
onErrors,
destroy,
};
}
//# sourceMappingURL=handle-form.js.map