Israel Obanijesu
Israel Obanijesu

Reputation: 696

ReactJS: Preview multiple images before upload

I am trying to preview multiple images in the browser before they are uploaded to the server using ReactJS and the FileReader() API. The problem that I have However is every time I select some images for preview, only the last image is displayed.

My code looks like this:

class App extends Component {
    constructor(props){
        super(props);
        this.state = {
            id: "upload-photo",
            imageURI: null
        }
    }

    buildImgTag(){
        let imgTag = null;
        if (this.state.imageURI !== null) {
            imgTag = (
                <div className="photo-container">
                    <img className="photo-uploaded" src={this.state.imageURI} alt="Photo uploaded"/>
                </div>
            );
            return imgTag;
        }
    }

    readURI(e){
        if (e.target.files) {
            let filesAmount = e.target.files.length;
            let i;
            for (i = 0; i < filesAmount; i++) {
                let reader = new FileReader();
                reader.onload = function(ev) {
                    this.setState (
                        {
                            imageURI: ev.target.result
                        }
                    )
                }.bind(this);
                reader.readAsDataURL(e.target.files[i]);
            }
        }
    }

    handleChange(e){
        this.readURI(e);
        if (this.props.onChange !== undefined) {
            this.props.onChange(e);
        }
    }

    render() {
        const imgTag = this.buildImgTag();

        return (
            <div className="container">
                <div className="row justify-content-center">

                    <div className="col-md-6">
                        <div className="card">
                            <div className="card-header" style={{backgroundColor: 'rgb(232, 245, 253)', borderTopLeftRadius: '4px', borderTopRightRadius: '4px', display: 'flex', maxHeight: '50vh', minHeight: '25vh', overflow: 'hidden'}}>
                                <div className="avatar">
                                    <img src="http://laratweet.local:8080/images/avatar-default.png" alt="User Avatar" className="user-avatar"/>
                                </div>
                                <div id="textEditor">
                                    <form method="post" action="" encType="multipart/form-data">
                                        <textarea name="" id="richTextArea" placeholder="What's happening?"></textarea>
                                        {imgTag}
                                        <div id="theRibbon">
                                            <div>
                                                <input
                                                    id={this.state.id}
                                                    type="file"
                                                    name=""
                                                    accept="image/gif,image/jpeg,image/jpg,image/png,video/mp4,video/x-m4v"
                                                    title="Add photos or video"
                                                    onChange={this.handleChange.bind(this)}
                                                    multiple
                                                />
                                                <label htmlFor={this.state.id}>
                                                    <figure>
                                                        <svg xmlns="http://www.w3.org/2000/svg" width="30" height="27"
                                                             viewBox="0 0 20 17" className="upload-icon">
                                                            <path
                                                                d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"/>
                                                        </svg>
                                                    </figure>
                                                    <span className="tooltiptext">Add photos or video</span>
                                                </label>
                                            </div>
                                            <button type="submit" className="tweet">Tweet</button>
                                        </div>
                                    </form>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

I expect all images selected to be displayed / previewed in the browser but only the last image is previewed. What am I doing wrong?

Upvotes: 2

Views: 9878

Answers (3)

Elvis Moraes
Elvis Moraes

Reputation: 41

Tentei durante um dia todo a resposta do amigo @Dacre Denny e acabei utilizando a primeira linha de sua resolução, ambas as respostas são nativas do JavaScript mas parece que o FileReader() deve servir para aplicações mais complexas como por exemplo salvar as imagens em base64 em um banco de dados, não tenho certeza...

Acabei resolvendo sem converter para Base64 de uma forma muito "mais simples" Primeiro declarei meus States

const [files, setFiles] = useState([]);
const [previews, setPreviews] = useState([]);
const [files, setFiles] = useState([]);

...

const handleUpload = (event) => {
const fileList = Array.from(event.target.files);

setFiles(fileList); //Passei o valor de fileList para o State Files

const mappedFiles = fileList.map((file) => ({
  ...file,
  preview: URL.createObjectURL(file),
}));

setPreviews(mappedFiles); };

...Em meu Styled-Component de Upload:

<UploadPhoto type="file" id="files" accept="image/*" multiple="true" onChange={(e) => handleUpload(e)} />

E finalmente para exibição dos arquivos fiz um array.Map

if (previews.length > 0) {
  return (
    <FormGroup> //styled-component (div) para organizar os itens do form
      <UploadContainer> //styled-component (div) display: flex para suportar as imagens
        {previews.map((file) => <ImgUpload preview={file.preview} />)} //styled-component 
        //(div) que se repete de acordo com o número de imagens
      </UploadContainer>
    </FormGroup>
  );
}

A melhor forma para exibição das imagens em minha opinião é colocando como background-image de alguma div juntamente com as propriedades background-repeat: no-repeat, background-size: cover e background-position: 50% 50%

export const ImgUpload = styled.div`
flex-direction: row;
text-align: center;
margin: 10px;
background-image: ${(props) => (props.preview ? `url(${props.preview})` : null)} ;
min-height: 100px;
min-width: 100px;
border-radius: 5px;
background-repeat: no-repeat;
background-size: cover;
background-position: 50% 50%;
margin-right: 5px;
`;

Upvotes: 1

Dan
Dan

Reputation: 574

For those coming looking for the same answer, there is a cleaner option which doesn't involve faffing around with anything other than javascript:

HTML:

<!DOCTYPE html>
<html>
    <head>
        <title>File API - FileReader as Data URL</title>
    </head>
    <body>
        <header>
            <h1>File API - FileReader</h1>
        </header>
        <article>
            <label for="files">Select multiple files: </label>
            <input id="files" type="file" multiple/>
            <output id="result" />
        </article>
    </body>
</html>

CSS:

body
{
    font-family: 'Segoe UI';
    font-size: 12pt;
}

header h1
{
    font-size:12pt;
    color: #fff;
    background-color: #1BA1E2;
    padding: 20px;
}
article
{
    width: 80%;
    margin:auto;
    margin-top:10px;
}
.thumbnail
{
    height: 100px;
    margin: 10px;    
}

JAVASCRIPT

window.onload = function()
{

    //Check File API support
    if ( window.File && window.FileList && window.FileReader )
    {
        var filesInput = document.getElementById("files");

        filesInput.addEventListener 
        ( 
            "change", function ( event )
            {
                var files = event.target.files; //FileList object
                var output = document.getElementById ( "result" );

                for ( var i = 0; i< files.length; i++ )
                {
                    var file = files [ i ];

                    //Only pics
                    if ( !file.type.match ( 'image' ) )
                    continue;

                    var picReader = new FileReader();

                    picReader.addEventListener 
                    ( 
                        "load", function ( event )
                        {                    
                            var picFile = event.target;

                            var div = document.createElement ( "div" );

                            div.innerHTML = "<img class='thumbnail' src='" + picFile.result + "'" + "title='" + picFile.name + "'/>";

                            output.insertBefore ( div, null );
                        }
                    );

                   //Read the image
                   picReader.readAsDataURL ( file );
               }                               
           }
        );
    }
    else
    {
        console.log ( "Your browser does not support File API" );
    }
}

http://jsfiddle.net/0GiS0/Yvgc2/

Upvotes: 1

Dacre Denny
Dacre Denny

Reputation: 30360

The main problem here is that your App components state can currently only track one imageURI. Consider revising the state model so that an array of imageURI's can be stored and rendered:

this.state = {
    id: "upload-photo",
    imageArray: [] /* Replace imageURI with an array for multiple images */
}

Next, you'll need to update readURI() so that it stores multiple images in the component state. One approach would be to use Promise.all() to load an array of images asynchronously:

readURI(e){
    if (e.target.files) {

        /* Get files in array form */
        const files = Array.from(e.target.files);

        /* Map each file to a promise that resolves to an array of image URI's */ 
        Promise.all(files.map(file => {
            return (new Promise((resolve,reject) => {
                const reader = new FileReader();
                reader.addEventListener('load', (ev) => {
                    resolve(ev.target.result);
                });
                reader.addEventListener('error', reject);
                reader.readAsDataURL(file);
            }));
        }))
        .then(images => {

            /* Once all promises are resolved, update state with image URI array */
            this.setState({ imageArray : images })

        }, error => {        
            console.error(error);
        });
    }
}

Lastly, you'll just need to update buildImgTag() so that multiple images are rendered. One approach to that might be:

buildImgTag(){

    return <div className="photo-container">
    { 
      this.state.imageArray.map(imageURI => 
      (<img className="photo-uploaded" src={imageURI} alt="Photo uploaded"/>)) 
    }
    </div>
}

Also, here is a jsFiddle showing the file reading logic in action. Hope that helps

Upvotes: 5

Related Questions