Created by @pinkavam in Q&As
@pinkavam
@pinkavam

I create multiupload according to demo project on GitHub which send images together, but I need upload files separetely. It is possible?

If not, I will use Dropzone.js https://www.dropzonejs.com/ which work as I want. But I don't know how to pass response from server to Nittro, to apply snippets, flashes and etc.

It is possible to process response from request which is not created by Nittro?

@jahudka
@jahudka

Hi, uploading files separately is indeed possible - you'll just need to do some of the heavy lifting yourself.

  • If your Dropzone is bound to a Nette Forms upload field then from the server's point of view you probably want the field to not allow multiple files, since otherwise you'd always receive an array with one item in the success handler instead of getting the upload directly. That bit's easy - just use $form->addUpload() instead of $form->addMultiUpload() (or don't set the $multiple argument of $form->addUpload() to true).
  • You might still want to allow users to drop / select in the file picker multiple files at once while processing them separately at the backend - to enable that, just add the multiple attribute to the file input manually. The only issue here is that you can only do that by rendering the whole form manually - if you try setting the attribute using something like $form->addUpload()->setAttribute('multiple', true), you'll find that Nette will behave just like if you passed $multiple = true to $form->addUpload().
  • In your client-side code you can use the page.open() method to dispatch the uploads like this:

    _stack.push(function(di) {
       var form = di.getService('formLocator').getForm({form.id myForm});
    
       var dz = di.create('dropZone', { from: form.getElement('upload') });
    
       dz.on('file', function(evt) {
           // prevent enqueueing the file in Dropzone's internal queue
           evt.preventDefault();
    
           // clear the input field's name to prevent it from being serialized
           var inp = form.getElement('upload');
           inp.name = '';
    
           // serialize form and append file
           var data = form.serialize();
           data.append('upload', file);
    
           // reset input field's name
           inp.name = 'upload';
    
           var formEl = form.getElement();
    
           di.getService('page').open(formEl.action, formEl.method, data, {
               // to make data attributes on the form element work:
               element: formEl,
               // alternatively you can specify most options
               // like 'history' and 'transition' here
    
               // enable uploads to run in parallel:
               background: true
           });
       });
    });
@pinkavam
@pinkavam

Tanks for your anwser. I treid it and it works.

But if I set field.name = '' , I get an error Form.js:129 Uncaught TypeError: Invalid argument to setValue(), must be (the name of) an existing form element.

And if I don't set field.name = '', this part of code

var data = form.serialize();
data.append(fieldName, evt.data.file);

add two objects with same name - fieldName. So I need to remove one of that. Have you any solution?

Second thing what I need is to do some preview with progress bar. I add some table under field and in dropzone.on('file', callback) event I add row with preview. Next in di.getService('page').open(...) I used transition property, which I set to that row. Transition is applied to row. I need to remove that row after request is complete.

Has the di.getService('page').open(..) some callback after request is successful or unsuccessful?

@jahudka
@jahudka

Ouch, my bad, I see now. I updated the code in my previous reply.

When you derive a Dropzone instance from an existing form input field, Dropzone will sort of 'hijack' the input field in order to normalise UI behaviour - no matter how you select the files to be uploaded (drag & drop vs. using the input field), Dropzone will handle both cases using the same logic. If you select the files using the input field though then Dropzone needs to reset the field after processing the files, otherwise they'd get uploaded twice. The thing is that within a file event handler the field isn't reset yet - that's why I added the field.name = ''; bit originally - but right after handling all the selected files Dropzone will attempt to reset the field value and it assumes the field has a name by then.

The updated code only resets the input field's name for the form.serialize() call and restores it right after, so you should get neither the TypeError you mentioned nor the duplicated files in the serialised data.

As for the progress bar - the open() method returns a Promise, so you can just do:

page.open(/* ... */).then(
    function() { /* cleanup when successful */ },
    function() { /* cleanup when an error occurred */ }
);

Also you could animate the progress bar using actual upload progress metrics - this is a bit harder because you need to gain access to the underlying AJAX requests, but just the ones that are actually uploads from this Dropzone instance. This is how you could do it:

_stack.push(function(di) {
    var form = di.getService('formLocator').getForm({form.id myForm});

    // init dropzone etc. as before, except before calling `page.open()`
    // you'd create a progress bar and pass it along in the last parameter
    // to `page.open()` - e.g. under the key `myProgressElem`

    var page = di.getService('page');

    page.on('transaction-created', handleTransaction);

    page.getSnippet({form.id myForm}).teardown(function() {
        // clean up when leaving the page containing the form
        page.off('transaction-created', handleTransaction);
    });

    function handleTransaction(evt) {
        if (evt.data.context.myProgressElem) {
            evt.data.transaction.on(
                'ajax-request',
                handleRequest.bind(null, evt.data.context.myProgressElem)
            );
        }
    }

    function handleRequest(progressElem, evt) {
        evt.data.request.on('progress', function(evt) {
            // e.g.
            progressElem.style.width = (100 * evt.data.loaded / evt.data.total).toFixed(2) + '%';
        });
    }
});

Sign in to post a reply