Notes to self

Auto-saving Rails forms with Turbo Streams

Here’s how to implement autosaving for inline input fields the Hotwire way.

Autosaved forms

What’s autosave? Autosaving is saving a user input automatically on changes, lost focus or after an interval of no interactivity without any specific user action. Typically in inline forms.

To make things straigtforward let’s say we want to save a post’s title while reusing an existing update action that can save the title or perhaps all the post’s attributes.

They are couple options to go around it, but here’s how I do it. You just need Turbo and Stimulus installed.

Stimulus autosave

Since we’ll remove the usual ‘Save’ button from the form, we’ll need an auto-submission done in a different way. We can create a small Stimulus autosave controller that will be able to autosave anything by submitting the model form:

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["form"];

  submit(event) {
    event.preventDefault();
    this.formTarget.requestSubmit();
  }
}

The controller has one form target to determine which form to submit. You could even avoid a target and find the form element, but this makes it explicit.

We need to call requestSubmit instead of submit otherwise we wouldn’t get a Turbo request.

The form

The form to submit the action is completely same, we keep our form_with, we keep the same url if we specified that. We just wrap it with data-controller set to our Stimulus controller name and provide data attributes on the form and the field in question:

<div data-controller="autosave">
  <%= form_with(model: post, url: post_path(post), data: { "block-target": "form" }) do |form| %>
    <%= form.label :title %>
    <%= form.text_field :title, class: "input", data: { action: "blur->autosave#submit" } %>
      
    <turbo-frame id="title-status">
    </turbo-frame>
  <% end %>
</div>

I used the blur event to autosave on lost focus but you could also use change to immediately save any progress (at the expense of many HTTP calls).

I also prepared a small <turbo-frame> with an ID that will inform the user about the saving status. The type of the element is irrelevant, it can be a <div> too. The important thing is the ID.

Note that Turbo could also replace the whole form or in a different approach you could even morph the whole screen.

Turbo stream

Given that our update action remains the same as with traditional forms, the form would already be submitted but we would get the usual redirection. Instead, we want to return a Turbo stream:

class PostsController < ApplicationController
...
  def update
    if @blog_post.update(post_params)
      respond_to do |format|
        format.turbo_stream
        format.html { redirect_to post_path(@post), notice: "Post was successfully updated." }
      end
    else
      respond_to do |format|
        format.turbo_stream
        format.html { render :edit, status: :unprocessable_entity }
      end
    end
  end

Now we have an autosave but without any feedback for the user. To add it we’ll instruct Turbo to replace our <turbo-frame> with either message Saved. or the error in the view:

<!-- app/views/posts/update.turbo_stream.erb -->
<%= turbo_stream.append "title-status", method: :morph, target: :current_step do %>
  <% if @blog_post.errors[:title].any? %>
    <%= @blog_post.errors[:title].first %>
  <% elsif @blog_post.title_previously_changed? %>
    Saved.
  <% end %>
<% end %>

Asking on title_previously_changed? will only output the message if any actual changes happened.

And that’s it! You have an inline form with autosaving.

Warning

I spent a considerable amount of time chasing an issue with wrong field focus. If that happens to you, go through all of your forms and make sure each input field has a unique ID. If you have two forms of the same model on the page they will have a different form ID but same input IDs.

Check out my book
Interested in Ruby on Rails default testing stack? Take Minitest and fixtures for a spin with my latest book.

Get Test Driving Rails and make your tests faster and easier to maintain.

by Josef Strzibny
RSS