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.
Get Test Driving Rails and make your tests faster and easier to maintain.