Dropzone: AJAX Upload

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When I started creating this tutorial, I got a lot of requests for things to talk about... which, by the way - thank you! That was awesome! Your requests absolutely helped drive this tutorial. One request that I heard over and over again was: handling multiple file uploads at a time.

It makes sense: instead of uploading files one-by-one, an author should be able to select a bunch at a time! This is something that's totally supported by the web: if you add a multiple attribute to a file input, boom! Your browser will allow you to select multiple files. In Symfony, we would then be handling an array of UploadedFile objects, instead of one.

But, I'm not going to show how to do that. Mostly... because I don't like the user experience! What if I select 10 files, wait for all of them to upload, then one is too big and fails validation? If you're not inside a form, you could probably save 9 of them and send back an error. But if you're inside a form, good luck: unless you do some serious work, none of them will be saved because the entire form was invalid!

I also want my files to start uploading as soon as I select them and I want a progress bar. Basically... I want to handle uploads via JavaScript. In fact, over the next few videos, we're going to create a pretty awesome little widget for uploading multiple files, deleting them, editing their filenames and even re-ordering them.

Installing Dropzone

First: the upload part. Google for a library called Dropzone: it's probably the most popular JavaScript library for handling file uploads. It creates a little... "drop zone"... and when you drop a file here or select a file, it starts uploading. Super nice!

Search for a Dropzone CDN. I normally use Webpack Encore, and so, whenever I need a third-party library, I install it via yarn and import it when I need to use it. If you're using Encore, you can do this - and I recommend it. But in this tutorial, to keep things simple, we're not using Encore. And so, in our edit template, we're including a normal JavaScript file that lives in the public/js/ directory: admin_article_form.js, which holds some pretty traditional JavaScript.

To get Dropzone rocking, copy the minified JavaScript file and go to the template Actually, copy the whole script tag with SRI - that'll include the nice integrity attribute.

... lines 1 - 47
{% block javascripts %}
... lines 49 - 50
<script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.js" integrity="sha256-cs4thShDfjkqFGk5s2Lxj35sgSRr4MRcyccmi0WKqCM=" crossorigin="anonymous"></script>
... line 52
{% endblock %}

Grab the minified link tag too. We don't have a stylesheets block yet, so we need to add one: {% block stylesheets %}{% endblock %}, call {{ parent() }} and paste the link tag.

... lines 1 - 41
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.css" integrity="sha256-e47xOkXs1JXFbjjpoRr1/LhVcqSzRmGmPqsrUQeVs+g=" crossorigin="anonymous" />
{% endblock %}
... lines 47 - 54

Dropzone basically "takes over" your form tag. You don't need a button anymore... or even the file input. The form tag does need a dropzone class... but that's it!

... lines 1 - 2
{% block content_body %}
... lines 4 - 7
<div class="row">
... lines 9 - 14
<div class="col-sm-4">
... lines 16 - 33
<form action="{{ path('admin_article_add_reference', {
id: article.id
}) }}" method="POST" enctype="multipart/form-data" class="dropzone">
</form>
</div>
</div>
{% endblock %}
... lines 41 - 54

Try it! Refresh and... hello Dropzone!

How Dropzone Uploads

When you select a file with Dropzone, it's smart enough to upload to the action URL on our form. So... in theory... it should just... sort of work.

Back in the controller, scroll up to the upload endpoint and dump($uploadedFile). I'm not using dd() - dump and die - because this will submit via AJAX - and by using dump() without die'ing, we'll be able to see it in the profiler.

... lines 1 - 19
class ArticleReferenceAdminController extends BaseController
{
... lines 22 - 25
public function uploadArticleReference(Article $article, Request $request, UploaderHelper $uploaderHelper, EntityManagerInterface $entityManager, ValidatorInterface $validator)
{
/** @var UploadedFile $uploadedFile */
$uploadedFile = $request->files->get('reference');
dump($uploadedFile);
... lines 31 - 76
}
... lines 78 - 101
}

Ok: select a file. The first cool thing is that the file upload AJAX request showed up down on the web debug toolbar! I'll click the hash and open that up in a new tab.

This is awesome! We're now looking at all the profiler data for that AJAX request! Actually... hmm... that's not true. Look closely: it says that we were redirected from a POST request to the admin_article_add_reference route. We're looking at the profiler for the article edit page!

This is a bit confusing. Click the "Last 10" link to see a list of the last 10 requests made into our app. Now it's more obvious: Dropzone made a POST request to /admin/article/41/references - that's our upload endpoint. But, for some reason, that redirected us to the edit page. Click the token link to see the profiler for the POST request.

