You may shudder remembering that long form you were once filling out, only to accidentally click a link, or refresh the page. You lost everything, all your unsaved form entries, and then you had to redo your work and fill out the form again.
Don’t let your customers fall into the same trap. There are a number of ways to prevent the browser from changing the page without a user’s consent. Malware sites sometimes abuse these features, but as long as you’re respectful, your customers will appreciate not losing their information.
How?
The browser fires off a event every time it’s about to change the current page called beforeunload
that we’ll use to prevent losing an unsaved form. We’ll also listen for Turbolinks turbolinks:before-visit
event for page transitions inside our app.
Getting Started
I’m assuming you’ve setup your page to properly pull in Stimulus Controllers. The Stimulus Handbook has instructions if you’re new and need to get started.
HTML
Let’s add the Stimulus annotations to our form. The <form>
tag is going to have the controller, and the window event listening actions. Each of the form inputs will have an action for when their value changes. The form will also have a data attribute called changed
that we’ll update as data is entered into the form, which will be used to decide whether to stop the browser from leaving our page. The form’s submit button will have the final action that we’ll use to hijack the form submission, and stop the browser from preventing the form submission by resetting the changed to false.
I’m working with a simple ActiveRecord Post
model which has a title, author, and text.
<%= form_for @post, html: { data: { controller: 'unsaved-changes', action: 'beforeunload@window->unsaved-changes#leavingPage turbolinks:before-visit@window->unsaved-changes#leavingPage', 'unsaved-changes-changed': "false" } } do |form| %>
<%= form.text_field :title, :placeholder => "Gripping Title...", :data => { :'action' => "change->unsaved-changes#formIsChanged" } %>
<br />
<%= form.text_field :author, :placeholder => "Written By...", :data => { :'action' => "unsaved-changes#formIsChanged" } %>
<br />
<%= form.text_area :text, :placeholder => "The body of the message...", :rows => 4, :data => { :'action' => "unsaved-changes#formIsChanged" } %>
<br />
<%= form.submit :'data-action' => "unsaved-changes#allowFormSubmission" %>
<% end %>
Stimulus Controller
Our controller has the three actions, and uses a changed
data attribute to keep track of whether to prevent the browser from changing the page.
The changed
attribute always starts at false. Every time our input field’s value changes, Stimulus calls the formIsChanged( event )
method, and the unsaved changes controller updates changed
to true
. In order for the form submission to work, the controller reverts changed to false
to prevent the leavingPage( event )
function from displaying the leaving page popup and stopping the form submission.
Every time the browser attempts to leave the page, either with a beforeunload
event or a turbolinks:before-visit
event, the unsaved changes controller checks to see if the form has changed. If changed
is false
, it allows the browser to change page. If changed
is true
, then the controller prevents the page from changing. The turbolinks:before-visit
uses a confirm message to make sure the user want’s to leave the page. The beforeunload
event uses the browser’s native alert to make sure that the user wants to leave the page.
Here is the code for the unsaved_changes_controller.js
:
import { Controller } from "stimulus"
const LEAVING_PAGE_MESSAGE = "You have attempted to leave this page. If you have made any changes to the fields without clicking the Save button, your changes will be lost. Are you sure you want to exit this page?"
export default class extends Controller {
formIsChanged(event) {
this.setChanged("true")
}
leavingPage(event) {
if (this.isFormChanged()) {
if (event.type == "turbolinks:before-visit") {
if (!window.confirm(LEAVING_PAGE_MESSAGE)) {
event.preventDefault()
}
} else {
event.returnValue = LEAVING_PAGE_MESSAGE;
return event.returnValue;
}
}
}
allowFormSubmission(event) {
this.setChanged("false")
}
setChanged(changed) {
this.data.set("changed", changed)
}
isFormChanged() {
return this.data.get("changed") == "true";
}
}
In Closing
You should now have happier customers, who aren’t losing changes in your forms, especially if they accidentally click a link to another page, or refresh the page.
Feel free to leave a comment or question below.
Want To Learn More?
Try out some more of my Stimulus.js Tutorials.
4 comments on “Stimulus.js Tutorial – Don’t Lose Unsaved Form Fields”
[…] Stimulus.js Tutorial – Don’t Lose Unsaved Form Fields […]
I found I needed to wrap the html code in a otherwise the changed attribute is not persisted.
Sorry, the html in my last post was cleansed, so here I go again.
I thought this was very useful, but I needed to wrap the html code in a div with data-controller=”unsaved-changes” otherwise the changed attribute is not persisted.
[…] files are uploaded, causing the whole upload batch to fail. Thankfully, we can setup our form, like in the previous form example, to prevent our customer from accidentally leaving the page while uploading […]