Ad

How To Upload Multiple Images From Multiple Browsing In Laravel?

- 1 answer

This is my first question in Stack Overflow. Any suggestions woule be really appreciated.

I'm working on creating a website and the main function of it is to allow user to upload images as many as they want. It can come from different folders and users are able to browse for images as many times as they want. In addition, users can preview image they selected. In case users do not want one of photos selected at first. User can delete it by clicking the button near by the previewed image.

At View Part of Laravel, you will see the button to browse multiple files with the text to tell users how many photos selected right now. When I'm working with View Part, it seems to work perfectly. However, the result after store the form. It shows that it stored only the photos selected come from the last time browsing only.

To see more clear picture, here is the example.

  • User choose 4 photos which are A, B, C, and D
  • Those 4 photos are previewed and each of them have the deleted button next to it
  • User do not want D, so user delete it by clicking the button near the preview section
  • The left photos are A, B, and C
  • User want to upload more photos
  • User choose 3 more photos which are E, F, and G
  • The total number of photo to preview is 6 (A, B, C, E, F, G)
  • A, B, and C are from the first time browsing
  • E, F, and G are from the second time browsing
  • User do not want G, so user delete it by clicking the button near the preview section
  • The left photos are A, B, C, E, and F
  • Finally, user click submit button

The expected and correct result of storing this from is to have A, B, C, E, and F as the photos selected from user. However, it turn out that the database only store E and F which are the photos from the second time browsing.

I want to fix it so it can save the correct photos; in this case, A, B, C, E, and F.

I use javascript to add or remove the preview image and add the checkbox to store the name of the image selected. I will use this later in the Controller to check whether it is the file user want to store or not. Here is the code :

var totalFiles = [];

    function handleFileSelect(evt) {

        // FileList object
        var files = evt.target.files; 

        // Loop through the FileList and render image files as thumbnails.
        for (var i = 0, f; f = files[i]; i++) {

            // Only process image files.
            if (!f.type.match('image.*')) {
                continue;
            }

            var reader = new FileReader();

            // Closure to capture the file information.
            reader.onload = (function(theFile) {
                return function(e) {
                // Render thumbnail.
                var span = document.createElement('span');
                span.innerHTML = ['<input type="checkbox" name="selected[]" value="' , theFile.name, '" style="display: none;" checked /><img width=50% height="auto" class="thumb p-3" src="', e.target.result,
                                    '" title="', theFile.name, '"/>', "<button onclick='deleteImage()'>" + "<i class='fas fa-trash-alt' aria-hidden='true'></i>" + " ลบรูปภาพ</button><br/>"].join('');

                document.getElementById('preview_photo').insertBefore(span, null);
                };
            })(f);

            totalFiles.push(f);

            // Read in the image file as a data URL.
            reader.readAsDataURL(f);
        }

        if(Array.from(totalFiles).length > 0){
            document.getElementById('count_selected_photo').innerHTML = "Image Selected:  " + Array.from(totalFiles).length + " Photo(s)";
        }
        else{
            document.getElementById('count_selected_photo').innerHTML = "No Image Selected";
        }
    }


    function deleteImage() { 
        var index = Array.from(document.getElementById('preview_photo').children).indexOf(event.target.parentNode)
        document.querySelector("#preview_photo").removeChild( document.querySelectorAll('#preview_photo span')[index]);

        totalFiles.splice(index, 1); 

        if(Array.from(totalFiles).length > 0){
            document.getElementById('count_selected_photo').innerHTML = "Image Selected:  " + Array.from(totalFiles).length + " Photo(s)";
        }
        else{
            document.getElementById('count_selected_photo').innerHTML = "No Image Selected";
        }
    }

    document.getElementById('before_photo').addEventListener('change', handleFileSelect, false);

In Controller, I use request->file('before_photo') to get the file selected before deleting and get the name of photo selected by using request->input('selected') and compare these two together. If the name of the file in request->file('before_photo') is the same in request->input('selected'), it will store this photo. If not, which means user delete it before clicking submit, it will not store.

Here is some part of my code in Controller :

// Store Photo If Existed
                    if (request()->hasFile('before_photo')) {
                        // File Chosen at First Time
                        $before_photos = $request->file('before_photo');

                        // File Chosen After Delete Image
                        $selected_photos = $request->input('selected');

                        // Collect Path of Each Photo
                        $paths  = [];

                        // Collect File Name of Each Photo
                        $filenames = [];

                        foreach($selected_photos as $selected_photo){
                            foreach ($before_photos as $before_photo) {
                                // If File Name of Chosen Photo at First Time = File Name of Photo After Deleting
                                if($before_photo->getClientOriginalName() == $selected_photo){

                                    // Transform The File Name to be 'before-photo-TIME-name-originalFileName'
                                    $filename = "before-photo-" . time() . "-name-" . $before_photo->getClientOriginalName();
                                    $paths[] = $before_photo->storeAs('', $filename, 'irepair_photo');
                                    array_push($filenames, $filename);
                                }
                            }
                        }

                        // Transform List of Photo Name into Array
                        $repair_ticket->before_photo = $filenames;
                    }

If you have any suggestion or comments on this, please leave your message below. Thank you for your attention.

Ad

Answer

This might not be what you are looking for but should guide you in the right direction. I see you are using vanilla JavaScript which might make things trickier. But since you are also using Laravel and Laravel ships with Vue support (at least it used to), I'll be showing you my implementation using Vue. If you know JavaScript, it shouldn't be that hard to understand as Vue is very easy.

