Passing uploaded files between Laravel Livewire components

I recently discovered a need to be able pass temporary (without being saved to your filesystem) uploaded files using Laravel Livewire for an API that I am building for my 9-5 job. Essentially (thinking with my VueJS cap) I wanted to build a uniform file-upload input that I could use multiple times within my application.

Again, using my VueJS-centered brain, I just assumed that I would be able to simply emit up the TemporaryUploadedFile class that Livewire generates with a file-upload. Naturally I was incorrect. My problem: when you attempt to pass this class to a parent/child component all you get is:

[
    'disk' => 'public'
];
The dumped response from a emitted temporary file

Obviously you cannot do anything with this. So after some discovery, I was able to come up with a scalable solution.

The result

For the sake of being thorough, I am going to show you all of my code, so you can replicate the entire component for yourself. It is worth noting that I also used Alpine.JS and Tailwind for this component.

In the next few steps, this is what we are going to build:

Yours MIGHT look a little different as I have modified my tailwind.config.js file a bit.

How I did it

Assuming you have at least skimmed the documentation for Laravel Livewire on file uploads, you can see that once a file is uploaded, Livewire stores the file in a temporary directory within your filesystem; whether it is remote or locally depends on your infrastructure. The response of this action is an object with a bunch of other meta data named Livewire\TemporaryUploadedFile. From there you can extract any information that you need and save/move the file around where you need it. Again, you can refer to the documentation for further details.

Like I said above, if you simply try to use the emit() action to pass the result to other components, all that is passed is the disk path. I even tried JSON encoding the object, but that didn't work either. After dissecting the Livewire\TemporaryUploadedFile class, I discovered there is an exposed method getRealPath() that returns the temporary location of the file. Furthermore, I found that the path of the file is the parameter required to construct a new instance of the TemporaryUploadedFile class. Passing strings with Laravel Livewire is easy, so that was my ticket: emitting the full real path of the temporary file, and then simply re-constructing the TemporaryUploadedFile class in the parent/listening component.

Once finished, then you can either move/save the emitted file like so:

class FileUpload extends Component
{
    use WithFileUploads;

    public $file;
    
    ...
    
    public function fileComplete()
    {
        $this-file->storePubliclyAs(
            'documents',
            'sample',
            config('filesystems.default'),
        );
    }
}

Full code

Child Component Template

<div
    x-data="{ isUploading: false, progress: 0, name: null }"
    x-on:livewire-upload-start="isUploading = true"
    x-on:livewire-upload-finish="isUploading = false; $wire.fileComplete()"
    x-on:livewire-upload-error="isUploading = false"
    x-on:livewire-upload-progress="progress = $event.detail.progress"
>

    <div class="overflow-hidden relative w-64 mt-4 mb-4">
        <label class="font-sans py-2 px-4 border border-transparent text-sm font-semibold tracking-wider rounded-md transition duration-150 ease-in-out uppercase border-gray-300 text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue inline-flex cursor-pointer" x-show="!name">
            <svg fill="#FFF" height="18" viewBox="0 0 24 24" width="18" xmlns="http://www.w3.org/2000/svg">
                <path d="M0 0h24v24H0z" fill="none"/>
                <path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/>
            </svg>
            <span class="ml-2">Select File</span>
            <input class="hidden" type="file" wire:model="file" x-on:change="name = $event.target.files[0].name">
        </label>
        <div class="text-gray-500" x-show="name">
            <span x-show="isUploading">Uploading... Please wait.</span>
            <div class="flex items-center" x-show="!isUploading">
                <div class="flex-1 truncate mr-2">
                    <span x-text="name"></span>
                </div>
                <button type="button" class="inline-flex items-center justify-center h-7 w-7 rounded-full bg-red-600 hover:bg-red-800 text-white shadow-lg hover:shadow-xl transition duration-150 ease-in-out focus:bg-red-700 outline-none focus:outline-none" x-on:click="progress = 0; name = null; $wire.file = null; $wire.fileReset()">
                    <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                    <path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
                </svg>
                </button>
            </div>
        </div>
    </div>

    <!-- Progress Bar -->
    <div class="relative pt-1" x-show="isUploading">
        <div class="overflow-hidden h-2 mb-4 text-xs flex rounded bg-green-200">
            <div :style="`width: ${progress}%`" class="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-green-500"></div>
        </div>
    </div>
</div>

Child Component

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Livewire\WithFileUploads;

class FileUpload extends Component
{
    use WithFileUploads;

    public $file;

    public function render()
    {
        return view('livewire.file-upload');
    }

    public function updatedFile()
    {
        $this->validate([
            'file' => 'file|max:8192', // 8MB Max
        ]);

        $this->emitUp('fileUploaded', $this->file);
    }

    public function fileComplete()
    {
        $this->emitUp('fileUpload', $this->file->getRealPath());
    }

    public function fileReset()
    {
        $this->emitUp('fileReset');
    }
}

Parent Component

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Livewire\TemporaryUploadedFile;

class ParentComponent extends Component
{
    protected $listeners = [
        'fileUpload',
        'fileReset',
    ];
    
    public $file;
    
    public function fileUpload($payload)
    {
        $this->file = new TemporaryUploadedFile($payload, config('filesystems.default'));
        
        /* When you are ready to store the file */
        /*
        $this->file->storePubliclyAs(
            'documents',
            'sample',
            config('filesystems.default'),
        );
        */
    }

    public function fileReset()
    {
        $this->file = null;
    }
}

Hopefully you can find this useful. I was banging my head against the wall for a couple hours with this, and naturally the solution was easier than anticipated.

via GIPHY