Ok, so this is slightly hacky. Let me also preface this by saying there's probably no real need to do this if you add some client side form validation. Nonetheless sometimes submissions can get through and it's nice to know you can persist the direct upload when we rerender the view.
If anyone knows a better way to do the PLEASE feel free to let me know in the comments below. I searched around a bit but came up with nothing so this is how I'm handling it for now.
If you're building a Rails 5.2 app, or just follow Rails in general, there's a good chance you're familiar with ActiveStorage. If you read through the Rails docs you'll see it's really easy to set up. Even direct uploads are crazy easy. What's not so easy, well not so clear, is handling the direct upload if our submission is kicked back from the controller.
Pretty standard Rails stuff here. Everything but the ActiveStorage and validation is what you would expect from a typical scaffold.
This will all work as expected with the super cool feature of the post image being directly uploaded to whatever storage solution we are using.
Ok, cool. So what's the issue?
Well, remember how our model validates the presence of title and body? We'll run into a SNAFU if we submit the form without the required information.
Now, this is where things get a little tricky. Our form will repopulate when our controller calls
render :new with the exception of the image we attached. To further complicate the matter, the image we attached has already been uploaded via the direct upload. This presents two issues, one, if we simply attach the file to the form again it will now be uploaded twice. And two, if we have any kind of preview/variant/indication to our users that there is an attached image, like when using the form for editing, we are going to confuse our users. Why? Because if we're conditionally rendering the image or changing a label or just telling the user that, yes, the post has an attached image.
@post.image.attached? # => true
Yup. Because of the direct upload a record on the active_storage_blobs table has been created, like it should. And because we tried to attach the image to a post a record on the active_storage_attachments join table was created. Unfortunately since our post was not saved, the record_id column is
nil so this attachment won't persist. At least at the time or this writing (or because I missed something entirely) ActiveStorage is not smart enough to handle the invalid attachment record. So if we correct the form issues and resubmit, we won't have the image associated with our post, leaving an orphaned file in our cloud storage and our users a little confused (and probably) frustrated.
Like I said, kinda more like a hack. If we add the following line to to the else block of our posts_controller:
@image = params[:post][:image] if params[:post][:image].present?
and update our view form with something like this:
<% if @image %> <%= f.hidden_field :image, value: @image %> <% else %> <%= f.file_field :image, direct_upload: true %> <% end %>
Now that everything looks like this:
Like magic, things will work a little more as expected. Next time we try to attach an image to our post, submit the form without a title or body, and then resubmit our uploaded image will be attached.
Cause reasons! And because of how Rails/ActiveStore handles direct uploads. After the direct upload is completed it sends back a
signed_id that Rails uses to created the blob record in the database and attached to the post via the attachment join table. The sign_id is passed via params to our controller and ActiveStorage handles all the magic. So if we updated our controller to store the value of
params[:document][:file] and our view to place that value (if it exists) into a hidden field on the form we will be able to submit the file and because the signed_id already exists on the blob table, ActiveStorage is smart enough (this time) to only create an attachment record.