如何在单页面触发「离开网站?」提示——Turbo & Stimulus JS实现
实现特定表单页面的原生离开提示(匹配StackOverflow效果)
需求
在/posts/new或/posts/xxx/edit路径的表单页面,用户离开前触发浏览器原生的「离开网站?」提示,效果与StackOverflow一致。
当前问题
- 页面加载后,离开任意页面都会弹出提示,无法仅限制在目标表单页面
- 通过Turbo触发的提示是自定义弹窗,不是浏览器原生样式
期望弹窗样式:
当前Turbo触发的弹窗样式:
当前代码
Stimulus控制器代码
// javascript/controllers/unsaved_changes_controller.js import { Controller } from "@hotwired/stimulus" const alert_message = "Leave site? Changes you made may not be saved." export default class extends Controller { connect() { window.addEventListener("beforeunload", (event) => { leavingPage(event); }); document.addEventListener('turbo:before-visit', (event) => { leavingPage(event); }); } } function leavingPage(event) { console.log("leavingPage") if (event.type == "turbo:before-visit") { if (!window.confirm(alert_message)) { event.preventDefault() } } else { event.returnValue = alert_message; return event.returnValue; } }
视图代码
<form ... data-controller="unsaved-changes">
解决方案
核心问题分析
- 全局事件监听未在页面销毁时移除,导致离开表单页面后提示仍生效
- Turbo单页跳转不会触发浏览器原生
beforeunload事件,只能用自定义弹窗
完整修改后的控制器代码
// javascript/controllers/unsaved_changes_controller.js import { Controller } from "@hotwired/stimulus" const alert_message = "Leave site? Changes you made may not be saved." export default class extends Controller { connect() { // 保存表单初始值,用于判断是否有未保存更改 this.initialFormData = new FormData(this.element); // 保存事件处理函数引用,方便销毁时移除 this.beforeUnloadHandler = (event) => this.leavingPage(event); this.turboBeforeVisitHandler = (event) => this.leavingPage(event); window.addEventListener("beforeunload", this.beforeUnloadHandler); document.addEventListener('turbo:before-visit', this.turboBeforeVisitHandler); } disconnect() { // 移除全局事件监听,确保离开表单页面后不再触发提示 window.removeEventListener("beforeunload", this.beforeUnloadHandler); document.removeEventListener('turbo:before-visit', this.turboBeforeVisitHandler); } leavingPage(event) { // 只有表单有未保存更改时才触发提示 if (!this.hasUnsavedChanges()) return; if (event.type === "turbo:before-visit") { // 阻止Turbo的单页跳转,改用原生跳转触发beforeunload提示 event.preventDefault(); window.location.href = event.detail.url; } else { // beforeunload事件:设置返回值触发原生提示 event.returnValue = alert_message; return event.returnValue; } } // 判断表单是否有未保存更改 hasUnsavedChanges() { const currentFormData = new FormData(this.element); // 比较初始值和当前值 for (const [key, initialValue] of this.initialFormData.entries()) { const currentValue = currentFormData.get(key); if (currentValue !== initialValue) { return true; } } // 检查是否有新增的文件(处理文件上传场景) for (const [key] of currentFormData.entries()) { if (!this.initialFormData.has(key)) { return true; } } return false; } }
额外优化:仅在目标路径加载控制器
通过视图条件判断,仅在目标路径的表单上添加控制器,避免不必要的初始化:
<% if request.path.match?(/\/posts\/new$|\/posts\/\d+\/edit$/) %> <form ... data-controller="unsaved-changes"> <% else %> <form ...> <% end %>
内容的提问来源于stack exchange,提问作者Kobius
相关产品推荐
相关产品推荐

