Rails does not help much when dealing with AJAX uploads by means of external JS libraries. I recently came across a case where a user on www.codementor.io was struggling to use JQuery-upload-file to upload a video to a Rails backend. The main reason to use the library was the progress bar feature, something that is missing in the Rails world. In this blog post I’ll show you how to implement the functionality in the simplest case possible.
The interface looks as follows:
The view looks like follow:
1 2 3 4 5 |
= form_for @movie do |m| = m.text_field :title, placeholder: 'Title' = m.text_area :description, placeholder: 'Description', rows: '3' = m.file_field :video = m.submit 'Upload', id: 'fileUpload', data: { disable_with: 'Uploading' } |
As you can imagine there is a Movie model in the backend ready to be created. The data: { disable_with: ‘Uploading’ } bit will disable the button when we submit our form. The actual form submission will be handled by jquery-upload-file.
On the JS code this is what we need now:
1 2 3 4 5 |
$(document).ready(function() { var uploadObj = $("#movie_video").uploadFile({ url: "/movies", multiple: false, fileName: "movie |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
", autoSubmit: false, formData: { "movie[title]": $('#movie_title').text(), "movie[description]": $('#movie_description').text() }, onSuccess:function(files,data,xhr) { window.location.href = data.to; } }); $("#fileUpload").click(function(e) { e.preventDefault(); $.rails.disableFormElements($($.rails.formSubmitSelector)); uploadObj.startUpload(); }); }); |
This is far from perfect, but bear with me for the sake of this example. Javascript is populating the formData with the form parameters which will arrive to the backend together with the video. When the user click the submit button we do the following: (i) we avoid the default form submission with e.preventDefault() and (ii) we disable the button for multiple submissions with $.rails.disableFormElements($($.rails.formSubmitSelector));
The create action in the controller looks as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def create @movie = Movie.create(movie_params) @movie.user = current_user if @movie.save render :json => { :status => :redirect, :to => movie_path(@movie.id) }.to_json else render 'new' end end |
A gotcha to keep in mind is that we do not use redirect_to
here because we want the JS code to handle the redirection explicitly. This is why we return the new path in the JSON response. The create action is taking the parameters from the request and attaching the video to the model using the paperclip and paperclip-av-transcoder gem.
This is an example of parameters that we receive in the controller:
1 2 3 4 5 6 |
[1] pry(#<MoviesController>)> params => {"movie"=> {"video"=> #<ActionDispatch::Http::UploadedFile:0x007fb1d00f31b8 @content_type="video/quicktime", @headers="Content-Disposition: form-data; name=\"movie |
1 2 3 4 5 6 7 |
"; filename=\"TestVideo.mov\"\r\nContent-Type: video/quicktime\r\n", @original_filename="TestVideo.mov", @tempfile=#<File:/var/folders/fj/68q9tlrj4h30xpc_xxhbb4h80000gn/T/RackMultipart20150523-36995-mrhxma.mov>>, "title"=>"Cool video", "description"=>"This is a great video indeed"}, "controller"=>"movies", "action"=>"create"} |
The model will have the following (magic) code to attach the video:
1 2 3 4 5 6 7 |
has_attached_file :video, styles: { :medium => { :geometry => "640x480", :format => 'mp4' }, :thumb => { :geometry => "160x120", :format => 'jpeg', :time => 10} }, :processors => [:transcoder] |
Finally the controller simply returns a JSON object with the url for the next page where the user should be redirected after the upload is done. The JSON is picked up by the onSuccess() callback of the the jquery-upload-file library. At this point a simple window.location.href will do the trick.
Hope it is useful,
Enjoy
Disclaimer: This blog post covers the fastest way to have a progress bar for file uploads working on Rails. However I highly encourage you to allocate time for more configurable and maintainable solutions likes this one http://www.sitepoint.com/asynchronous-file-uploads-rails/
that is o good trick. It works for simple things. That solution solved my problem. Thank you.
Thanks Bruno! Glad you liked it
Thanks 🙂 It’s solved my problem 😀
I am glad it did!
And how can I show the video, as video_tag?
Yes Sebastián, you can use the Rails video_tag method to embedd the video in the page.
I don’t understand… You use: $(‘#movie_description’) but I don’t find that id anywhere in the view. Where did you put it?
Also, if my upload needs to be associated with an user, how can the app know which user is currently logged in? I had a hidden field to put there the member_id but that’s not the best way for sure :X
Hi Rui,
By convention the Rails form_for generates this id for the view. It is weird that you don’t find it. Are you using the same code I posted?
Usually to keep track of the current user you rely on external gems like Devise. If you don’t want to go the gem way you can always keep the user_id in the session object of Rails. Check http://stackoverflow.com/questions/12719958/rails-where-does-the-infamous-current-user-come-from
Let me know if I can help further!
Thank you for your tutorial first
However, I come cross a problem that nothing happens, when I click submit button .
I think the problem is that the upload ajax form created by the plugin just doesn’t submit properly, but I don’t know how to solve it.
Any help would be appreciated
Hi daorren, there is any code that you can share to see what could be the issue?
Above are all my code, only the model changes from your movie to this pin.
f.input :image, input_html: { class: 'form-control'}
This statement create a simple input, but I can see a rather complex div in HTML through Chrome Devtool.So I conclude that the plugin initial statement works. And my problem is that when I click the submit button, nothing happens. No request incoming in my rails log, only the button being disabled.
Made a mistake
line 38 is not commented actually
Hi daorren,
have you checked if using a normal form instead of simple_form changes anything?
I see nothing obvious in there, but the problem is definitely in the frontend.
Hope this helps,
Alfredo
I have done mainly 2 things to make things work
First, instead of using ‘m.file_field :video’
I just use a empty div, for the plugin to do its work
Secondly, making some changes to the variables in js
we shouldn’t use formData, or it would always be empty, because it’s called when the dom is loaded. Instead we should use dynamicFormData, as the project maintainer mentioned when he answer others’ questions
and we should use val() instead of text() in form
what to write in URL if I ‘m doing this for create/update