Sean Kelley
Sean Kelley

Reputation: 501

jQuery File Upload in Rails 4.x Nested Form

I have a Horse model and a Photo model. I am using jQuery File Upload to resize (client side) images and save on Amazon s3 directly since I am using Heroku.

I have seen other questions similar that use carrierwave, paperclip, or that are very old. I am not sure why you would use carrierwave/paperclip, but I think based on what heroku says, I do not want to have images hitting the server potentially causing time-outs.

Heroku recommends using jQuery File Upload and shows js appending new file input with a value of the image's link (returned from amazon s3). I have this working when saving a photo separately. I now want to make it work in a nested form for Horse but js is not finding input (since it does not exist yet because it's nested I presume).

I am using Cocoon for nested forms (I am open to anything that will work better). I am not too familiar with javascript/jQuery but a far as I can tell, Cocoon 'hides' the nested element until I click to add it via the add_association.

haml view code:

= link_to_add_association 'add photo', f, :photos

html source before clicking 'add photo'

<div class='links'>
<a class="btn btn-default btn-sm add_fields" data-association-insertion-method="after" data-association="photo" data-associations="photos" data-association-insertion-template="&lt;div class=&#39;nested-fields&#39;&gt;
  &lt;fieldset class=&quot;inputs&quot;&gt;&lt;ol&gt;&lt;input type=&quot;file&quot; name=&quot;horse[photos_attributes][new_photos][url]&quot; id=&quot;horse_photos_attributes_new_photos_url&quot; /&gt;
  &lt;input type=&quot;hidden&quot; name=&quot;horse[photos_attributes][new_photos][_destroy]&quot; id=&quot;horse_photos_attributes_new_photos__destroy&quot; value=&quot;false&quot; /&gt;&lt;a class=&quot;remove_fields dynamic&quot; href=&quot;#&quot;&gt;remove photo&lt;/a&gt;
  &lt;/ol&gt;&lt;/fieldset&gt;
&lt;/div&gt;
" href="#">add photo</a>

How do I work with this input and how do I handle multiple file uploads as they are added correctly?

My current upload js:

$(function() {
  if ($('#new_horse').length > 0) {
    $.get( "/presign", function( s3params ) {

  $('.direct-upload').find("input:file").each(function(i, elem) {
    var fileInput    = $(elem);
    var form         = $(fileInput.parents('form:first'));
    var submitButton = form.find('input[type="submit"]');
    var progressBar  = $("<div class='bar'></div>");
    var barContainer = $("<div class='progress'></div>").append(progressBar);

    fileInput.fileupload({
      fileInput:       fileInput,
      url:             "http://" + s3params.url.host,
      type:            'POST',
      autoUpload:       true,
      formData:         s3params.fields,
      paramName:        'file', // S3 does not like nested name fields i.e. name="user[avatar_url]"
      dataType:         'XML',  // S3 returns XML if success_action_status is set to 201
      disableImageResize: false,
      imageQuality: 0.5,
      disableImageResize: /Android(?!.*Chrome)|Opera/ 
        .test(window.navigator && navigator.userAgent),
      imageMaxWidth: 500,
      imageMaxHeight: 1000,
      imageOrientation: true,  //auto rotates images
      acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, //I added this to jquery.fileupload-validate: alert('Must Be JPG GIF or PNG Image')
      replaceFileInput: false,
      progressall: function (e, data) {
        var progress = parseInt(data.loaded / data.total * 100, 10);
        progressBar.css('width', progress + '%')
      },
      start: function (e) {
        submitButton.prop('disabled', true);
        fileInput.after(barContainer);
        progressBar.
          css('background', 'green').
          css('display', 'block').
          css('width', '0%').
          text("Loading...");
      },
      done: function(e, data) {
        submitButton.prop('disabled', false);
        progressBar.text("Pre-uploading done... Please Save or Cancel");

        // extract key and generate URL from response
        var key   = $(data.jqXHR.responseXML).find("Key").text();
        var url   = 'https://' + s3params.url.host +'/' + key;

        // remove first input to prevent phantom upload delay
       fileInput.remove();

        // create new hidden input with image url
        var input = $("<input />", { type:'hidden', name: fileInput.attr('name'), value: url })
        var imgPreview =  '<img src="' + url + '">';

        form.append(input);
        form.append(imgPreview);
      },
      fail: function(e, data) {
        submitButton.prop('disabled', false);

        progressBar.
          css("background", "red").
          text("Failed");
      }
    });
  });

    }, 'json');
  }
});

