Laravel Livewire makes file uploading very easy, it's almost like magic to say. Here we will see how to create a modular upload component with the help of laravel blade component.
This post assumes you have a basic knowledge of Laravel Livewire and AlpineJS and have already installed a fresh copy of it along with Laravel.
Features it will include are:
- Upload Component - both single and multiple files
- Show progress when uploading
- Show preview of the uploaded file
- Validation
- Remove already uploaded file
Let us create a Livewire component SingleUpload
php artisan livewire:make SingleUpload
After running this command two files will be generated in app/Http/Livewire and resources/views/livewire directory respectively.
- SingleUpload.php : The component class to handle our logic
- single-upload.blade.php : The view file for our component
SingleUpload.php
use Livewire\WithFileUploads; class SingleUpload extends Component { use WithFileUploads; public $photo; public function render() { return view('livewire.single-upload'); } }
How to use in Laravel Blade Views
For single image upload the blade component will look like this:
<x-file-attachment wire:model="photo" :file="$photo" />
For multiple image upload the blade component will look like this:
<x-file-attachment wire:model="photo" :file="$photo" multiple />
For profile image upload the blade component will look like this:
<x-file-attachment wire:model="photo" :file="$photo" mode="profile" profile-class="w-24 h-24 rounded-lg" accept="image/jpg,image/jpeg,image/png" />
The full component is given below:
@props([ 'file' => null, 'accept' => 'image/jpg,image/jpeg,image/png,application/pdf', 'multiple' => false, 'mode' => 'attachment', 'profileClass' => 'w-20 h-20 rounded-full' ]) <div x-data="{ isMultiple: Boolean('{{ $multiple }}') || false, progress: 0, isFocused: false, handleFiles() { if (this.isMultiple === true) { @this.uploadMultiple('{{ $attributes->wire('model')->value }}', this.$refs.input.files, () => { }, () => { }, (event) => { this.progress = event.detail.progress || 0 }) } else { @this.upload('{{ $attributes->wire('model')->value }}', this.$refs.input.files[0], () => { }, () => { }, (event) => { this.progress = event.detail.progress || 0 }); } } }" x-cloak> @if(! $file || $mode === 'profile') @php $randomId = Str::random(6); @endphp <label for="file-{{ $randomId }}" class="relative block leading-tight bg-white hover:bg-gray-100 cursor-pointer inline-flex items-center transition duration-500 ease-in-out group overflow-hidden {{ $mode === 'profile' ? 'border group '. $profileClass : 'border-2 w-full pl-3 pr-4 py-2 rounded-lg border-dashed' }} " wire:loading.class="pointer-events-none" :class="{ 'border-indigo-300': isFocused === true }" > {{-- hack to get the progress of upload file --}} <input type="hidden" name="{{ $attributes->wire('model')->value }}" {{ $attributes->wire('model') }} /> <input type="file" id="file-{{ $randomId }}" class="absolute inset-0 cursor-pointer opacity-0 text-transparent sr-only" accept="{{ $accept }}" :multiple="isMultiple" x-ref="input" x-on:change.prevent="handleFiles" x-on:focus="isFocused = true" x-on:blur="isFocused = false" /> {{-- Upload Progress --}} <div wire:loading.flex wire:target="{{ $attributes->wire('model')->value }}" wire:loading.class="w-full"> @if ($mode === 'profile' && $file) <div class="select-none text-sm text-indigo-500 flex flex-1 items-center justify-center text-center p-4 flex-1"> <svg class="animate-spin h-6 w-6 text-indigo-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> </svg> </div> @endif @if ($mode === 'attachment') <div class="text-center flex-1 p-4"> <div class="mb-2">Uploading...</div> <div> <div class="h-3 relative max-w-lg mx-auto rounded-full overflow-hidden"> <div class="w-full h-full bg-gray-200 absolute"></div> <div class="h-full bg-green-500 absolute" x-bind:style="'width:' + progress + '%'"></div> </div> </div> </div> @endif </div> {{-- Preview for mode 'profile' --}} @if ($mode === 'profile' && $file) <div class="absolute inset-0"> <div class="relative w-full overflow-hidden shadow-inner" style="padding-bottom: 100%"> @if(collect(['jpg', 'png', 'jpeg', 'webp'])->contains($file->getClientOriginalExtension())) <img src="{{ $file->temporaryUrl() }}" class="inset-0 w-full h-full absolute object-cover"> <div class="h-10 w-10 my-auto flex items-center justify-center inset-0 mx-auto rounded-full group-hover:bg-gray-400 group-hover:bg-opacity-25 text-white absolute z-10"> <svg class="h-6 w-6 text-gray-100" 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="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /> </svg> </div> @else <span>Type invalid.</span> @endif </div> </div> @endif {{-- Placeholder text for mode 'attachment' --}} <div class="flex items-center justify-center flex-1 px-4 py-2" wire:loading.class="hidden" wire:target="{{ $attributes->wire('model')->value }}"> @if($slot->isEmpty()) @if($multiple) <svg class="-mr-5 -mt-2 transform -rotate-6 h-8 w-8 text-gray-300 group-hover:text-indigo-300 transition duration-500 ease-in-out" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><g><circle cx="99.99951" cy="92" r="12"></circle><path d="M208.00049,31.99963h-160a16.01833,16.01833,0,0,0-16,16V175.97369l-.001.0307.001,31.99524a16.01833,16.01833,0,0,0,16,16h160a16.01833,16.01833,0,0,0,16-16v-160A16.01834,16.01834,0,0,0,208.00049,31.99963Zm-28.68653,80a16.019,16.019,0,0,0-22.62792,0l-44.68555,44.68653L91.314,135.99963a16.02161,16.02161,0,0,0-22.62792,0L48.00049,156.68457V47.99963h160l.00586,92.6922Z"></path></g></svg> <svg class="transition duration-500 ease-in-out relative h-8 w-8 transform rotate-3 text-gray-400 group-hover:text-indigo-500" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><path d="M168.001,100.00017v.00341a12.00175,12.00175,0,1,1,0-.00341ZM232,56V200a16.01835,16.01835,0,0,1-16,16H40a16.01835,16.01835,0,0,1-16-16V56A16.01835,16.01835,0,0,1,40,40H216A16.01835,16.01835,0,0,1,232,56Zm-15.9917,108.6936L216,56H40v92.68575L76.68652,112.0002a16.01892,16.01892,0,0,1,22.62793,0L144.001,156.68685l20.68554-20.68658a16.01891,16.01891,0,0,1,22.62793,0Z"></path></svg> @else @if ($mode === 'profile') <svg class="h-8 w-8 text-gray-300 group-hover:text-indigo-500" 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="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /> </svg> @else <svg class="h-8 w-8 text-gray-300 group-hover:text-indigo-500" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"></rect><path d="M168.001,100.00017v.00341a12.00175,12.00175,0,1,1,0-.00341ZM232,56V200a16.01835,16.01835,0,0,1-16,16H40a16.01835,16.01835,0,0,1-16-16V56A16.01835,16.01835,0,0,1,40,40H216A16.01835,16.01835,0,0,1,232,56Zm-15.9917,108.6936L216,56H40v92.68575L76.68652,112.0002a16.01892,16.01892,0,0,1,22.62793,0L144.001,156.68685l20.68554-20.68658a16.01891,16.01891,0,0,1,22.62793,0Z"></path></svg> @endif @endif @if ($mode === 'attachment') <span class="ml-2 text-gray-600">{{ is_array($file) ? 'Browse files' : 'Browse file' }} | <span class="text-sm">PNG or JPEG</span></span> @endif @else {{ $slot }} @endif </div> </label> @endif @if ($mode === 'attachment') {{-- Loading indicator for file remove --}} <div wire:loading.delay wire:loading.flex wire:target="removeUpload" wire:loading.class="w-full"> <div class="text-sm text-red-500 bg-red-100 flex-1 p-1 text-center rounded"> Removing file... </div> </div> {{-- Preview for mode 'attachment' --}} <div> @if(is_array($file) && count($file) > 0) @foreach($file as $key => $f) <div class="py-3 flex {{ !$loop->last ? 'border-b border-gray-200' : '' }}"> <div class="w-16 mr-4 flex-shrink-0 shadow-xs rounded-lg" > @if(collect(['jpg', 'png', 'jpeg', 'webp'])->contains($f->getClientOriginalExtension())) <div class="relative pb-16 overflow-hidden rounded-lg border border-gray-100"> <img src="{{ $f->temporaryUrl() }}" class="w-full h-full absolute object-cover rounded-lg"> </div> @else <div class="w-16 h-16 bg-gray-100 text-blue-500 flex items-center justify-center rounded-lg border border-gray-100"> <svg class="h-12 w-12" 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="1" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> </svg> </div> @endif </div> <div> @if ($multiple) {{-- prints attachment.* --}} @error($attributes->wire('model')->value . '.'. $key) <p class="text-sm text-red-600" class="mb-2">{{ $message }}</p> @enderror @endif <div class="text-sm font-medium truncate w-40 md:w-auto">{{ $f->getClientOriginalName() }}</div> <div class="flex items-center space-x-1"> <div class="text-xs text-gray-500">{{ Str::bytesToHuman($f->getSize()) }}</div> <div class="text-gray-400 text-xs">•</div> <div class="text-xs text-gray-500 uppercase">{{ $f->getClientOriginalExtension() }}</div> </div> <button wire:key="remove-attachment-{{ $f->getFilename() }}" wire:loading.attr="disabled" type="button" x-on:click.prevent="$wire.removeUpload('{{ $attributes->wire('model')->value }}', '{{ $f->getFilename() }}')" class="text-xs text-red-500 appearance-none hover:underline"> Remove </button> </div> </div> @endforeach @else @if($file) <div class="mt-3 flex"> <div class="w-16 mr-4 flex-shrink-0 shadow-xs rounded-lg"> @if(collect(['jpg', 'png', 'jpeg', 'webp'])->contains($file->getClientOriginalExtension())) <div class="relative pb-16 w-full overflow-hidden rounded-lg border border-gray-100"> <img src="{{ $file->temporaryUrl() }}" class="w-full h-full absolute object-cover rounded-lg"> </div> @else <div class="w-16 h-16 bg-gray-100 text-blue-500 flex items-center justify-center rounded-lg border border-gray-100"> <svg class="h-12 w-12" 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="1" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> </svg> </div> @endif </div> <div> @error($attributes->wire('model')->value) <p class="text-sm text-red-600" class="mb-2">{{ $message }}</p> @enderror <div class="text-sm font-medium truncate w-40 md:w-auto">{{ $file->getClientOriginalName() }}</div> <div class="flex items-center space-x-1"> <div class="text-xs text-gray-500">{{ Str::bytesToHuman($file->getSize()) }}</div> <div class="text-gray-400 text-xs">•</div> <div class="text-xs text-gray-500 uppercase">{{ $file->getClientOriginalExtension() }}</div> </div> <button wire:loading.attr="disabled" type="button" x-on:click.prevent="$wire.removeUpload('{{ $attributes->wire('model')->value }}', '{{ $file->getFilename() }}')" class="text-xs text-red-500 appearance-none hover:underline"> Remove </button> </div> </div> @endif @endif </div> @endif </div>
Feel free to check out the source code on Github:
https://github.com/mithicher/file-attachment
Resources
For more information related to Livewire file uploads, visit:
Top comments (3)
Hello @mithicher
Thanks for this great example.
I added the x-on:drop to the input to enable drag-drop, but it didn't work. Can you help please?
x-on:drop="handleFiles"
I will check on this later...meanwhile you can try this tailwindcomponents.com/component/d...
never mind, I just noticed the "sr-only" which is making the input invisible :)
thanks though