beforeunload in Phoenix LiveView
Usually beforeunload is pretty straightforward:
function formHasChanged() {
window.addEventListener(`beforeunload`, (e) => {
e.preventDefault()
})
}Now just call formHasChanged once form field values have changed.
However, Phoenix LiveView is a different case because of its pushState-based navigation nature, which means we need to polyfill this in addition to the browser-native implementation above:
const message = `You have unsaved changes. Are you sure you want to leave?`
function handleWinBeforeunload(e) {
e.preventDefault()
}
function handleDocClick(e) {
const link = e.target.closest(`a[data-phx-link]`)
if (!link) return
if (!confirm(message)) {
e.preventDefault()
e.stopImmediatePropagation()
} else stop()
}
function start() {
window.addEventListener(`beforeunload`, handleWinBeforeunload)
document.addEventListener(`click`, handleDocClick, true)
}
function stop() {
window.removeEventListener(`beforeunload`, handleWinBeforeunload)
document.removeEventListener(`click`, handleDocClick, true)
}
window.addEventListener(`phx:start-beforeunload`, start)
window.addEventListener(`phx:stop-beforeunload`, stop)Everything starts with phx:*-beforeunload listeners that can be initiated from either server or client side. That leads to browser-native beforeunload handling to cover typical navigation cases, and handleDocClick that handles Phoenix’s link clicks and fires up a confirmation dialog with a custom message.
Initiate “before unload” on form changes in a Phoenix module:
def render(assigns) do
~H"""
<.form phx-submit="save" phx-change="validate">
<!-- inputs -->
</form>
"""
end
def handle_event("validate", %{"post" => post_params}, socket) do
changeset =
%Post{}
|> Blog.change_post(post_params)
|> push_event("start-beforeunload", %{})
{:noreply, assign(socket, :changeset, changeset)}
endFinally cancel “before unload” when form values are saved successfully:
def handle_event("save", %{"post" => post_params}, socket) do
case Blog.create_post(post_params) do
{:ok, _post} ->
{:noreply,
socket
|> push_event("stop-beforeunload", %{})
|> assign(:changeset, Blog.change_post(%Post{}))}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
endMay Elixir prolong your life!