import { Logger } from '../../core/logger.js'; import { FormValidator } from './FormValidator.js'; import { FormState } from './FormState.js'; export class FormHandler { constructor(form, options = {}) { this.form = form; this.options = { validateOnSubmit: true, validateOnBlur: false, validateOnInput: false, showInlineErrors: true, preventSubmitOnError: true, submitMethod: 'POST', ajaxSubmit: true, ...options }; this.validator = FormValidator.create(form); this.state = FormState.create(form); this.isSubmitting = false; this.init(); } static create(form, options = {}) { return new FormHandler(form, options); } init() { this.bindEvents(); this.setupErrorDisplay(); // Mark form as enhanced this.form.setAttribute('data-enhanced', 'true'); Logger.info(`[FormHandler] Initialized for form: ${this.form.id || 'unnamed'}`); } bindEvents() { // Submit event this.form.addEventListener('submit', (e) => this.handleSubmit(e)); // Field validation events if (this.options.validateOnBlur || this.options.validateOnInput) { const fields = this.form.querySelectorAll('input, textarea, select'); fields.forEach(field => { if (this.options.validateOnBlur) { field.addEventListener('blur', () => this.validateSingleField(field)); } if (this.options.validateOnInput) { field.addEventListener('input', () => this.validateSingleField(field)); } }); } } async handleSubmit(event) { if (this.isSubmitting) { event.preventDefault(); return; } // Validate if enabled if (this.options.validateOnSubmit) { const isValid = this.validator.validate(); if (!isValid) { event.preventDefault(); this.displayErrors(); return; } } // AJAX submission if (this.options.ajaxSubmit) { event.preventDefault(); await this.submitViaAjax(); } // If not AJAX, let the form submit naturally } async submitViaAjax() { try { this.setSubmitState(true); this.clearErrors(); const formData = new FormData(this.form); const url = this.form.action || window.location.href; const method = this.form.method || this.options.submitMethod; const response = await fetch(url, { method: method.toUpperCase(), body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); const data = await this.parseResponse(response); if (response.ok) { this.handleSuccess(data); } else { this.handleError(data); } } catch (error) { Logger.error('[FormHandler] Submit error:', error); this.handleError({ message: 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', errors: {} }); } finally { this.setSubmitState(false); } } async parseResponse(response) { const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { return await response.json(); } const text = await response.text(); // Try to parse as JSON, fallback to text try { return JSON.parse(text); } catch { return { message: text }; } } handleSuccess(data) { Logger.info('[FormHandler] Form submitted successfully'); // Clear form if configured if (data.clearForm !== false) { this.form.reset(); this.state.reset(); } this.showMessage(data.message || 'Formular erfolgreich gesendet!', 'success'); // Call success callback this.triggerEvent('form:success', { data }); } handleError(data) { Logger.warn('[FormHandler] Form submission error:', data); // Handle field-specific errors if (data.errors && typeof data.errors === 'object') { for (const [fieldName, errorMessage] of Object.entries(data.errors)) { this.validator.errors.set(fieldName, errorMessage); } this.displayErrors(); } // Show general error message this.showMessage(data.message || 'Ein Fehler ist aufgetreten.', 'error'); // Call error callback this.triggerEvent('form:error', { data }); } validateSingleField(field) { this.validator.errors.delete(field.name); this.validator.validateField(field); this.displayFieldError(field); } displayErrors() { if (!this.options.showInlineErrors) return; const errors = this.validator.getErrors(); for (const [fieldName, message] of Object.entries(errors)) { const field = this.form.querySelector(`[name="${fieldName}"]`); if (field) { this.displayFieldError(field, message); } } } displayFieldError(field, message = null) { const errorMessage = message || this.validator.getFieldError(field.name); const errorElement = this.getOrCreateErrorElement(field); if (errorMessage) { errorElement.textContent = errorMessage; errorElement.style.display = 'block'; field.classList.add('error'); field.setAttribute('aria-invalid', 'true'); field.setAttribute('aria-describedby', errorElement.id); } else { errorElement.textContent = ''; errorElement.style.display = 'none'; field.classList.remove('error'); field.removeAttribute('aria-invalid'); field.removeAttribute('aria-describedby'); } } getOrCreateErrorElement(field) { const errorId = `error-${field.name}`; let errorElement = document.getElementById(errorId); if (!errorElement) { errorElement = document.createElement('div'); errorElement.id = errorId; errorElement.className = 'form-error'; errorElement.setAttribute('role', 'alert'); errorElement.style.display = 'none'; // Insert after field or field container const container = field.closest('.form-group') || field.parentElement; container.appendChild(errorElement); } return errorElement; } setupErrorDisplay() { // Add CSS for error states if not present if (!document.getElementById('form-handler-styles')) { const styles = document.createElement('style'); styles.id = 'form-handler-styles'; styles.textContent = ` .form-error { color: #dc2626; font-size: 0.875rem; margin-top: 0.25rem; } input.error, textarea.error, select.error { border-color: #dc2626; box-shadow: 0 0 0 1px #dc2626; } .form-message { padding: 0.75rem; border-radius: 0.375rem; margin: 1rem 0; } .form-message.success { background-color: #dcfce7; color: #166534; border: 1px solid #bbf7d0; } .form-message.error { background-color: #fef2f2; color: #dc2626; border: 1px solid #fecaca; } `; document.head.appendChild(styles); } } showMessage(message, type = 'info') { let messageContainer = this.form.querySelector('.form-messages'); if (!messageContainer) { messageContainer = document.createElement('div'); messageContainer.className = 'form-messages'; this.form.prepend(messageContainer); } const messageElement = document.createElement('div'); messageElement.className = `form-message ${type}`; messageElement.textContent = message; messageElement.setAttribute('role', type === 'error' ? 'alert' : 'status'); messageContainer.innerHTML = ''; messageContainer.appendChild(messageElement); // Auto-hide success messages after 5 seconds if (type === 'success') { setTimeout(() => { if (messageElement.parentElement) { messageElement.remove(); } }, 5000); } } clearErrors() { this.validator.clearErrors(); // Clear visual error states const errorFields = this.form.querySelectorAll('.error'); errorFields.forEach(field => { field.classList.remove('error'); field.removeAttribute('aria-invalid'); field.removeAttribute('aria-describedby'); }); // Hide error messages const errorElements = this.form.querySelectorAll('.form-error'); errorElements.forEach(element => { element.style.display = 'none'; element.textContent = ''; }); // Clear general messages const messageContainer = this.form.querySelector('.form-messages'); if (messageContainer) { messageContainer.innerHTML = ''; } } setSubmitState(isSubmitting) { this.isSubmitting = isSubmitting; const submitButtons = this.form.querySelectorAll('button[type="submit"], input[type="submit"]'); submitButtons.forEach(button => { button.disabled = isSubmitting; if (isSubmitting) { button.setAttribute('data-original-text', button.textContent); button.textContent = 'Wird gesendet...'; } else { const originalText = button.getAttribute('data-original-text'); if (originalText) { button.textContent = originalText; button.removeAttribute('data-original-text'); } } }); } triggerEvent(eventName, detail = {}) { const event = new CustomEvent(eventName, { detail, bubbles: true, cancelable: true }); this.form.dispatchEvent(event); } destroy() { // Remove event listeners and clean up this.form.removeAttribute('data-enhanced'); Logger.info('[FormHandler] Destroyed'); } }