Validations and File Caching using ActiveStorage

Andrew Courter
EarthVectors
Published in
6 min readJul 20, 2019

I work for a small consultancy, EarthVectors, Inc, and we get to work in a variety of domains and spaces where we help clients solve both business and technical problems every day. I recently ran into an interesting case around ActiveStorage and validations and figured I would share my experiences.

The application is a standard Rails application using ActiveStorage and erb templates to render bootstrap forms and handle your typical CRUD (Create Read Update Delete) operations. During some manual testing of the app we uncovered an issue where after a failed validation, the files that were uploaded were removed and upon resubmission the user would get another failed validation that their file was missing. Definitely not the most intuitive user experience, so we decided it needed to be fixed.

When uncovering an issue like this I like to take a step back and understand why something is a problem before jumping into coming up with solutions. There are usually many solutions to a problem so understanding the tradeoffs between each is key to building great software. This particular issue was a problem because the user did not have a clear indication of what fields were required and in filling out the minimum fields needed for form submission would get into a bad state potentially causing a lot of frustration. We need to provide a solution that will allow the user to understand our expectations and to have a seamless experience without unexpected errors. Let’s explore a few solutions that could help make the workflow more clear to the user.

Solution 1

We can let the user know what fields are required before they submit the form. If they know what we expect them to enter into the form before it is complete then we wouldn’t need to even worry about whether or not the photo is lost after a saved validation because they shouldn’t get into that state. Below is the file field that we need to add styling to and make sure the user can clearly see that this field is required. The ujs_target is used to trigger some javascript to load an image preview after the file input is changed.

<%= form.file_field :photo, hide_label: true, data: { ujs_target: "photo-input" }, help: "PNG or JPG only" %>

First let’s start by adding a required class into our application.scss file. A convention for form fields is to make required fields red so let’s add a class to append some red text after the field input help text.

.required small:after {
color: red;
content:" *Required";
font-size: 0.5rem;
text-transform: uppercase;
}

Now that we have our CSS class, we need to add it to our file field. Because we are using a bootstrap_form_with we can do that by adding our class as a wrapper_class. After adding that class we should now see our Required text after the help for the file input text.

<%= form.file_field :photo, wrapper_class: "required", hide_label: true, data: { ujs_target: "photo-input" }, help: "PNG or JPG only" %>

Success! Now that we have a working example of applying styling to indicate required fields, what are the tradeoffs for implementing only this solution? First, we have clearly denoted which field is required and at a minimum the user should see this field and understand they have to fill it in before submitting the form. This solution was quick to implement and we could reuse it across all our file fields in the entire app relatively quickly. However, we are still dependent on the user doing the right thing. What happens if they STILL don’t see the text and submit an empty form? What if they are an evil user and try to break the system by submitting nothing? Let’s discuss another solution in Solution 2 that will give us more confidence that the system will work correctly even if the user submits empty data.

Solution 2

Another solution to the problem would be to cache the photo so that the user does not have to re-upload after a failed validation and the photo is available even if the page refreshes. This gets at the root of the problem regardless of whether the user is evil or is just in a hurry and glances over any indications we have to denote that a field is required. Let’s see what it would take to implement something like that. Below is our file field that we would like to modify.

<%= form.file_field :photo, hide_label: true, data: { ujs_target: "photo-input" }, help: "PNG or JPG only" %>

If we were using CarrierWave as our image uploading solution then we would add a hidden field called photo_cache and that would provide the caching mechanism we would need. We can do something similar using ActiveStorage but need to provide the value to the hidden field. We need to get the signed_id from the photo so that ActiveStorage wires everything up correctly upon save. That information is available to us through the form using form.object.photo.signed_id. Additionally, we only want to add that hidden field if the photo has been attached, otherwise we expect the value to be attached using the file field. Let’s add that hidden field to the form so that we can start caching.

<%= form.hidden_field :photo, value: form.object.photo.signed_id if @model.photo.attached? %>
<%= form.file_field :photo, hide_label: true, data: { ujs_target: "photo-input" }, help: "PNG or JPG only" %>

Success! The user may not know that this field is required but at least they will not be forced to re-upload their photo if they received a validation error. As mentioned earlier, this solution gets at the root of the problem and is not dependent on human behavior for the system to work correctly. There are a couple drawbacks worth mentioning with this solution. The code is a bit more complicated than simply adding a CSS class and we would need to remember to do this across all places that we use file_fields. In order to make reuse easier we could extract this into a partial or form helper but that would also increase the complexity of the code for the sake of reuse. If there are many places that we want to reuse this code then that may be a good tradeoff to make.

Solution 3

Another solution to the problem would be to prevent the user from submitting the form using front end validations. That way they know immediately that they are missing required fields before they even make a request to the server. Let’s see what adding the required attribute to enable the front end validation looks like.

<%= form.file_field :photo, hide_label: true, required: true, data: { ujs_target: "photo-input" }, help: "PNG or JPG only" %>

Success! This was pretty painless to add and we have prevented requests from even getting to our system if the user forgets to add required information. However, if we miss adding required to all the fields that are required then we can still get into a weird state where the file gets lost.

In the end we went with a mix of all 3 solutions to provide the most amount of information to the user and to prevent losing files that they had just uploaded. Below is the final solution that we added to the codebase.

Final Solution

<%= form.hidden_field :photo, value: form.object.photo.signed_id if @model.photo.attached? %>
<%= form.file_field :photo, wrapper_class: "required", hide_label: true, required: true, data: { ujs_target: "photo-input" }, help: "PNG or JPG only" %>

Happy coding!

--

--