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.
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
.
rails new rails-auto-save-form-data -d=postgresql --webpacker=stimulus
rails db:create
rails db:migrate
rails g scaffold Post title body:text
rails db:migrate
Step 2: Create the Stimulus Controller
Next we’ll need to create a Stimulus Controller to store our Javascript.
-
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"]; }
-
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.
-
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)); } }
-
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
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.
- 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.
- 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);
});
}
}
}
-
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
.