The trick with multi file uploading is to use another array to store your files as the native FileList object that is returned by the file input is immutable. Look at the code snippets below (I have tried to add comments to explain what I'm doing.)

You can see a working example in this pen.

<!-- @change is the Vue equivalent of onchange -->
<input @change="handleFileSelect" type="file" multiple>
<ul>
  <li v-for="(photo, index) in photos" :key="`thumb-${index}`">
    <img :src="photo.preview" :alt="photo.file.name">
    <button @click="removePhoto(index)" type="button">Remove Photo</button>
  </li>
</ul>

In the HTML section, all we have to do is to listen to changes to the file input and setup a place where we can display thumbnails for the selected images. Vue makes this very easy by using the v-for directive to iterate over an array. The current element in the iteration is available for the li and its child elements and we can use it to populate attributes such as the img's src.

export default {
  data() {
    return {
      photos: []
    }
  },
  methods: {
    handleFileSelect(e) {
      Array.from(e.target.files).forEach(file => {
        // perform check here such as making sure its an image

        const reader = new FileReader()
        
        reader.onload = () => {
          // push the preview result and also the file to photos array
          this.photos.push({
            preview: reader.result,
            file
          })
        }
        
        reader.readAsDataURL(file)
      })
    },
    removePhoto(index) {
      this.photos.splice(index, 1)
    },
    upload() {
      const fd = new FormData()

      this.photos.forEach((photo, index) => {
        // we only want to append the actual file, excluding the preview
        // so we only append the `file` attribute
        fd.append(`photo-${index}`, photo.file)
      })

      // send the data
      // for example, using axios
      const uploadEndpoint = ''
      axios.post(uploadEndpoint, fd, {
        headers: {
          'Content-Type': 'multipart/form-data' // important
        }
      })
    }
  }
}

The JavaScript part is pretty easy. What we are doing here is setting up a photos "local state" to store our photos. We use this variable to iterate over and display thumbnails in our HTML. When a user selects images, we take the files and generate a thumbnail for them using the FileReader object and push it to the photos variable along with the actual file.

{
  preview: 'data:image/....', // the file preview generated by the FileReader
  file: File object // the actual file the user selected
}

When a user wants to unselect an image, we just splice it from the photos array so the photos array will always contain photos the user wants to upload. When we come to uploading, if we are using AJAX to send the request we can just create a FormData object and append each photo to it by iterating over the photos variable. In a Laravel backend, these photos can then be accessed using request()->file(). Before sending your request you have to set the proper headers though, mainly the Content-Type: multipart/form-data.

I hope this gave you some general idea on how to proceed with this issue. If you have any questions feel free to leave them in the comments section and I'll try my best to explain more.


UPDATE: More info on working with the photos on your Laravel Backend

Once you are successfully sending the request from your front end, you can access the photos/files using Laravel's request()->file() helper function. This will return all the files in the request in an array.

dd(request()->file());

Output will look something like:

array:3 [
  "photo-0" => Illuminate\Http\UploadedFile {#221
    -test: false
    -originalName: "The Dark Reef Fugitive.jpg"
    -mimeType: "image/jpeg"
    -error: 0
    #hashName: null
    path: "/tmp"
    filename: "phpjr6Cx5"
    basename: "phpjr6Cx5"
    pathname: "/tmp/phpjr6Cx5"
    extension: ""
    realPath: "/tmp/phpjr6Cx5"
    aTime: 2020-03-03 08:29:19
    mTime: 2020-03-03 08:29:19
    cTime: 2020-03-03 08:29:19
    inode: 12976169
    size: 298634
    perms: 0100600
    owner: 1000
    group: 1000
    type: "file"
    writable: true
    readable: true
    executable: false
    file: true
    dir: false
    link: false
  }
  "photo-1" => Illuminate\Http\UploadedFile {#243
    -test: false
    -originalName: "Nevermore.jpg"
    -mimeType: "image/jpeg"
    -error: 0
    #hashName: null
    path: "/tmp"
    filename: "php3LNGW5"
    basename: "php3LNGW5"
    pathname: "/tmp/php3LNGW5"
    extension: ""
    realPath: "/tmp/php3LNGW5"
    aTime: 2020-03-03 08:29:19
    mTime: 2020-03-03 08:29:19
    cTime: 2020-03-03 08:29:19
    inode: 12976170
    size: 322173
    perms: 0100600
    owner: 1000
    group: 1000
    type: "file"
    writable: true
    readable: true
    executable: false
    file: true
    dir: false
    link: false
  }
  "photo-2" => Illuminate\Http\UploadedFile {#250
    -test: false
    -originalName: "Magnanumus.png"
    -mimeType: "image/png"
    -error: 0
    #hashName: null
    path: "/tmp"
    filename: "phpJZ9El6"
    basename: "phpJZ9El6"
    pathname: "/tmp/phpJZ9El6"
    extension: ""
    realPath: "/tmp/phpJZ9El6"
    aTime: 2020-03-03 08:29:19
    mTime: 2020-03-03 08:29:19
    cTime: 2020-03-03 08:29:19
    inode: 12976172
    size: 1068594
    perms: 0100600
    owner: 1000
    group: 1000
    type: "file"
    writable: true
    readable: true
    executable: false
    file: true
    dir: false
    link: false
  }
]

As you can see each file is an instance of Illuminate\Http\UploadedFile. To store this file, you have a couple of options. One is to use the store or storeAs function on the file or just use the Storage facade's put or putFileAs method. Here is an example:

$filePaths = collect(request()->file())->values()->map(function ($photo) {
  return Storage::put('photos', $photo);
  // OR
  // return Storage::putFileAs('photos', $photo, $photo->getClientOriginalName());
  // OR
  // return $photo->store('photos');
  // OR
  // return $photo->storeAs('photos', $photo->getClientOriginalName());
});

Read the Laravel docuemntation on file storage to understand other options.

$filePaths will be an array of paths where the photos were stored.

Ad
source: stackoverflow.com
Ad