Skip to main content

Command Palette

Search for a command to run...

Passing uploaded files between Laravel Livewire components

Step-by-step guide to passing uploaded files between Laravel Livewire components using TemporaryUploadedFile, file paths, and reusable upload logic.

Updated
•5 min read•View as Markdown
Passing uploaded files between Laravel Livewire components
D
I am a full-stack developer with a passion for creating intuitive and user-friendly experiences. With a strong background in frontend development and a deep understanding of backend technologies, I excel in building robust and scalable applications. My expertise spans across various frameworks and languages, allowing me to adapt to different project requirements. I am dedicated to staying updated with the latest industry trends and continuously improving my skills to deliver high-quality solutions.

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:

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:

Step 1 Step 2 Step 3

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.

More from this blog

T

The Technical Leader

10 posts

Bringing 15+ years of software and development expertise married with decades of leadership experience, I provide a unique and honest take on leading software teams and cultivating your own growth. As I am still a developer at heart, I will occasionally talk about new trends and experiences that have forced me to take a side, whether it's right or wrong.