whoisthisstud's avatar

AlpineJS/Livewire - duplicate temp image on upload

When adding wire:model="deal_photos" to my file input, each image uploaded is displayed twice. Clicking to select versus drag-n-drop makes no difference. The alpine files object contains exactly the number of objects related to the number of images I select. If I drag and drop images to sort them, each image the dragged image is hovering also highlights its duplicate.

If I remove wire:model from the input then Alpine displays the correct number of temp images. What am I missing?

<!-- blade file -->
<div>

    <div class="w-full bg-white rounded">
        <div id="dndBlock" x-data="dataFileDnD()" class="relative flex flex-col p-4 text-gray-400 rounded">
            <div x-ref="dnd"
                class="relative flex flex-col text-gray-400 border border-gray-200 border-dashed rounded cursor-pointer">
                <input wire:model="deal_photos" type="file" multiple
                    class="absolute inset-0 z-50 w-full h-full p-0 m-0 outline-none opacity-0 cursor-pointer"
                    @change="addFiles($event)"
                    @dragover="$refs.dnd.classList.add('border-[#8a1a5b]');"
                    @dragleave="$refs.dnd.classList.remove('border-[#8a1a5b]');"
                    @drop="$refs.dnd.classList.remove('border-[#8a1a5b]');"
                    title="" accept="image/jpg,image/jpeg,image/png" />

                <div class="flex flex-col items-center justify-center py-10 text-center">
                    <svg class="w-6 h-6 mr-1 text-current-50" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
                        stroke="currentColor">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                            d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
                    </svg>
                    <p class="m-0">Drag your files here or click in this area.</p>
                </div>
            </div>

            <template x-if="files.length > 0">
                <div class="grid grid-cols-2 gap-4 mt-4 md:grid-cols-6" @drop.prevent="drop($event)"
                    @dragover.prevent="$event.dataTransfer.dropEffect = 'move'">
                    <template x-for="(file, index) in files">
                        <div class="relative flex flex-col items-center overflow-hidden text-center bg-gray-100 border rounded cursor-move select-none" style="padding-top: 100%;" @dragstart="dragstart($event)" @dragend="fileDragging = null" :class="{'border-blue-600': fileDragging == index}" draggable="true" :data-index="index">
                            <button class="absolute top-0 right-0 z-50 p-1 bg-white rounded-bl focus:outline-none" type="button" @click="remove(index)">
                                <svg class="w-4 h-4 text-gray-700" xmlns="http://www.w3.org/2000/svg" fill="none"
                                    viewBox="0 0 24 24" stroke="currentColor">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
                                        d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
                                </svg>
                            </button>

                            <template x-if="files[index].type.includes('image/')">
                                <img class="absolute inset-0 z-0 object-cover w-full h-full border-4 border-white preview"
                                    x-bind:src="loadFile(files[index])" />
                            </template>

                            <div class="absolute bottom-0 left-0 right-0 flex flex-col p-2 text-xs bg-white bg-opacity-50">
                                <span class="w-full font-bold text-gray-900 truncate"
                                    x-text="files[index].name">Loading</span>
                                <span class="text-xs text-gray-900" x-text="humanFileSize(files[index].size)">...</span>
                            </div>

                            <div class="absolute inset-0 z-40 transition-colors duration-300" @dragenter="dragenter($event)"
                                @dragleave="fileDropping = null"
                                :class="{'bg-blue-200 bg-opacity-80': fileDropping == index && fileDragging != index}">
                            </div>
                        </div>
                    </template>
                </div>
            </template>

            <!-- Temporary -->
            <span x-text="files.length"></span>

        </div>
    </div>

    <button wire:click="test"
                class="mt-4 px-4 py-2 rounded bg-black text-white">Test</button>

    <script src="https://unpkg.com/create-file-list"></script>
    <script>
    function dataFileDnD() {
        return {
            files: [],
            fileDragging: null,
            fileDropping: null,
            humanFileSize(size) {
                const i = Math.floor(Math.log(size) / Math.log(1024));
                return (
                    (size / Math.pow(1024, i)).toFixed(2) * 1 +
                    " " +
                    ["B", "kB", "MB", "GB", "TB"][i]
                );
            },
            remove(index) {
                let files = [...this.files];
                files.splice(index, 1);

                this.files = createFileList(files);
            },
            drop(e) {
                let removed, add;
                let files = [...this.files];

                removed = files.splice(this.fileDragging, 1);
                files.splice(this.fileDropping, 0, ...removed);

                this.files = createFileList(files);

                this.fileDropping = null;
                this.fileDragging = null;
            },
            dragenter(e) {
                let targetElem = e.target.closest("[draggable]");

                this.fileDropping = targetElem.getAttribute("data-index");
            },
            dragstart(e) {
                this.fileDragging = e.target
                    .closest("[draggable]")
                    .getAttribute("data-index");
                e.dataTransfer.effectAllowed = "move";
            },
            loadFile(file) {
                const preview = document.querySelectorAll(".preview");
                const blobUrl = URL.createObjectURL(file);

                preview.forEach(elem => {
                    elem.onload = () => {
                        URL.revokeObjectURL(elem.src); // free memory
                    };
                });

                return blobUrl;
            },
            addFiles(e) {
                this.files = createFileList([...this.files], [...e.target.files]);

                console.log(this.files);              
            }
        };
    }
    </script>
</div>
<?php

namespace App\Http\Livewire\Deals;

use Livewire\Component;
use Livewire\WithFileUploads;

class DealBuilder extends Component
{
    use WithFileUploads;

    public $deal_photos = [];

    public function test()
    {
        dd($this->deal_photos);
    }


    public function render()
    {
        return view('livewire.deals.deal-builder');
    }
}

0 likes
2 replies
whoisthisstud's avatar

Also, if I attempt to use @entangle('deal_photos') with the Alpine files variable, the same issue exists.

<script>
    function dataFileDnD() {
        return {
            files: @entangle('deal_photos'),  // has same result of duplicate images
            copy: [],
            fileDragging: null,
            ...

Please or to participate in this conversation.