Check out the Debug tab. There it is: this is the dump from our controller... and it's null. Where's our upload? The problem is that, by default, Dropzone uploads a field called file. But in the controller, we're expecting it to be called reference.

Customizing Dropzone

We could fix this in the controller... but we can also configure Dropzone to use the reference key. We're going to do that because, in general, as cool as it is that we can just add a "dropzone" class to our form and it mostly works, to really get this system working, we're going to need to customize a bunch of things on Dropzone.

Open up admin_article_form.js. First, at the very top, add Dropzone.autoDiscover = false. That tells Dropzone to not automatically configure itself on any form that has the dropzone class: we're going to do it manually.

Dropzone.autoDiscover = false;
... lines 3 - 42

Try it out - close the extra tab and refresh. Hmm... still there? Maybe a force refresh? Now it's gone. The dropzone class still gives us some styling, but it's not functional anymore.

To get it working again, inside the document.ready(), call a new initializeDropzone() function.

... lines 1 - 2
$(document).ready(function() {
initializeDropzone();
... lines 5 - 29
});
... lines 31 - 42

Copy that name, and, below, add it: function initializeDropzone(). If I were using Webpack Encore, I'd probably organize this function into its own file and import it.

... lines 1 - 31
function initializeDropzone() {
... lines 33 - 40
}

The goal here is to find the form element and initialize Dropzone on it. To do that, let's add another class on the form: js-reference-dropzone.

... lines 1 - 2
{% block content_body %}
... lines 4 - 7
<div class="row">
... lines 9 - 14
<div class="col-sm-4">
... lines 16 - 33
<form action="{{ path('admin_article_add_reference', {
id: article.id
}) }}" method="POST" enctype="multipart/form-data" class="dropzone js-reference-dropzone">
</form>
</div>
</div>
{% endblock %}
... lines 41 - 54

Copy that, and back inside our JavaScript, say var formElement = document.querySelector() with .js-reference-dropzone.

... lines 1 - 31
function initializeDropzone() {
var formElement = document.querySelector('.js-reference-dropzone');
... lines 34 - 40
}

Yes, yes, I'm using straight JavaScript here instead of jQuery to be a bit more hipster - no big reason for that. There's also a jQuery plugin for Dropzone. Next, to avoid an error on the "new" form that doesn't have this element, if !formElement, return.

... lines 1 - 31
function initializeDropzone() {
var formElement = document.querySelector('.js-reference-dropzone');
if (!formElement) {
return;
}
... lines 37 - 40
}

Finally, initialize things with var dropzone = new Dropzone(formElement). And now we can pass an array of options. The one we need now is paramName. Set it to reference.

... lines 1 - 31
function initializeDropzone() {
var formElement = document.querySelector('.js-reference-dropzone');
if (!formElement) {
return;
}
var dropzone = new Dropzone(formElement, {
paramName: 'reference'
});
}

That should do it! Head over and select another file - how about earth.jpeg. And... cool! It looks like it worked. Click to open the profiler for the AJAX request.

Oh... careful - once again, we got redirected! So this is the profiler for the edit page. Click the link to go back to the profiler for the POST request and go back to the Debug tab. Yes! Now we're getting the normal UploadedFile object.

Close this and refresh. Look at the list! There is earth.jpeg! It worked! Of course, it's a little weird that it redirected after success... and if there were a validation error... that would also cause a redirect... and so it would look successful to Dropzone. The problem is that our endpoint isn't set up to be an API endpoint. Let's fix that next and make Dropzone read our validation errors.

