Auto Save Form Data in Rails

In this tutorial I’m going to show you how to automatically save form data in Rails. Instead of saving a draft to the database, we’ll simply leverage Stimulus JS to save the data to localStorage. Below is what we’ll be creating.

demoClick to expand

Step 1: Setup

Run the following commands to create a new Rails application. If you’re using an existing Rails application, make sure to install Stimulus by running rails webpacker:install:stimulus.

  1. rails new rails-auto-save-form-data -d=postgresql --webpacker=stimulus
  2. rails db:create
  3. rails db:migrate
  4. rails g scaffold Post title body:text
  5. rails db:migrate

Step 2: Create the Stimulus Controller

Next we’ll need to create a Stimulus Controller to store our Javascript.

  1. touch app/javascript/controllers/auto_save_controller.js

    // app/javascript/controllers/auto_save_controller.js
    import { Controller } from "stimulus";
    
    export default class extends Controller {
      static targets = ["form"];
    }
    
  2. Connect the Controller to the post form.

    <%# app/views/posts/_form.html.erb %>
    <%= form_with(model: post, data: { controller: "auto-save", auto_save_target: "form" }) do |form| %>
      ...
    <% end %>
    

Step 3: Save Form Data to localStorage.

Next we’ll want to save the form data to localStorage each time the form is updated.

  1. Update app/javascript/controllers/auto_save_controller.js with the following code.

    // app/javascript/controllers/auto_save_controller.js
    import { Controller } from "stimulus";
    
    export default class extends Controller {
      static targets = ["form"];
    
      connect() {
        // Create a unique key to store the form data into localStorage.
        // This could be anything as long as it's unique.
        // https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
        this.localStorageKey = window.location;
      }
    
      getFormData() {
        // Construct a set of of key/value pairs representing form fields and their values.
        // https://developer.mozilla.org/en-US/docs/Web/API/FormData
        const form = new FormData(this.formTarget);
        let data = [];
    
        // Loop through each key/value pair.
        // https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries#example
        for (var pair of form.entries()) {
          // We don't want to save the authenticity_token to localStorage since that is generated by Rails.
          // https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
          if (pair[0] != "authenticity_token") {
            data.push([pair[0], pair[1]]);
          }
        }
    
        // Return the key/value pairs as an Object. Each key is a field name, and each value is the field value.
        // https://developer.mozilla.org/en-us/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries
        return Object.fromEntries(data);
      }
    
      saveToLocalStorage() {
        const data = this.getFormData();
        // Save the form data into localStorage. We need to convert the data Object into a String.
        localStorage.setItem(this.localStorageKey, JSON.stringify(data));
      }
    }
    
  2. Add action: "change->auto-save#saveToLocalStorage" to the post form.

    <%# app/views/posts/_form.html.erb %>
    <%= form_with(model: post, data: { controller: "auto-save", auto_save_target: "form", action: "change->auto-save#saveToLocalStorage" }) do |form| %>
      ...
    <% end %>
    

Every time the form changes, the values are saved to localStorage

demoClick to expand

Step 4: Populate the Form with Data from localStorage

