Implementing Autosave
Big thank you to Seth Addison for the initial code for this.
Implementing autosave can be done a number of ways.
JavaScript
// controllers/rhino_autosave_controller.js
import { Controller } from "@hotwired/stimulus";
// https://dev.to/jeetvora331/throttling-in-javascript-easiest-explanation-1081
function throttle(mainFunction, delay) {
let timerFlag = null; // Variable to keep track of the timer
// Returning a throttled version
return (...args) => {
if (timerFlag === null) { // If there is no timer currently running
mainFunction(...args); // Execute the main function
timerFlag = setTimeout(() => { // Set a timer to clear the timerFlag after the specified delay
timerFlag = null; // Clear the timerFlag to allow the main function to be executed again
}, delay);
}
};
}
export default class RhinoAutosave extends Controller {
initialize() {
// Throttle to avoid too many requests in a short time. This will save the editor at most 1 time every 300ms. Feel free to tune this number to better handle your workloads.
this.handleEditorChange = throttle(this.handleEditorChange.bind(this), 300)
}
connect() {
// "rhino-change" fires everytime something in the editor changes.
this.element.addEventListener(
"rhino-change",
this.handleEditorChange
); // Listen for rhino-change
}
disconnect() {
this.element.removeEventListener(
"rhino-change",
this.handleEditorChange
);
}
handleEditorChange() {
// Don't need to await. We're not relying on the response.
this.submitForm()
}
async submitForm() {
const form = this.element.closest("form");
const formData = new FormData(form);
try {
const response = await fetch(form.getAttribute("action"), {
// Its technically a "PATCH", but Rails will sort it out for us by using the `form_with`
method: "POST",
body: formData,
headers: {
Accept: "application/json",
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content,
},
});
if (!response.ok) {
const json = await response.json()
const errors = json["errors"]
// Decide how you want to use errors here, if at all.
console.error("Auto-save failed", errors);
} else {
console.log("Auto-save successful");
}
} catch (error) {
// This is usually a network error like a user losing connection, and not a 404 / 500 / etc.
console.error("Error in auto-save", error);
}
}
}
This assume you have a DOM like the following:
ERB
<%= form_with model: @model do %>
<rhino-editor data-controller="rhino-autosave"></rhino-editor>
<% end %>
You’ll also need your controller to respond to JSON, something like the following:
Ruby
class PostsController < ApplicationController
def update
@post = Post.find(params[:id])
if @post.update(post_params)
respond_to do |fmt|
fmt.html { redirect_to @post }
fmt.json { render json: {}, status: 200 }
end
else
respond_to do |fmt|
fmt.html { render :edit, status: 422 }
fmt.json { render json: { errors: @post.errors.full_messages }, status: 422 } }
end
end
end
private
def post_params
# We assume your model is something like `has_rich_text :content`
params.require(:post).permit(:content)
end
end
With the above, “autosave” should start working for you! (And if it doesn’t please open an issue and I’d be happy to take a look!)