Say we want …
- to create a
Gallery
that has a name andhas_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 Show archive.org snapshot . It's a mature library that can do the job frontend-wise. On the server, we'll use Carrierwave Show archive.org snapshot , 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 = 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.
# 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 Show archive.org snapshot 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 = $(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()
Posted by Dominik Schöler to makandra dev (2016-06-14 05:57)