Search code examples
javascripthtmlinputfilelist

Programmatically changing value of input type file?


I'm trying to produce a bit of code that I can reuse throughout my site which is, essentially, a photos picker/chooser with some validation. Here is my code:

class PhotoPicker
{
    constructor(element)
    {
        this.element = element;

        //Creating needed HTML Markup
        this.createMarkUp();

        //FileList of valid data.
        this.validFiles = new DataTransfer();

        //Initialise Picker.
        this.input.onchange = () => {
            this.updateOutput();
            this.output.files = this.validFiles.files;
        }
    }

    updateOutput()
    {
        const files = Array.from(this.input.files);
        files.forEach((file) => {                
            let reader = new FileReader();
            reader.readAsDataURL(file);
            reader.onload = () => {
                photo = this.createPhotoThumb({
                    url : reader.result,
                    name: file.name,
                    size: file.size
                });
                if (this.validatePhoto(file)) {
                    this.validFiles.items.add(file);
                };
            };
        });
    }

    createMarkUp()
    {
        //Creating needed HTML Markup
    }
    createPhotoThumb(data = {})
    {
        //Creating a photo thumbnail for preview
    }
    validatePhoto(photo)
    {
        //Validating the photo
    }
}

What's happening is when I do the first selection of some images, thumbnails are displayed and the list of valid files this.validFiles.files gets updated, but NOT the final list which I plan to send to the server this.output.files , however, on a second attempt, it works! where the final list gets updated with files from the first selection and NOT the second, and so on.. with every selection the files from the previous one are added to the final list but not files from last selection.


Solution

  • I think the problem is that you expect that

    reader.onload = () => {
        photo = this.createPhotoThumb({
            url : reader.result,
            name: file.name,
            size: file.size
        });
        if (this.validatePhoto(file)) {
            this.validFiles.items.add(file);
        };
    };
    

    get's executed before you assign the valid files to this.output.files.

    But reader.onload get executed asynchronously, so that your assignment of the valid files to this.output.files gets executed before the valid files are added to the array of valid files.

    You have to implement some logic that waits for the completeness of the onload handlers of your readers.

    This would be a possible solution:

    class PhotoPicker
    {
        constructor(element)
        {
            this.element = element;
    
            // Creating needed HTML Markup
            this.createMarkUp();
    
            // FileList of valid data.
            this.validFiles = new DataTransfer();
    
            // Initialise Picker.
            this.input.onchange = () => {
                this.updateOutput()
                    .then(() => {
                        this.output.files = this.validFiles.files;
                    });
            }
        }
    
        updateOutput()
        {
            const files = Array.from(this.input.files);
            const fileLoaderPromises = [];
            files.forEach((file) => {
                const promise = new Promise((resolve) => {
                    let reader = new FileReader();
                    reader.readAsDataURL(file);
                    reader.onload = () => {
                        photo = this.createPhotoThumb({
                            url : reader.result,
                            name: file.name,
                            size: file.size
                        });
                        if (this.validatePhoto(file)) {
                            this.validFiles.items.add(file);
                        };
                        // Mark the file loader as "done"
                        resolve();
                    };
                })
    
                // Add the promise to the list of file loader promises
                fileLoaderPromises.push(promise);
            });
    
            // Return a promise which resolves as soon as all file loader promises are done
            return Promise.all(fileLoaderPromises);
        }
    
        // ...
    }