File Upload: Rails + Refile + Dropzone

Youssef Chaker
Bear & Giraffe
Published in
13 min readFeb 8, 2017

Finding a good tutorial on how to add file upload to a Rails application is easy. But most of them will focus on how to integrate a specific gem like paperclip or carrierwave or refile into your application, but very rarely do you have a tutorial that will walk you through the full process of building out a feature from start to finish. So let’s change that…

Requirements

First, let’s start with the requirements. Let’s assume you are building an application which requires users to upload a document. That seems simple, right? It is, theoretically. In this theoretical app, we have a few models that we can represent: TaskCreator, Task, TaskDoer, and Doc.

TaskCreator's job is to create Tasks and assign them to a TaskDoer, and each Task will have a name which serves as the description of the Doc that is required from the TaskDoer to upload. The model architecture would look something like this:

model architecture for our theoretical app

I will be leaving out some details that are outside of the scope of this post, but in a nutshell, what you need to know is that we have a User model that can sign in, and we have 2 types of Users, a TaskCreator and a TaskDoer which are what we need to focus on. In our theoretical app, a User can only be one of the two roles, not both. Your app design choices might be different, which is why I won’t go into details about this part. Another thing to note is that in our architecture we have an attribute called number_of_items on the Task model which allows us to specify how many Docs are required to be uploaded for each Task. For the sake of this post, we will assume all Tasks require only 1 Doc but you can see how this can be flexible and allow for multiples if your app requires it.

So far so good, the requirements are clear, but there’s one component that is missing which will drive some of the technical decisions. That component is the UI. So here’s a quick sketch:

List of Tasks
Upload modal
Upload complete

Note: Keynote (or PowerPoint) can be a handy tool for quick sketching wireframes, you don’t need a fancy tool to do that.

The hidden requirements that were extracted from these sketches is that the UI will have a list of Tasks that are assigned to a TaskDoer, once the TaskDoer clicks on the upload button, a modal will pop up asking the TaskDoer to upload a Doc either by drag/drop over the drop area (which is the cloud icon) or by using the native browse functionality. Once the upload is complete the list of Tasks is automatically updated to mark the Task as complete by switching the button text from Upload to View and color from blue to green. For simplicity, we will assume that a Task is complete if it has a Doc associated with it, but completed Tasks will be grouped together at the bottom of the list and pending ones will be grouped at the top of the list.

Here’s the interaction as a gif because you can’t have a serious post without a gif:

Upload experience

So, to make the obvious clear we will need a way to handle both the native browse functionality to select files to upload and the drag and drop method. While making it all happen without reloading the page. You might be expecting me here to say that I will be using something like ReactJS or AngularJS to handle the process in the browser, but as you can tell from the title of the post, I won’t. But I will note that handling the list of Tasks and updating based on each one’s status is a good candidate for something like React, but when it’s possible to avoid using another dependency that usually wins for me, especially when the new dependency has the potential of adding complexity instead of reducing it. As you will see from the rest of the post that in this case using what Rails gives us will be super simple, but then again, this is a theoretical app with its complexity reduced fictitiously so YMMV.

Tech Choices

Ruby on Rails

Rails is an obvious choice for me. It’s a powerful framework that lets me focus on the business requirements while its convention over configuration philosophy reduces the amount of repetitive work I need to do. Looking at the model architecture from above you will see that the most intricate part is the associations between the models but with Rails' has_one / has_many / belongs_to methods that are provided by ActiveRecord we won’t have much work to do on that front and we can focus on the important part which is to provide the user the best experience handling the file uploads.

Refile

There are a couple of gems that handle file uploads in Ruby and paperclip might be the most known one, but I had a great experience using carrierwave in the past on many projects and refile is the better, more modern version of that written by the same people. It does say “Ruby file uploads, take 3” on the project github page after all, which means that all of the lessons learned from the first couple of times have led to a better tool this time around.

DropzoneJS

After some quick googling to see what people are using for a drag and drop solution, I landed on the DropzoneJS website and that seemed to provide the cleanest solution of all of the ones I had seen, and a good documentation. A nice looking site also helps when you’re trying to learn a new tool, because ugly sites are hard to read and if it’s hard to read it’s hard to comprehend. So that was my choice for this piece of the puzzle.

If you have a suggestion for another library to use, share it with us.

Time to Build

Let’s get to the code, shall we?

User Stories

First, we start with the user story description:

The story above sets up the scenario we care about but is outside of the scope of this post, so I’ll let you fill in the details. The one we care about the most is this one:

