Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
353
resources/js/modules/form-handling/FormHandler.js
Normal file
353
resources/js/modules/form-handling/FormHandler.js
Normal file
@@ -0,0 +1,353 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user