Livewire Added File Upload Support (And It’s Kind of a Game-Changer)

Published on by

Livewire Added File Upload Support (And It’s Kind of a Game-Changer) image

File uploads in web apps are something I’ve always struggled with.

They are easy when you’re just putting an <input type="file" inside a <form> tag and submitting the form the old-fashioned way.

However, things get more difficult in a JavaScript-rich front-end with AJAX form submissions.

Livewire JUST added support for file uploads out of the box. I think it’s going to be a game-changer and can’t wait to tell you all about it!

Let’s walk through the two most common file-upload strategies (and their shortcomings), and then we’ll see how Livewire is the best of both worlds.

The Form Submitty Way

First up, a traditional form submission. Laravel makes handling these kinds of uploads incredibly simple. Let’s review with some sample code:

The Developer Experience:
1. Add a file input to a form that posts to the server
2. Validate and upload the file from a controller on the server

<form action="/profile" method="POST" type="multipart">
<input type="text" name="email">
 
<input type="file" name="photo">
 
<button>Save</button>
</form>
public function store()
{
request()->validate([
'username' => 'required|email',
'photo' => 'image|max:2000',
]);
 
$filename = request('photo')->store('photos');
 
Profile::create([
'username' => request('username'),
'photo' => $filename,
]);
}

This approach is simple and straightforward, but has a few drawbacks:

  • Files must be submitted along with the form on submit (making the experience of submitting a form slow)
  • Files can’t be validated from PHP until the form is submitted
  • Showing the user a preview of the file they selected can only be done from complex JavaScript on the front-end
  • All files must pass through your Laravel server before being stored elsewhere (which can be a hangup for using S3 with server-less environments like Vapor)
  • Because the file doesn’t upload to a separate, dedicated endpoint, file-upload libraries like Filepond are impossible to use.

The AJAXy Way

Consider a modern, JS-based front-end. There are no more traditional form submissions. Everything is handled with AJAX requests to the server using something like axios or fetch().

Unfortunately, uploading a file via AJAX isn’t as easy as you’d hope.

Also, most developers choose to use a technique called “side-loading” to upload the file BEFORE the form is submitted for reasons I mentioned before.

Developer Experience:

  • Create a form with a file input
  • Listen for a new file selected on the input element using JavaScript
  • Upload the file immediately to a dedicated endpoint, storing it temporarily, and returning the filename
  • When the form is submitted, send along the temporary filename with the form
  • Handle the Form submission by loading the temporary file based on the filename, storing it, and deleting the temporary file
  • Because some files are uploaded but not submitted for storing, run a scheduled command to cleanup un-stored temporary files after a day or so

Here’s a Vue component I threw together by ear (no guarantees it actually works) to demonstrate this technique:

<template>
<form @submit.prevent="save">
<input v-model="email" type="text" name="email">
<input @change="updatePhoto" type="file" name="photo">
<button>Save</button>
</form>
</template>
 
<script>
export default {
data() {
return {
email: '',
photo: '',
file: null,
}
},
 
methods: {
updatePhoto(event) {
let formData = new FormData();
 
formData.append('file', event.target.files[0]);
 
axios.post('/file-upload', formData)
.then(response => {
this.photo = response.data.filePath
})
},
 
save() {
axios.post('/profile', {
email: this.email,
photo: this.photo,
}).then(response => {
...
})
},
}
}
</script>

Now let’s look at the server-side code required to make this work:

public function handleUpload()
{
request()->validate(['file' => 'file|max:10000']);
 
return [
'file' => request('file')->storeAs('/tmp'),
];
}
 
public function handleFormSubmit()
{
request()->validate([
'email' => 'required|email',
'photo' => 'required|string',
]);
 
$tmpFile = Storage::get('/tmp/'.request('photo'));
Storage::put('/avatars/'.request('photo'), $tmpFile);
Storage::delete('/tmp/'.request('photo'));
 
Profile::create([
'username' => request('username'),
'photo' => request('photo'),
]);
}

Like I mentioned, this strategy is called “side-loading”. It allows for all the power and flexibility you may want, but at the cost of some extreme amounts of added complexity. Often for something as simple as letting a user upload an avatar.

Note that this code will become MUCH more complex when we start adding things like validation, loading spinners, etc…

We can do better than this. MUCH better.

The Livewire Way

Because Livewire is JavaScript-based, we can’t simply use traditional form submissions.

Instead, Livewire uses “side-loading”, but hides all the complexity for you (with ZERO configuration), providing you with the experience of a traditional form submission, but with a handful of bad-ass upgrades.

Here’s the most basic example:

Developer Experience

  • Add a file input to a form (and apply wire:model="...")
  • Handle the form submission using Livewire and validate and store the file like you normally would in a controller
class Profile extends Component
{
use WithFileUploads;
 
public $email;
public $photo;
 
public function save()
{
$this->validate([
'email' => 'required|email',
'photo' => 'image|max:2000',
]);
 
$filename = $this->photo->store('photos');
 
Profile::create([
'username' => $this->username,
'photo' => $filename,
]);
}
 
public function render()
{
return view('livewire.profile');
}
}
<form wire:submit.prevent="save">
<input wire:model="email" type="text" name="email">
 
<input wire:model="photo" type="file" name="photo">
 
<button>Save</button>
</form>

Pretty simple eh?

Remember, under the hood Livewire is actually “side-loading” the file to a temporary directory.

The benefit here though is that you don’t have to do ANY of the work.

EVERYTHING is taken care of for you.

Let me blast through all the bad-ass things Livewire allows you to do with your file-uploads.

Handling Multiple Uploads

Handling multiple uploads in Livewire is cake.

Here it is:

<input wire:model=“photos” type=“file” multiple>

Livewire will detect the “multiple” attribute and handle everything for you.

On the server-side, the $this->photos property will be an ARRAY of uploaded files.

You can validate and store them like any other array:

...
public $email;
public $photos;
 
public function save()
{
$this->validate([
'email' => 'required|email',
'photos.*' => 'image|max:2000',
]);
 
$filenames = collect($this->photos)->map->store('photos');
 
Profile::create([
'username' => $this->username,
'photos' => $filenames->implode(','),
]);
}
...

Showing Loading Indicators

Want to show the user a loading indicator while their file is uploading?

Again, handle this like you normally would in Livewire with wire:loading:

<input wire:model=“photo” type=“file”>
 
<div wire:loading wire:target="photo">Uploading...</div>

The “Uploading…” message will now be shown for the entire duration of the upload.

Showing Progress Indicators

A simple loading indicator isn’t enough?

Livewire dispatches several useful JavaScript events that can be easily hooked into by something like AlpineJS.

Here’s a simple loading indicator written with Alpine inside a Livewire component:

<div
x-data="{ progress: 0, uploading: false }"
@livewire-upload-start="uploading = true"
@livewire-upload-finish="uploading = false"
@livewire-upload-error="uploading = false"
@livewire-upload-progress="progress = $event.detail.progress"
>
<input wire:model=“photo” type=“file”>
 
<progress max="100" x-bind:value="progress" x-show="uploading"></progress>
</div>

Real-Time File Validation

You can validate a file as soon as it’s selected, just like you would validate ANY value when it’s updated in Livewire.

Hook into the “updated” lifecycle off your file property and you’re off!

class Profile extends Component
{
use WithFileUploads;
 
public $email;
public $photo;
 
// Validate the photo as soon as it's set.
public function updatedSave()
{
$this->validate([
'photo' => 'image|max:2000',
]);
}
 
...
}

Notice, you get the experience of real-time validation on the front-end, but the backend code is the same Laravel error-handling code you’re used to:

...
<input wire:model="photo" type="file" name="photo">
 
@error('photo') {{ $message }} @enderror
...

Direct Upload To S3

Livewire makes it easy to allow your users to upload files that NEVER actually touch your server.

This is EXTREMELY useful for big uploads or server-less environments like Laravel Vapor.

Specify a storage disk in your app that uses the s3 Laravel storage driver, and everything will work magically:

config/livewire.php

...
'temporary_file_upload' => [
'disk' => 's3',
...
],
...

Livewire will now upload the temporary file directly to S3 using a pre-signed upload URL.

The files will be stored in a directory called livewire-tmp/ by default.

To configure this folder to remove files older than 24 hours, Livewire provides a handy artisan command:

php artisan livewire:configure-s3-upload-cleanup

Note: You can still treat the $this->photo property like normal without actually calling on S3. Only when you access it’s contents ($this->photo->get();) or it’s size (for validation: $this->photo->getSize()) will S3 be called on.

This keeps your Livewire requests fast for non-upload related tasks.

Temporary Preview URLs

Sometimes you may want to show your user a preview of the file they just selected before they press “submit”.

Normally, this is difficult because the temporary upload files aren’t publicly accessible.

Livewire makes it extremely easy to generate temporary, secure, public URLs for browser consumption.

Here’s an example of the component’s view that would display an image preview of the newly uploaded file to the user:

...
@if ($photo)
<img src="{{ $photo->temporaryUrl() }}">
@endif
 
<input wire:model="photo" type="file" name="photo">
...

Now as soon as a user selects a file (and after Livewire processes the new upload internally), they will see a preview displayed before they decide to submit the form.

Even More S3 Awesomeness

If you’ve configured Livewire’s temporary upload disk to “s3”, then ->temporaryUrl() will generate a pre-signed temporary file URL that reads directly from S3.

Testing File Uploads

Testing file uploads with Livewire is incredibly simple.

You can use all the functionality you’re used to with standard Laravel controllers.

Here’s an example of a test covering file uploads in a component:

/** @test **/
function can_upload_photo() {
Storage::fake();
 
$file = UploadedFile::fake()->image('avatar.png');
 
Livewire::test(Profile::class)
->set('email', 'calebporzio@livewire.com')
->set('photo', $file)
->call('save');
 
Storage::assertExists('avatar.png');
}

Integrating With Filepond

Filepond is a fantastic JavaScript library that makes drag and drop and other upload-related fanciness extremely easy.

Livewire’s file-upload feature was built to cater to integrations like this.

If you’re interested in what this looks like, hop over to Livewire’s File Upload Screencasts for an in-depth tutorial.

Signing Off

Whew, what a feature. Simple at first glance, but much more powerful if you have deeper needs.

My goal with Livewire is to make web development in Laravel as simple as humanly possible (without being “hand-holdy”). I hope this feature echos that mantra.

If you’re ready to dive in, head over to the documentation, or watch the new screencast series on file uploads to learn more!

Happy uploading!
– Caleb

Caleb Porzio photo

Hi friends! I'm Caleb. I created Livewire and Alpine because I love building tools that make web development feel like magic. I also love to fly fish!

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes.

Visit Larafast: Laravel SaaS Starter Kit
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
All Green logo

All Green

All Green is a SaaS test runner that can execute your whole Laravel test suite in mere seconds so that you don't get blocked – you get feedback almost instantly and you can deploy to production very quickly.

All Green
Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with VILT and TALL stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector

The latest

View all →
Reversible Form Prompts and a New Exceptions Facade in Laravel 11.4 image

Reversible Form Prompts and a New Exceptions Facade in Laravel 11.4

Read article
Integrate Laravel with Stripe Connect Using This Package image

Integrate Laravel with Stripe Connect Using This Package

Read article
The Random package generates cryptographically secure random values image

The Random package generates cryptographically secure random values

Read article
Automatic Blade Formatting on Save in PhpStorm image

Automatic Blade Formatting on Save in PhpStorm

Read article
PhpStorm 2024.1 Is Released With a Integrated Terminal, Local AI Code Completion, and More image

PhpStorm 2024.1 Is Released With a Integrated Terminal, Local AI Code Completion, and More

Read article
Laravel Prompts Adds a Multi-line Textarea Input, Laravel 11.3 Released image

Laravel Prompts Adds a Multi-line Textarea Input, Laravel 11.3 Released

Read article