and with that goes the steps implementation:

That’s a lot of code. Most of it is not specific to our case, it’s code that you would use to test any feature using capybara and cucumber. The one thing worth noting is that capybara gives us a helper method that we can use to simulate someone attaching a file attach_file. You may have noticed that we’re lacking code to test the drag and drop feature, which is impossible to do. In either case, we wouldn’t want to test the functionality of libraries that we use anyway. We are not concerned with testing Rails functionality, neither would be concerned with testing refile nor dropzonejs. What we do want to test however is that the workflow we’ve described works. In other words, do the things we expect to happen after a user’s interaction do indeed happen. And that’s what you see in our example, we test that the user is seeing the right feedback messages, an email is sent out, etc.

Of course, none of this code will pass at the moment because we have not yet implemented any of it. So we should get to that next.

DISCLAIMER: I would not normally write out the entire set of tests first before doing any code. I would build it out piece by piece and get the first test to pass and move on to the next. But as of this writing, Medium does not support pasting in individual files from a gist, instead, it spits out all of the files of a gist together, which means if I only want to show one snippet at a time I have to create a separate gist for each. That can be very tedious. So instead, I’ve created a separate gist for each file, and that’s what you see. Just keep that in mind if you ever feel that it would be daunting to try to reproduce this effort yourself.

Gems

The next step would be to install the gems, add the following to your Gemfile:

# Use Dropzone for drag and drop of document to upload.
gem 'dropzonejs-rails'
# Use Refile for uploading document
gem 'refile', require: ['refile/rails', 'refile/simple_form'], github: 'manfe/refile'
gem 'refile-mini_magick'
gem 'refile-s3'

and run bundle install. Note that we are using the 'manfe/refile fork because of a Rails 5 compatibility issue currently. You might not need to do that, so check the gem’s page for the latest info.

UI

From here I like to work backwards from the UI to the backend. The reason I like to do that is because I want to work from the user’s perspective, not the developer’s perspective. It’s the same reason I write the feature descriptions first (the cucumber tests from above). User behavior is the primary driver for the design of the app. When working in this manner I tend to notice many things that I had not thought of at first glance when I started architecting the feature and it allows me to go back to the user stories and update them. These changes usually come at no cost because I still have not implemented any code, which means that the design of the code structure can change easily.

I am going to skip the part where the TaskCreator creates the Tasks, that could be a good exercise for you to do on your own. Instead, I’m going to pick up from the TaskDoer's perspective. Which means a bunch of Tasks has already been created, and the TaskDoer is looking at their dashboard with a list of Tasks and wants to upload a Doc for one of them. For that, we assume that we have a Dashboard controller that fetches all the Tasks associated with the TaskDoer and then lists them out. In other words, the index.html.erb view template has the following code:

<%= render partial: 'tasks/list', locals: { completed_tasks: @completed_tasks, pending_tasks: @pending_tasks } %>

Where the tasks/_list.html.erb partial is the following:

I’ve removed any classes or styling related info from this HTML snippet and kept only the necessary stuff. We have a heading at the top that lets the user know how many completed and pending Tasks there are, and then lists each group separately since our requirement was to group the completed Tasks at the bottom. This part is also very simple, the real work starts in the upload modal we referenced on line 27.