Now that we’re saving data into localStorage we need to populate the form with those values.

  1. Update app/javascript/controllers/auto_save_controller.js with the following code.
  
    // app/javascript/controllers/auto_save_controller.js
    import { Controller } from "stimulus";
    
    export default class extends Controller {
      static targets = ["form"];
    
      connect() {
        // Create a unique key to store the form data into localStorage.
        // This could be anything as long as it's unique.
        // https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
        this.localStorageKey = window.location;
    
        // Retrieve data from localStorage when the Controller loads.
        this.setFormData();
      }
    
      getFormData() {
        // Construct a set of of key/value pairs representing form fields and their values.
        // https://developer.mozilla.org/en-US/docs/Web/API/FormData
        const form = new FormData(this.formTarget);
        let data = [];
    
        // Loop through each key/value pair.
        // https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries#example
        for (var pair of form.entries()) {
          // We don't want to save the authenticity_token to localStorage since that is generated by Rails.
          // https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
          if (pair[0] != "authenticity_token") {
            data.push([pair[0], pair[1]]);
          }
        }
    
        // Return the key/value pairs as an Object. Each key is a field name, and each value is the field value.
        // https://developer.mozilla.org/en-us/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries
        return Object.fromEntries(data);
      }
    
      saveToLocalStorage() {
        const data = this.getFormData();
        // Save the form data into localStorage. We need to convert the data Object into a String.
        localStorage.setItem(this.localStorageKey, JSON.stringify(data));
      }
    
      setFormData() {
        // See if there is data stored for this particular form.
        if (localStorage.getItem(this.localStorageKey) != null) {
          // We need to convert the String of data back into an Object.
          const data = JSON.parse(localStorage.getItem(this.localStorageKey));
          // This allows us to have access to this.formTarget in the loop below.
          const form = this.formTarget;
          // Loop through each key/value pair and set the value on the corresponding form field.
          Object.entries(data).forEach((entry) => {
            let name = entry[0];
            let value = entry[1];
            let input = form.querySelector(`[name='${name}']`);
            input && (input.value = value);
          });
        }
      }
    }
  

Step 5: Clear localStorage when Form is Submitted

Finally, we’ll want to clear the form data from localStorage once the form submits. Otherwise, old drafts will continue to be persisted in the form.

  1. Update app/javascript/controllers/auto_save_controller.js with the following code.
  
    // app/javascript/controllers/auto_save_controller.js
    import { Controller } from "stimulus";
    
    export default class extends Controller {
      static targets = ["form"];
    
      connect() {
        // Create a unique key to store the form data into localStorage.
        // This could be anything as long as it's unique.
        // https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
        this.localStorageKey = window.location;
    
        // Retrieve data from localStorage when the Controller loads.
        this.setFormData();
      }
    
      clearLocalStorage() {
        // See if there is data stored for this particular form.
        if (localStorage.getItem(this.localStorageKey) != null) {
          // Clear data from localStorage when the form is submitted.
          localStorage.removeItem(this.localStorageKey);
        }
      }
    
      getFormData() {
        // Construct a set of of key/value pairs representing form fields and their values.
        // https://developer.mozilla.org/en-US/docs/Web/API/FormData
        const form = new FormData(this.formTarget);
        let data = [];
    
        // Loop through each key/value pair.
        // https://developer.mozilla.org/en-US/docs/Web/API/FormData/entries#example
        for (var pair of form.entries()) {
          // We don't want to save the authenticity_token to localStorage since that is generated by Rails.
          // https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf
          if (pair[0] != "authenticity_token") {
            data.push([pair[0], pair[1]]);
          }
        }
    
        // Return the key/value pairs as an Object. Each key is a field name, and each value is the field value.
        // https://developer.mozilla.org/en-us/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries
        return Object.fromEntries(data);
      }
    
      saveToLocalStorage() {
        const data = this.getFormData();
        // Save the form data into localStorage. We need to convert the data Object into a String.
        localStorage.setItem(this.localStorageKey, JSON.stringify(data));
      }
    
      setFormData() {
        // See if there is data stored for this particular form.
        if (localStorage.getItem(this.localStorageKey) != null) {
          // We need to convert the String of data back into an Object.
          const data = JSON.parse(localStorage.getItem(this.localStorageKey));
          // This allows us to have access to this.formTarget in the loop below.
          const form = this.formTarget;
          // Loop through each key/value pair and set the value on the corresponding form field.
          Object.entries(data).forEach((entry) => {
            let name = entry[0];
            let value = entry[1];
            let input = form.querySelector(`[name='${name}']`);
            input && (input.value = value);
          });
        }
      }
    }
  
  1. Add action: "submit->auto-save#clearLocalStorage" to the post form.

    <%# app/views/posts/_form.html.erb %>
    <%= form_with(model: post, data: { controller: "auto-save", auto_save_target: "form", action: "change->auto-save#saveToLocalStorage submit->auto save#clearLocalStorage" }) do |form| %>
      ...
    <% end %>
    

Security Considerations

I don’t recommend using this technique on forms that store sensitive data such as passwords or credit cards, since their values would be stored in plain text in localStorage.