Files
michaelschiemer/resources/js/modules/form-handling/FormState.js
Michael Schiemer 55a330b223 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
2025-08-11 20:13:26 +02:00

256 lines
7.0 KiB
JavaScript

import { Logger } from '../../core/logger.js';
export class FormState {
constructor(form) {
this.form = form;
this.pristineValues = new Map();
this.touchedFields = new Set();
this.dirtyFields = new Set();
this.init();
}
static create(form) {
return new FormState(form);
}
init() {
// Store initial values
this.captureInitialValues();
// Track field interactions
this.bindEvents();
Logger.info(`[FormState] Initialized for form: ${this.form.id || 'unnamed'}`);
}
captureInitialValues() {
const fields = this.form.querySelectorAll('input, textarea, select');
fields.forEach(field => {
let value;
switch (field.type) {
case 'checkbox':
case 'radio':
value = field.checked;
break;
default:
value = field.value;
}
this.pristineValues.set(field.name, value);
});
}
bindEvents() {
const fields = this.form.querySelectorAll('input, textarea, select');
fields.forEach(field => {
// Skip hidden fields for interaction tracking (they can't be touched/interacted with)
if (field.type === 'hidden') return;
// Mark field as touched on first interaction
field.addEventListener('focus', () => {
this.markAsTouched(field.name);
});
// Track changes for dirty state
field.addEventListener('input', () => {
this.checkDirtyState(field);
});
field.addEventListener('change', () => {
this.checkDirtyState(field);
});
});
}
markAsTouched(fieldName) {
this.touchedFields.add(fieldName);
this.updateFormClasses();
}
checkDirtyState(field) {
const fieldName = field.name;
const currentValue = this.getFieldValue(field);
const pristineValue = this.pristineValues.get(fieldName);
if (currentValue !== pristineValue) {
this.dirtyFields.add(fieldName);
} else {
this.dirtyFields.delete(fieldName);
}
this.updateFormClasses();
}
getFieldValue(field) {
switch (field.type) {
case 'checkbox':
case 'radio':
return field.checked;
default:
return field.value;
}
}
updateFormClasses() {
// Update form-level classes based on state
if (this.isDirty()) {
this.form.classList.add('form-dirty');
this.form.classList.remove('form-pristine');
} else {
this.form.classList.add('form-pristine');
this.form.classList.remove('form-dirty');
}
if (this.hasTouchedFields()) {
this.form.classList.add('form-touched');
} else {
this.form.classList.remove('form-touched');
}
}
// State check methods
isPristine() {
return this.dirtyFields.size === 0;
}
isDirty() {
return this.dirtyFields.size > 0;
}
hasTouchedFields() {
return this.touchedFields.size > 0;
}
isFieldTouched(fieldName) {
return this.touchedFields.has(fieldName);
}
isFieldDirty(fieldName) {
return this.dirtyFields.has(fieldName);
}
isFieldPristine(fieldName) {
return !this.dirtyFields.has(fieldName);
}
// Get field states
getFieldState(fieldName) {
return {
pristine: this.isFieldPristine(fieldName),
dirty: this.isFieldDirty(fieldName),
touched: this.isFieldTouched(fieldName),
pristineValue: this.pristineValues.get(fieldName),
currentValue: this.getCurrentFieldValue(fieldName)
};
}
getCurrentFieldValue(fieldName) {
const field = this.form.querySelector(`[name="${fieldName}"]`);
return field ? this.getFieldValue(field) : undefined;
}
// Form-level state
getFormState() {
return {
pristine: this.isPristine(),
dirty: this.isDirty(),
touched: this.hasTouchedFields(),
dirtyFields: Array.from(this.dirtyFields),
touchedFields: Array.from(this.touchedFields),
totalFields: this.pristineValues.size
};
}
// Reset methods
reset() {
this.touchedFields.clear();
this.dirtyFields.clear();
// Re-capture values after form reset
setTimeout(() => {
this.captureInitialValues();
this.updateFormClasses();
}, 0);
Logger.info('[FormState] State reset');
}
resetField(fieldName) {
this.touchedFields.delete(fieldName);
this.dirtyFields.delete(fieldName);
// Reset field to pristine value
const field = this.form.querySelector(`[name="${fieldName}"]`);
const pristineValue = this.pristineValues.get(fieldName);
if (field && pristineValue !== undefined) {
switch (field.type) {
case 'checkbox':
case 'radio':
field.checked = pristineValue;
break;
default:
field.value = pristineValue;
}
}
this.updateFormClasses();
Logger.info(`[FormState] Field "${fieldName}" reset to pristine state`);
}
// Event handling for external state changes
triggerStateEvent(eventName, detail = {}) {
const event = new CustomEvent(eventName, {
detail: {
...detail,
formState: this.getFormState()
},
bubbles: true,
cancelable: true
});
this.form.dispatchEvent(event);
}
// Utility methods
hasChanges() {
return this.isDirty();
}
getChangedFields() {
const changes = {};
this.dirtyFields.forEach(fieldName => {
changes[fieldName] = {
pristineValue: this.pristineValues.get(fieldName),
currentValue: this.getCurrentFieldValue(fieldName)
};
});
return changes;
}
// Warning for unsaved changes
enableUnsavedChangesWarning() {
window.addEventListener('beforeunload', (e) => {
if (this.isDirty()) {
e.preventDefault();
e.returnValue = 'Sie haben ungespeicherte Änderungen. Möchten Sie die Seite wirklich verlassen?';
return e.returnValue;
}
});
}
destroy() {
this.pristineValues.clear();
this.touchedFields.clear();
this.dirtyFields.clear();
Logger.info('[FormState] Destroyed');
}
}