Skip to content

Blogging On Rails

Everything on Rails!

Stimulus.js Tutorial – Don’t Lose Unsaved Form Fields

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.

Make Interactivity Default 

Make your web app interactive now with easy to implement and simple to add HOTWire integrations. 

Enter your email and get a free sample of my HOTWire Tutorials ebook.

We won’t send you spam. Unsubscribe at any time.

4 comments on “Stimulus.js Tutorial – Don’t Lose Unsaved Form Fields”

  1. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *