Courses

Livewire 3 for Beginners with Laravel 12 Starter Kit

File Upload with Spatie Media Library

Summary of this lesson:
- Installing Spatie Media Library for file uploads
- Modifying Task Model for file handling
- Adding file input to create form
- Displaying and updating file in the table

Now, let's add a file upload field to the Tasks CRUD.


Prepare Back-End: Spatie Media Library

Let's install a well-known package spatie/laravel-medialibrary to handle the file data.

composer require "spatie/laravel-medialibrary"

Then, according to its documentation, we need to publish and run migrations:

php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan migrate

Finally, we add the appropriate Traits to the Task Model file.

app/Models/Task.php:

use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
 
// ...
 
class Task extends Model
class Task extends Model implements HasMedia
{
use HasFactory;
use InteractsWithMedia;
 
// ...

Ok, the package installation/configuration is ready. Let's dive into the forms.


File Upload in Create Form: Front-End

This time, let's start with the front end. In create page we will add input with a type of file.

resources/views/livewire/tasks/create.blade.php:

<section class="max-w-5xl">
<form wire:submit="save" class="flex flex-col gap-6">
<flux:input
wire:model="name"
:label="__('Task Name')"
required
badge="required"
/>
 
<flux:input
wire:model="due_date"
type="date"
:label="__('Due Date')"
/>
 
<flux:input
wire:model="media"
type="file"
:label="__('Media')"
/>
 
<div>
<flux:button variant="primary" type="submit">{{ __('Save') }}</flux:button>
</div>
</form>
</section>

Here's the visual result:

Now, what happens after we submit the form?


Submit the Create Form: Back-End

We need to modify the Livewire component. To enable file uploading, first, the WithFileUploads trait must be added. Then, the usual, public property, validation, and uploading file.

app/Livewire/Tasks/Create.php:

use Livewire\WithFileUploads;
 
class Create extends Component
{
use WithFileUploads;
 
#[Validate('required|string|max:255')]
public string $name = '';
 
#[Validate('nullable|date')]
public null|string $due_date;
 
#[Validate('nullable|file|max:10240')]
public $media;
 
public function save(): void
{
$this->validate();
 
Task::create([
$task = Task::create([
'name' => $this->name,
'due_date' => $this->due_date,
]);
 
if ($this->media) {
$task->addMedia($this->media)->toMediaCollection();
}
 
session()->flash('success', 'Task successfully created.');
 
$this->redirectRoute('tasks.index', navigate: true);
}
 
public function render(): View
{
return view('livewire.tasks.create');
}
}

Showing File in the Table

In the Index Livewire component, we need to just eager load the media() relationship:

app/Livewire/Tasks/Index.php:

class Index extends Component
{
// ...
 
public function render(): View
{
return view('livewire.tasks.index', [
'tasks' => Task::latest()->paginate(3),
'tasks' => Task::with('media')->paginate(3),
]);
}
}

But we also need to change one default behavior of the package.

By default, Spatie Media Library returns multiple files per record. To overcome that and auto-load what we actually need, let's create a special method to return the first media file for each Task. This is a bit tricky advanced Eloquent operation:

  • We need to auto-load the file with $appends Eloquent property
  • However, to avoid N+1 query performance issues, we need to check for eager loading

app/Models/Task.php:

class Task extends Model implements HasMedia
{
// ...
 
protected $appends = [
'mediaFile'
];
 
public function getMediaFileAttribute()
{
if ($this->relationLoaded('media')) {
return $this->getFirstMedia();
}
 
return null;
}
}

Now, we can reference the mediaFile on the front end to show file in the table.

resources/views/livewire/tasks/index.blade.php:

// ...
 
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
Task
</th>
<th scope="col" class="px-6 py-3">
File
</th>
<th scope="col" class="px-6 py-3">
Status
</th>
<th scope="col" class="px-6 py-3">
Due date
</th>
<th scope="col" class="px-6 py-3">
Actions
</th>
</tr>
</thead>
<tbody>
@foreach($tasks as $task)
<tr class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700 border-gray-200">
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
{{ $task->name }}
</th>
<td class="px-6 py-4">
@if($task->media_file)
<a href="{{ $task->media_file->original_url }}" target="_blank">
<img src="{{ $task->media_file->original_url }}" alt="{{ $task->name }}" class="w-8 h-8" />
</a>
@endif
</td>
<td class="px-6 py-4">
<span @class([
'text-green-600' => $task->is_completed,
'text-red-700' => ! $task->is_completed,
])>
{{ $task->is_completed ? 'Completed' : 'In progress' }}
</span>
</td>
<td class="px-6 py-4">
{{ $task->due_date?->format('M d, Y') }}
</td>
<td class="px-6 py-4 space-x-2">
<flux:button href="{{ route('tasks.edit', $task) }}" variant="filled">{{ __('Edit') }}</flux:button>
<flux:button wire:confirm="Are you sure?" wire:click="delete({{ $task->id }})" variant="danger" type="button">{{ __('Delete') }}</flux:button>
</td>
</tr>
@endforeach
</tbody>
</table>
 
// ...

I've tried to upload files for a few tasks. Here's the result in the table:


Edit Form: Show the File

Finally, let's add the same input in the Edit Task form with the current file image shown.

First, we need to append some data in the Livewire component. The default Route Model Binding with (Task $task) doesn't load those fields by default, so we need to do it manually.

app/Livewire/Tasks/Edit.php:

use App\Models\Task;
use Livewire\Component;
use Illuminate\View\View;
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;
 
class Edit extends Component
{
use WithFileUploads;
 
#[Validate('required|string|max:255')]
public string $name;
 
#[Validate('nullable|boolean')]
public bool $is_completed;
 
#[Validate('nullable|date')]
public null|string $due_date;
 
#[Validate('nullable|file|max:10240')]
public $media;
 
public Task $task;
 
public function mount(Task $task): void
{
$this->task = $task;
$this->task->load('media');
$this->name = $task->name;
$this->is_completed = $task->is_completed;
$this->due_date = $task->due_date?->format('Y-m-d');
}
 
// ...
}

Then, we need to add input with a type of file to the form.

resources/views/livewire/tasks/edit.blade.php:

<section class="max-w-5xl">
<form wire:submit="save" class="flex flex-col gap-6">
<flux:input
wire:model="name"
:label="__('Task Name')"
required
badge="required"
/>
 
<flux:switch
wire:model="is_completed"
label="Completed?"
align="left"
/>
 
<flux:input
wire:model="due_date"
type="date"
:label="__('Due Date')"
/>
 
<flux:input
wire:model="media"
type="file"
:label="__('Media')"
/>
 
@if($task->media_file)
<a href="{{ $task->media_file->original_url }}" target="_blank">
<img src="{{ $task->media_file->original_url }}" alt="{{ $task->name }}" class="w-32 h-32" />
</a>
@endif
 
<div>
<flux:button variant="primary" type="submit">{{ __('Save') }}</flux:button>
</div>
</form>
</section>

And now, if we visit the Edit page, it shows the current file thumbnail!


Submit the Edit Form

When saving the task we need to do additional step. Delete file if it is already is added.

app/Livewire/Tasks/Edit.php:

class Edit extends Component
{
// ...
 
public function save(): void
{
$this->validate();
 
$this->task->update([
'name' => $this->name,
'is_completed' => $this->is_completed,
'due_date' => $this->due_date,
]);
 
if ($this->media) {
$this->task->getFirstMedia()?->delete();
$this->task->addMedia($this->media)->toMediaCollection();
}
 
session()->flash('success', 'Task successfully updated.');
 
$this->redirectRoute('tasks.index', navigate: true);
}
 
public function render(): View
{
return view('livewire.tasks.edit');
}
}

And that's it. Now, we can replace the file with another file in the edit form!


The repository for this starter kit project section is here on GitHub.

Previous: Date Picker with due_date Field

No comments yet…

avatar
You can use Markdown