• Reading time ~ 9 min
  • 30.06.2022

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', '[email protected]')
    ->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

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

ABOUT

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

About author CrazyBoy49z
WORK EXPERIENCE
Contact
Ukraine, Lutsk
+380979856297