Posted about 1 year 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:

Copy
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:

Copy
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 = Image.new.teh_image uploader.cache! image # Return the image's cache name to the client render json: uploader.cache_name end end

HTML

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

Copy
# 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 = gallery_form.object.images.build # 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

Javascript

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

Copy
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 = $(event.target).closest('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 / data.total * 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.

Author of this card:

Avatar
Dominik Schöler
Last edit:
about 1 year 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 makandropedia