Upvotes: 0

Views: 372

Answers (1)

Sean Kelley
Sean Kelley

Reputation: 501

I guess I should have looked at cocoon documentation first: http://www.rubydoc.info/gems/cocoon#Callbacks__upon_insert_and_remove_of_items_ http://www.rubydoc.info/gems/cocoon/1.2.6

I modified my upload.js file to the following and it worked for multiple files in nested forms perfectly:

// added for file uploading
// https://devcenter.heroku.com/articles/direct-to-s3-image-uploads-in-rails
// Get our s3params from our endpoint

$(document).on('ready page:load', function () {
  $('.direct-upload')
    .on('cocoon:after-insert', function(e, photo) {
      console.log('inside cocoon image function');

      $.get( "/presign", function( s3params ) {
         $('.direct-upload').find("input:file").each(function(i, elem) {
           console.log('inside nested-fields photo input form');

          var fileInput    = $(elem);
          var form         = $(fileInput.parents('form:first'));
          var submitButton = form.find('input[type="submit"]');
          var progressBar  = $("<div class='bar'></div>");
          var barContainer = $("<div class='progress'></div>").append(progressBar);

          fileInput.fileupload({
            fileInput:       fileInput,
            url:             "http://" + s3params.url.host,
            type:            'POST',
            autoUpload:       true,
            formData:         s3params.fields,
            paramName:        'file', // S3 does not like nested name fields i.e. name="user[avatar_url]"
            dataType:         'XML',  // S3 returns XML if success_action_status is set to 201
            disableImageResize: false,
            imageQuality: 0.5,
            disableImageResize: /Android(?!.*Chrome)|Opera/ 
              .test(window.navigator && navigator.userAgent),
            imageMaxWidth: 500,
            imageMaxHeight: 1000,
            imageOrientation: true,  //auto rotates images
            acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, //I added this to jquery.fileupload-validate: alert('Must Be JPG GIF or PNG Image')
            replaceFileInput: false,
            previewMaxWidth: 100,
            previewMaxHeight: 100,
            previewCrop: true,
            progressall: function (e, data) {
              var progress = parseInt(data.loaded / data.total * 100, 10);
              progressBar.css('width', progress + '%')
            },
           start: function (e) {
              submitButton.prop('disabled', true);
              fileInput.after(barContainer);
              progressBar.
              css('background', 'green').
              css('display', 'block').
              css('width', '0%').
              text("Loading...");
           },
           done: function(e, data) {
              submitButton.prop('disabled', false);
              progressBar.text("Photo Uploaded");

              // extract key and generate URL from response
              var key   = $(data.jqXHR.responseXML).find("Key").text();
              var url   = 'https://' + s3params.url.host +'/' + key;

              // remove first input to prevent phantom upload delay
             fileInput.remove();

              // create new hidden input with image url
              var input = $("<input />", { type:'hidden', name: fileInput.attr('name'), value: url })
              var imgPreview =  '<img src="' + url + '">';

              form.append(input);
              form.append(imgPreview);
            },
            fail: function(e, data) {
              submitButton.prop('disabled', false);
              progressBar.
              css("background", "red").
              text("Failed");
            }

    }, 'json'); //fileupload
   }); //each file 

 }); //presign call

});    // function cocoon
});    // page ready  

I guess Google and a well documented gem can replace knowledge of JS in the short term :-) I am sure it's not at tight as it could be so please offer any improvements.

Upvotes: 1

Related Questions