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:
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.