Skip to content

Commit f5c013e

Browse files
committed
Improve form validation event unbinding
1 parent e31a5ff commit f5c013e

File tree

1 file changed

+63
-2
lines changed

1 file changed

+63
-2
lines changed

app/javascript/controllers/better_together/form_validation_controller.js

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export default class extends Controller {
1212
this._onSubmit = this.handleFormSubmit.bind(this);
1313
this._onSubmitEnd = this.handleSubmitEnd.bind(this);
1414
this._onBeforeVisit = this.handleTurboNavigation.bind(this);
15+
this._onBeforeCache = this.handleBeforeCache?.bind(this) || this.handleBeforeCache.bind(this);
16+
this._onBeforeUnload = this.handleBeforeUnload.bind(this);
1517

1618
this.element.addEventListener("input", this._onInput);
1719

@@ -31,28 +33,37 @@ export default class extends Controller {
3133

3234
// Handle Turbo navigation (unsaved changes warning)
3335
document.addEventListener("turbo:before-visit", this._onBeforeVisit);
36+
// Clean up transient UI before Turbo caches the page
37+
document.addEventListener("turbo:before-cache", this._onBeforeCache);
38+
// Warn on full page unload if there are unsaved changes
39+
window.addEventListener("beforeunload", this._onBeforeUnload);
3440
}
3541

3642
disconnect() {
3743
// Remove all listeners using the cached handler references
3844
if (this._onBeforeVisit) document.removeEventListener("turbo:before-visit", this._onBeforeVisit);
45+
if (this._onBeforeCache) document.removeEventListener("turbo:before-cache", this._onBeforeCache);
3946
if (this._onSubmitEnd) this.element.removeEventListener("turbo:submit-end", this._onSubmitEnd);
4047
if (this._onSubmit) this.element.removeEventListener("submit", this._onSubmit);
4148
if (this._onChange) this.element.removeEventListener("change", this._onChange);
4249
if (this._onInput) this.element.removeEventListener("input", this._onInput);
50+
if (this._onBeforeUnload) window.removeEventListener("beforeunload", this._onBeforeUnload);
4351
}
4452

4553
storeInitialValues() {
4654
const fields = this.element.querySelectorAll("input, select, textarea");
4755
fields.forEach(field => {
48-
this.originalValues.set(field, field.value);
56+
this.originalValues.set(field, this.getFieldValue(field));
4957
});
5058
}
5159

5260
markFieldAsDirty(event) {
5361
const field = event.target;
5462

55-
if (this.originalValues.get(field) !== field.value) {
63+
const original = this.originalValues.get(field);
64+
const current = this.getFieldValue(field);
65+
66+
if (!this.valuesEqual(original, current)) {
5667
this.dirtyFields.add(field);
5768
} else {
5869
this.dirtyFields.delete(field);
@@ -150,6 +161,56 @@ export default class extends Controller {
150161
this.storeInitialValues(); // Re-store current values as "original"
151162
}
152163

164+
// Normalize field value for dirty tracking
165+
getFieldValue(field) {
166+
const tag = field.tagName.toLowerCase();
167+
if (tag === "input") {
168+
const type = (field.getAttribute("type") || "text").toLowerCase();
169+
if (type === "checkbox" || type === "radio") {
170+
return field.checked;
171+
}
172+
return field.value;
173+
}
174+
if (tag === "select") {
175+
if (field.multiple) {
176+
return Array.from(field.options)
177+
.filter(o => o.selected)
178+
.map(o => o.value)
179+
.sort();
180+
}
181+
return field.value;
182+
}
183+
// textarea or others
184+
return field.value;
185+
}
186+
187+
valuesEqual(a, b) {
188+
if (Array.isArray(a) && Array.isArray(b)) {
189+
if (a.length !== b.length) return false;
190+
for (let i = 0; i < a.length; i++) {
191+
if (a[i] !== b[i]) return false;
192+
}
193+
return true;
194+
}
195+
return a === b;
196+
}
197+
198+
// Clear transient UI before Turbo caches the page
199+
handleBeforeCache() {
200+
this.resetValidation();
201+
this.isSubmitting = false;
202+
}
203+
204+
// Show native prompt on hard reload/close if form is dirty
205+
handleBeforeUnload(event) {
206+
if (this.isFormDirty() && !this.isSubmitting) {
207+
event.preventDefault();
208+
event.returnValue = ""; // Required for Chrome to show prompt
209+
return ""; // For older browsers
210+
}
211+
return undefined;
212+
}
213+
153214
showErrorMessage(field) {
154215
const errorMessage = field.nextElementSibling;
155216
if (errorMessage?.classList.contains("invalid-feedback")) {

0 commit comments

Comments
 (0)