Obviously, for the modal we are using Bootstrap but you can ignore that part. The first thing to note <%= simple_form_for @doc, remote: true, html: { class: ‘dropzone' } do |f| %>, and more specifically remote: true and class: 'dropzone'. We want the form to submit the Doc remotely so we can update the list without refreshing the page, and DropzoneJS requires us to add the dropzone class to our form.

Then comes the part below <!-- Dropzone --> which specifies the area that we want to consider the drop zone for the drag and drop and matches the template that we saw in the wireframes above. We have the cloud icon and the fake button that serves as a visual cue but in fact, if the user clicks on any part of that whole area the file browser will pop up since we specifically designated it all with the tag data-dz-clickable. Then we have the part that glues it all together which is this <%= f.input :file, as: :attachment, direct: true, label: false, input_html: { class: 'hidden' } %>. This is the part that lets refile upload the document to the server and attach it to the Doc. We want it hidden because we don’t want to show the default file input the browser displays, instead, we’re using the layout we mentioned as the visual element that lets the user know that they need to upload a file and how to do it.

We then have the preview section that we use to preview files that are images. You can leave that out or use the default template that DropzoneJS provides. In our case we just want to show the filename and the size of the file, and in case it’s an image display the thumbnail. Then we’ve set up a progress bar that will display the progress of the upload as it happens. That’s pretty much it for the template. Next, we’ll take a look as the js that makes it all work as expected.

JS

To make DropzoneJS hook into our form, we need to add some configurations using js, so let’s add a uplod_doc.coffee file to our assets and require it into our application.js manifest (I’ll let you handle that part on your own).

In our case, we’re customizing quite a bit of the DropzoneJS's behavior. We don’t want it to do anything automatically for us because we have multiple Tasks on the same page and each one of them has its own modal which then contains its own specific form. So we want to avoid the library trying to attach a dropzone to the same area multiple times because that will result in errors and weird behavior. And we want it to be active only if the modal is visible. That explains lines 2, 4, and 9.

Then we’ve specified some of the options that DropzoneJS makes available to us. Primarily we only want one Doc per upload, and we told it which input to use as the file input by specifying the paramName. We don’t want it to process the file before the user clicks on the Upload button, so we set autoProcessQueue to false.

In this snippet:

maxfilesexceeded: (file) ->
this.removeAllFiles()
this.addFile(file)

we are making sure that if the user selects a new file after already selecting one, that it overrides it instead of trying to upload both ( DropzoneJS lets you select and upload multiple files at the same time, but that’s not what we want for our app).

Skipping ahead to line 47, here we specifying that once the button is clicked and there are files in the queue to be uploaded, we should disable the button and replace the text with a sinner, then process the queue. Then on line 44 we listen to the event broadcasted by DropzoneJS which returns the progress of the upload and update the progress bar.

Now back to line 21 which handles the response of the call once it’s done. First, it restores the button back to its original state (re-enables it and removes the spinner), then it hides the modal and displays the success message. Then we want to update the list of Tasks so in this case, we make another call to fetch the updated list. This where using something like AngularJS or ReactJS would have been handy to clean this up. But since I said we were going to keep it simple, the 2 $.get calls will have to do.

Lines 68 to 78 don’t do a lot in this case. They hook into the Refile event listeners and update the state of the button when the upload is triggered and complete. It’s a bit redundant since we are already handling this in the code before it, but I left it there to illustrate that it’s something available to you to use.

If you’ve gotten this far, you have a modal that pops up for the user to upload a Doc and the user can either drag and drop or select a file from the file browser. Once they do that though, they might see the preview but the upload does not work. So we should have 1 or 2 passing scenarios but that’s about it. Time to make them all pass.

Backend

We’ve gotten this far without writing too much Ruby or Rails code if you haven’t noticed, so let’s get to it already.

Based on the work we’ve already done, we know exactly which routes we need. Those were determined by the tests and by the work we did in the UI. We don’t have to guess whether we need a certain route or controller, we already know, and that’s a big benefit of working through it while being mindful of the user interaction.

Here are the routes we need:

resources :docs, only: [ :create, :show, :update ] do
collection do
get '/reload' => 'docs#reload'
end
member do
get '/reload' => 'docs#reload_doc'
end
end

which tells us that we need a DocsController:

Quick note, we haven’t mentioned view a Doc before but in the controller you might have noticed the show action. This is another example of what you might be able to do even though it might not fit in our theoretical app per se.

The relevant section is in the create action. You might be wondering what’s up with the @operation, @model, and @form objects are all about. They come from using trailblazer and that’s a topic for another day. What you need to know is that the trailblazer operation takes the form params, runs the appropriate validations, and then process them according to whichever business logic you specify. In this case, it looks something like this:

Which basically takes the params passed the controller, which includes the Task id, the file, and the current_user and creates a Doc object and saves it to the database. Our Doc model is the following:

class Doc < ApplicationRecord
# Associations
belongs_to :task
belongs_to :uploader, class_name: 'TaskDoer'
attachment :file,
extension: %w[pdf jpg jpeg gif png],
content_type: %w[image/jpeg image/png image/gif application/pdf]
end

and the missing piece is the template that goes with the create action:

which takes care of loading the DOM with the updated list of Tasks.

That’s it! Easy, right?

I have mostly talked about the create action but the code you see includes parts that are used for the update action, which is very similar even though I have not specifically talked about or included all the required pieces. I trust you’d be able to fill in the blanks.

I hope this can help you implement file uploads in Rails with drag and drop capabilities, and customize it to your own needs.

DISCLAIMER: This post did not tackle the issue of security. Could be a topic for another post in the future. So be sure to follow and be on the lookout for more content like this.

--

--