Leave a comment!

  • 2020-02-06 Victor Bocharsky

    Hey Anton,

    This comment might be useful for you, please, take a look at it: https://symfonycasts.com/sc... . But really, having nice JS library that helps to handle multiple uploads is enough. Fairly speaking, upload files one by one technically has less possible problems, and if you have a nice JS library with nice UI and UX - that's perfect. You should not care about how it works behind the scene - all the job is done by JS library. What you do care is UX and that handling single file uploads on the server side is just easier. So, as for me, the ideal scheme for uploading multiple files - have a JS library with nice UX that allows users to select multiple files at once and that uploads those files one by one to the server side where your PHP script handles those uploads.

    I hope this helps!

    Cheers!

  • 2020-02-05 Anton Pool

    Sorry, but what about the real multiple files uploading?The Dropzone.js uploads the files one by one. Can you please advice how to count then the uploaded files (at once) on the server side? Thank you

  • 2019-11-19 Roberto Santana

    WOW! Thanks for your reply!

  • 2019-11-18 weaverryan

    Hey Roberto Santana !

    Great question :). I cheat.. and I feel great about it :). If I have an upload on some sort of an entity, I *always* will save the original object (e.g. Article) first. It just makes life MUCH simpler. There are bundles out there that have really cool fanciness to save files in a temporary location that you can then read when your form actually submits... but I've never thought it worth the trouble. For an Article, it would look like this:

    Add a step 1 where the user at least needs to set a "title" so you have *something* to add to the database. You'll then need to put any "NotBlank" annotations you want in some validation group that will only be activated on the 2nd step. Btw, for that "step 1". I've even seen it where it looks like 1 form, but first you only see the title. Once you type that in, it saves via AJAX and the rest of the form loads.

    In the end, the problem you're referring to simply is a tricky one. That's why I like to cheat. There are bundles that can help (I think https://github.com/1up-lab/... has this), but it ultimately comes down to some tricks. For example, you could also allow your related objects to save with a null article_id... store THEIR ids in the session (or, more fancy, return their ids back, and put them in the form as hidden fields) and set them when the form FINALLY saves (and have a process to clean up old records for forms that NEVER got submitted successfully).

    Let me know if this helps :).

    Cheers!

  • 2019-11-14 Roberto Santana

    Hi! How do you solve the new item issue? What I mean is, I create a new Article, in the form I fill all the fields and upload some files.. we don't know the new article_id, how do you store them? How do you do the relationship? Thanks

  • 2019-04-29 weaverryan

    Hey Skylar Scotlynn Gutman!

    Hmm, maybe? :) There are two parts to that:

    1) First, you'll need to base64 encode the uploaded file data so that it can be sent. That *does* appear to be possible: https://stackoverflow.com/q...

    2) Then you need to teach Dropzone to send JSON, instead of the normal format. I'm not sure that's possible. Someone forked the entire library to add it - https://github.com/slothbag... but they changed a lot of core code to make it possible. Someone else "sorta" did it - https://github.com/rowanwin... - but that's a bit of a hack: they are *still* sending a traditional form submit, but with JSON as one of the keys (it's not *really* a JSON formatted request).

    So... it looks like... kind of :). If you want an "pure" JSON upload endpoint for some other use (beyond Dropzone), the easier solution might be to have your endpoint support both JSON input and normal, form-submitted input (from Dropzone).

    Cheers!

  • 2019-04-26 Skylar Scotlynn Gutman

    Hey guys,
    Is it possible for dropzone to send the form data as json?

  • 2019-04-16 weaverryan

    Hey Sasa Milivojevic!

    Makes sense! Honestly, what you probably want is a setup like what we have here - where you use Dropzone outside of a a form to create the Medias and relate it to whatever object you need (you would need to send up some additional flag in the URL to say which object the Media should be related to). But, there are so many variants on how you can want this to look and work.

    So, are you creating a traditional Symfony form then adding a Dropzone inside of it? By default, Dropzone will use the action="" of your form tag as the upload URL. You *can* by passing a url option when you initialize Dropzone. But then, Dropzone isn't really using your form... it's still uploading independently of your form.

    That's a long way of saying: what exactly are you trying to accomplish by putting it into a form? What is the user flow you imagine?

    Cheers!

  • 2019-04-16 Sasa Milivojevic

    Hi!!!!

    I'm glad that You understand my comment in the right way :-), I enjoy watching this tutorial.

    So my idea is to create one entity - Medias, and only in Medias to upload files. Medias will be related to every entity that we need picture (Blog entity, User entity....). From user perspective, user can upload more picture at once in Medias (somehow), and then he can use those pictures (one or more) for blogs or blog or other entity where picture is needed. And at the end I want to copy that entity, and logic for upload from project to project and reuse that, and of course I want to use symfony components(forms). And now when I use symfony forms dropzone trow error that he can find url.

  • 2019-04-16 weaverryan

    Hey Sasa Milivojevic!

    Haha, we could! But this form would just have one field... and that field would be an instance of an UploadedFile object. So, I'm not sure the form would give us much in this case. But, what do you think?

    Cheers!

  • 2019-04-16 Sasa Milivojevic

    And why, just why we don't use Symfony forms????????

  • 2019-04-02 Diego Aguiar

    Cheers!!

  • 2019-04-02 Krzysztof Krakowiak

    Thanks!!! :D