AJAX Photo Uploading the Easy Way with Rails 4 and Paperclip

Nerd Notes
10.08.14

In creating JustPayme (Week 5 of the 20/20 Challenge), the most important step was the simple on-boarding of a new user and their card.  On reaching the site, the user travels through a three step form, as displayed in the gallery below:

Just Pay Me Flow

The step that often trips me up is what to do with photo uploading.  In this case, I wanted the user to be able to choose a file, preview it, and then submit it when they are ready.  In addition, this whole form would have to be submitted through AJAX so that we could continue the user flow and move on to the final step of the on-boarding process. 

Choosing Tools For Simple Photo Uploading

When looking for the best way to do something, I always try to do some research on Rails gems that are out there.  For those that don't know, a great way to browse them is Ruby Toolbox

I came up with a list of potential tools I could use:

  • Paperclip - A simple file attachment library that works well with S3
  • Carrierwave - A flexible solution for Rails file uplaods
  • Dropzone (gem here) - An open source library that provides drag'n'drop file uploads with image previews
  • jQuery File Upload (gem here) - The most popular jQuery file upload solution that contains front end stuff, progress bars, and more.  Compatible with nearly every language, framework, etc

I wanted to store the photos in Amazon S3 (see next section for instructions on setting that up). So when choosing between Paperclip and Carrierwave, that was a big factor. Paperclip makes it very easy to work with S3 and has the most plug and play solution (ideal for a 1-week project).  So I went with Paperclip.

When choosing a AJAX gem, I realized that I was falling into the overkill trap.  I didn't need Dropzone's multi-upload tool and preview bars, or jQuery File Upload's massive array of options. I try not to fall into the trap of making the fanciest solution possible, because at the end of the day, user-friendliness is what matters. And users generally don't appreciate cool extra features that just slow them down. So I scratched Dropzone and jQuery File Upload and moved on.

General Setup - Paperclip and Amazon S3

I needed to first install and set up Paperclip and Amazon S3.  To start out with, if you already don't have one, make an Amazon Web Services account here.  Once you've done so, go find your Security Credentials and write those down somewhere.  Then, head over to the Amazon S3 (Scalable Storage in the Cloud) section.  Create a bucket that you want to put your saved files in. Store all of this info in environment variables.  I use the figaro gem to manage my variables and an example is below:

config/application.yml

  S3_BUCKET_NAME: "whatever-your-bucket-name-is"
  AWS_ACCESS_KEY_ID: "access-key-id"
  AWS_SECRET_ACCESS_KEY: "secret-access-key"

Now, we want to install what we need as gems.  Head to your Gemfile and enter the following:

  gem "paperclip", "~> 4.2"
  gem 'aws-sdk-v1'

Run bundle install and you should be good.  To run Paperclip locally, you'll have to install ImageMagick and you may need GhostScript.  If you are on Mac OS X, you can use Homebrew to easily install those.  Otherwise, you'll have to look around a bit for the best way for you.  If you need help with this, reply in the comments and I'll try my best to help you out there.

