I’ve recently been using Active Storage, and it has a really neat feature called Direct Uploads. Active Storage asks your app for a URL for uploading each file, and then proceeds to upload the file directly to your storage service, without having to tax your app server’s processor. But if you’ve selected a lot of files, or your file is huge, or both, you may accidentally leave the page before those files are uploaded, causing the whole upload batch to fail. Thankfully, we can setup our form, like in the previous form example, to prevent our customer from accidentally leaving the page while uploading files.
How?
The browser fires off a event every time it’s about to change the current page called beforeunload
that we’ll use to prevent losing an unsaved form. We’ll also listen for Turbolinks turbolinks:before-visit
event for page transitions inside our app. By intercepting and handling these events, and keep tracking of uploads by listening to active storage DOM events, the controller can put up an alert, making sure the user actually wants to leave the page.
Getting Started
I’m assuming you’ve setup your page to properly pull in Stimulus Controllers. The Stimulus Handbook has instructions if you’re new and need to get started.
I’m also assuming you have Active Storage configured with your preferred storage service. You can easily get started with the built in disk service.
HTML
Let’s add the Stimulus annotations to our form. The <form>
tag is going to have the controller, and the window event listening actions. We will also listen for two Active Storage events, direct-uploads:start
and direct-uploads:end
. When uploading starts, we will change the uploading
data attribute on the form to true
, and then change it back to false
when uploading is done.
I’m working with a simple ActiveRecord PhotoAlbum
model which has a name, description, and is set to have may photos through Active Storage.
<%= form_for @photo_album, remote: true, data: { controller: 'photo-albums', action: 'beforeunload@window->photo-albums#leavingPage turbolinks:before-visit@window->photo-albums#leavingPage direct-uploads:start@window->photo-albums#startDirectUploads direct-uploads:end@window->photo-albums#endDirectUploads', 'photo-albums-uploading': "false" } do |form| %>
<%= form.text_field :name, :placeholder => "Album Name" %>
<%= form.text_area :description, :placeholder => "Album Description", :rows => 4 %>
<%= form.file_field :photos, multiple: true, direct_upload: true %>
<%= form.submit %>
<% end %>
Stimulus Controller
Our controller has the three actions, and uses an uploading
data attribute to keep track of whether to prevent the browser from changing the page.
The uploading
attribute always starts as false. When a direct-uploads:start
event happens, it is set it to true
and when a direct-uploads:end
event happens, it is set to false
. If uploading is true
when the a user tries to navigate from the page, the controller attempts to stop them. The turbolinks:before-visit
uses a confirm message and the beforeunload
event uses the browser’s native alert to make sure that the user wants to leave the page.
Here is the code for the photo_albums_controller.js
:
import { Controller } from "stimulus"
const LEAVING_PAGE_MESSAGE = "You have attempted to leave this page and you are uploading files. If you leave, you will need to reupload these files. Are you sure you want to exit this page?"
export default class extends Controller {
startDirectUploads(event) {
this.setUploading("true")
}
endDirectUploads(event) {
this.setUploading("false")
}
leavingPage(event) {
if (this.isUploadingFiles()) {
if (event.type == "turbolinks:before-visit") {
if (!window.confirm(LEAVING_PAGE_MESSAGE)) {
event.preventDefault()
}
} else {
event.returnValue = LEAVING_PAGE_MESSAGE;
return event.returnValue;
}
}
}
setUploading(changed) {
this.data.set("uploading", changed)
}
isUploadingFiles() {
return this.data.get("uploading") == "true";
}
}
In Closing
Now you can pick 20 or so photos, and not have to worry about accidentally closing a tab or leaving the page while they are still uploading. I hope this helps you build a great user experience.
Feel free to leave a comment or question below.
Want To Learn More?
Try out some more of my Stimulus.js Tutorials.