Posted almost 3 years ago. Visible to the public.

Dynamically uploading files to Rails with jQuery File Upload

Say we want …

  • to create a Gallery that has a name and has_many :images, which in turn have a caption
  • to offer the user a single form to create a gallery with any number of images
  • immediate uploads with a progress bar per image
  • a snappy UI

Enter jQuery File Upload. It's a mature library that can do the job frontend-wise. On the server, we'll use Carrierwave, because it's capable of caching images.

(FYI, here's how to do the upload all manually.)

Rails setup

Build these models:

class Gallery < ActiveRecord::Base has_many :images, dependent: :destroy accepts_nested_attributes_for :images, allow_destroy: true, reject_if: :all_blank # Ignore the blank template record end class Image < ActiveRecord::Base belongs_to :gallery mount_uploader :teh_image, YourUploaderHere # Mount Carrierwave on attribute :teh_image end

And these controllers:

class GalleryController < ApplicationController # ... private def gallery_params params.require(:gallery).permit [:name, { images_attributes: [:id, :caption, :_destroy, :teh_image_cache] }] end end class UploadsController < ApplicationController def gallery_image image = params.require(:gallery).require(:add_images).first uploader = uploader.cache! image # Return the image's cache name to the client render json: uploader.cache_name end end


In the gallery form, we'll need a template for adding new images. I chose to add a blank record for this purpose.

# app/views/galleries/_form.html.haml = form_for @gallery, html: { 'gallery-form': true } do |gallery_form| .form-group = gallery_form.label :name = gallery_form.text_field :name %label Images %table - template = # Build template record = gallery_form.fields_for :images do |image_form| %tr{ class: ('hidden gallery-form--template' if image_form.object == template) } %td = image_tag image_form.object.teh_image # Used by Carrierwave to connect a record to an already uploaded file = image_form.hidden_field :file_cache # Used by Rails to distinguish new from existing records = image_form.hidden_field :id # Used by Rails to know when an image should be destroyed = image_form.hidden_field :_destroy %td = image_form.text_field :caption %td = link_to '×', '#', title: 'Remove image', class: 'gallery-form--remove' -# :add_images is no attribute of Gallery, but will be the params key when -# sending the image files to the server = gallery_form.label :add_images do = form.file_field :add_images, class: 'hidden', multiple: true Add Images = gallery_form.submit


Now to the dynamic part. I built it with an Unpoly compiler, but the code will work without as well.

up.compiler '[gallery-form]', (element) -> imageTemplate = element.find('.gallery-form--template') or throw 'Missing template' fileField = element.find('input[type=file]') or throw 'Missing file input' uploadURL = '/uploads/gallery_image' # Where to POST the images submitButton = element.find('input[type=submit]') # Disabled during upload init = -> element.on 'click', '.gallery-form--remove', removeImage fileField.fileupload add: addImages # Invoked when images are added (here via the file field) dropzone: false progress: Helper.updateProgressBar # Run for each file each few ms start: -> submitButton.attr 'disabled', true # Run when the first upload starts stop: -> submitButton.removeAttr 'disabled' # Run when the last upload ends type: 'POST' url: uploadURL addImages = (e, data) -> for file in data.files imageRow = Helper.addImageToForm() # Store a reference to the progress bar in `data` so it can be retrieved # later in `progress` events data.progressBar = Helper.addProgressBar(imageRow) # Send the image *now*, and set the cache name when it's done data.submit().then Helper.setCacheName(imageRow) # Render an image preview Helper.preview file, imageRow.find('img') # Cancel upload when removing an image imageRow.find('.gallery-form--remove').on 'click', data.abort removeImage = (e) -> e.preventDefault() imageRow = $('tr') destroyField = imageRow.find('input[id$=_destroy]') imageRow.hide() # Hide the image from the form destroyField.val('true') # Tell Rails to destroy the image submitButton.removeAttr 'disabled' # Prevent unsubmittable form (dirty fix) Helper = # Clone the template row # @return: The newly created image row addImageToForm: -> row = imageTemplate.clone() Helper.increaseTemplateIndices() row.removeClass 'gallery-form--template' row.insertBefore imageTemplate # Prepare the template to be cloned for the next image increaseTemplateIndices: -> # E.g. ("[1]", "1") -> "[2]" increase = (match, $1) -> match.replace $1, parseInt($1) + 1 imageTemplate.find('input').each -> $input = $(this) $input.attr name: $input.attr('name').replace(/\[(\d+)\]/, increase) id: $input.attr('id').replace(/_(\d+)_/, increase) # @return A promise callback handler setCacheName: (template) -> # Store the image cache name to the form, so the already uploaded # image file gets stored with the image record on save return (response) -> cacheField = template.find('input[type=hidden][id$=file_cache]') cacheField.val response preview: (file, img) -> blobURL = URL.createObjectURL(file) img.attr 'src', blobURL # @return: The progress bar element addProgressBar: (element) -> progressBar = $('<div>').addClass('image--progress').css 'width', '100%' progressBar.appendTo element.find('.image') # This progress bar *shrinks* from full width to the right updateProgressBar: (event, data) -> progress = data.loaded / * 100 remainingPercent = 100 - Math.round(progress) + '%' data.progressBar.css 'width', remainingPercent init()
Growing Rails Applications in Practice
Check out our new e-book:
Learn to structure large Ruby on Rails codebases with the tools you already know and love.

Owner of this card:

Dominik Schöler
Last edit:
almost 3 years ago
by Tobias Kraze
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Dominik Schöler to makandra dev
This website uses cookies to improve usability and analyze traffic.
Accept or learn more