Next, in your *config/environments/**production.rb *and/or your *config/environments/development.rb *insert the following within the Rails.application.configure block (alter to your needs):

  config.paperclip_defaults = {
    :storage => :s3,
    :s3_credentials => {
      :bucket => ENV['S3_BUCKET_NAME'],
      :access_key_id => ENV['AWS_ACCESS_KEY_ID'],
      :secret_access_key => ENV['AWS_SECRET_ACCESS_KEY']
    },
    :url => ':s3_domain_url',
    :path => "/:class/:attachment/:id_partition/:style/:filename"
  }

Now you are mostly set up to handle Paperclip and Amazon together.  So we just need to make it work within your application.

Configuring the model

The next thing you need to do is tell Paperclip which model that you want to associate your image uploads with.  We wanted each card to have a photograph.  Typically, you would want to have the photo with the user, but we have some (top secret) extensions that we may add to the platform at some point.  Make sure to think about these kind of things when you are making your app, as it will make it easier in the future.

First, run a migration on the desired model:

  class CreateCards < ActiveRecord::Migration
    def change
      add_attachment :cards, :avatar
    end
  end

If the add_attachment migration threw you off, it's because it is a very useful add on that Paperclip gives your migrations.  Now that you have the columns in the database, head over to the appropriate the model and insert some derivation of the following:

  has_attached_file :avatar,
                    styles: { :medium => "200x200>", :thumb => "100x100>" }
  validates_attachment_content_type :avatar, :content_type => /^image\/(png|gif|jpeg|jpg)/

This is a very basic implementation without many options or validations but it does most of the work we needed.  When the user uploads an avatar, it saves a medium version and a thumbnail version.  It also validates that image uploaded is either a png, gif, jpg, or jpeg.  To add more validations or styles to your app, check out the Paperclip docs here and here.  If these docs confuse you, feel free to ask questions below or check out some questions on Stack Overflow.  

MAKING THE ACTUAL FORM WORK

So now we have Rails set up to receive any image that the user may upload, but the app still doesn't do anything.  We need to make the actual form.  In the second step of our on-boarding process, we included the following view:

Upload Photo Just Pay Me

Photos can be viewed when uploaded and deleted.  When the user clicks submit, the form is submitted via AJAX and we move on to the next step.  Let's start out by creating a basic form below:

  <%= form_tag card_photo_upload_path, method: :patch, id: 'photoinfo', remote: true, html: { multipart: true } do %>
    <div class="photoPreview">
      <%= icon('upload', '', class: 'photoUpload') %>
      <p id="uploadClick">Click to Upload</p>
    </div>
    <%= file_field_tag :avatar, accept: 'image/png,image/gif,image/jpeg, image/jpg', id: 'uploadAvatar' %>
    <p class="deletePhoto">Delete</p>
    <%= submit_tag 'Submit Photo', id: 'submitPhoto' %>
  <% end %>

When a user clicks on the icon to upload, the file upload screen pops up and user sees the file.  I then use jQuery to display the image in the circle.  I also allow the Delete text to appear.   Making use of the file reader, insert the following into your Javascript file: 

  function circleImageClick () {
    $('.deletePhoto').hide();
    $('.photoPreview').click(function() {
        $(this).attr('disabled', 'true');
        $('#uploadAvatar').trigger('click');
    });
    $("#uploadAvatar").change(function(){
        $('.photoPreview').removeAttr('disabled');
        readURL(this);
    });
  }
  function readURL(input) {
      if (input.files && input.files[0]) {
          var reader = new FileReader();

          reader.onload = function (e) {
              $('.photoPreview').css('background', 'url(' + e.target.result + ')');
              $('.photoUpload, #uploadClick').hide();
          }
          $('.deletePhoto').show();

          reader.readAsDataURL(input.files[0]);
      }
  }
  function deletePhoto () {
      $('.deletePhoto').click(function() {
          $('.deletePhoto').hide();
          $('#uploadAvatar').val('');
          $('.photoPreview').css('background', '');
          $('.photoUpload, #uploadClick').show();
      });
  }

This is a big chunk of code, and you may not need it all, but I wanted to make sure that the process is clear.  

We start out by hiding the Delete photo link and attaching a click listener to the Photo Preview circle.   Once its clicked, we disable it, preventing double clicking.  Then, the upload file gets called and the user chooses a file.  The readURL function that checks to see when a file is loaded and we set the background of the Photo Preview circle to the selected photo.  We then hide the unnecessary upload prompts and show the Delete photo link.  By clicking on the Delete link, the process is reversed and the user can start over.

Once the user clicks submit, we want to make check to see if the file has been uploaded successfully and then move the user on to the next action: 

  $('#photoinfo').bind("ajax:complete", function(data){
    // do what you need, handle errors, etc
  });

Using jquery_ujs, which comes shipped with Rails 4, we can handle errors or move to the next step on "ajax:complete".  Now you have your form and an excellent user experience!

If any of this is confusing, feel free to ask questions in the comments.  I am also actively looking for more topics that people need help on, so please let me know if there is anything that you think would be useful to cover.  Also, follow us on twitter @TeamMuno for updates; Week 6 coming soon! 

Ruben