Creating a Filepond component using Blade, Livewire, & Alpine.js. Then validating & storing with Spatie Media Library

Creating a Filepond component using Blade, Livewire, & Alpine.js. Then validating & storing with Spatie Media Library


In this article we are going to cover a few key topics:

I will use the code I built for the backend of this custom blogging software as a base to more realistically portray using these items together in a real world scenario.

I should note that most of this is covered in the Livewire screencast section 'File Uploads', so go check that out and support Livewire if you can.

Please reference the "Resources" section in the right column for links to all items used in this article.

Buckle Up

Here is an example of what we will be building as seen from my backend of this blog:

Here's an example of what the Filepond component looks like when it has uploaded a file and is awaiting form submission:

Because this article has a lot of information, I'm going to assume you have at least a moderate knowledge of Laravel, Livewire, and Alpine. If not, use it as a learning tool and every time you reach a point of confusion, Google that topic.

This article assumes you have a Laravel project with Livewire & Alpine installed. If not, then do that and come back (An easy solution is to set up a new Laravel project and install Jetstream as it is a good base TALL stack).

To start we are going to make a base Livewire component, we are going to leave out the fancy stuff in the image above such as the modal and surrounding fields and pretend this is just a form that uploads an image for now.

1. Create Livewire Component

Here is our base Livewire Component:

<?php

namespace App\Http\Livewire\Forms;

use Livewire\Component;
use Livewire\WithFileUploads;

class UpdateProfile extends Component
{
    use WithFileUploads;

    public function render()
    {
        return view('livewire.forms.update-profile');
    }
}
<!-- views/livewire/forms/update-profile.blade.php -->

<div></div>

The only difference between this and a base component generated from the make:livewire command, is that I added the WithFileUploads trait as this is needed for Livewire server component to talk to the Livewire Javascript library to sync the file with the backend.

Now that we have our Livewire component made, we can add it to our view using:

<livewire:forms.update-profile />

Note: Here we are using dot syntax to get the component from its nested folder.

That's all we are going to do with that for now since the majority of our work is handled in the Blade component.

Next, we're going to create an anonymous blade component to include in our Livewire view:

Since this is an anonymous component, we don't need the class portion, any Blade files created in views/components will automatically be registered as a Blade component by Laravel:

<!-- views/components/forms/filepond.blade.php -->

<div></div>

Now back to our Livewire view, we can load in our new Blade component:

<!-- views/livewire/forms/update-profile.blade.php -->

<div>
    <form wire:submit.prevent="updateProfile">
        <x-forms.filepond />

        <button type="submit">Save</button>
    </form>
</div>

Note: We don't need the outer div, since the form can act as the sole element, but I like to add it just as a formality in case I needed to add something after the form later.

I added the form element so we have something to submit. So in the Livewire component we can add the updateProfile method to use later:

public function updateProfile(): void
{
    // Will get hit when the submit button is clicked
}

2. Build the Filepond Blade Component

Back to the Filepond component to flesh out the meat and potatoes of this whole article, and that's getting Alpine.js to upload a file to Livewire using Filepond.

There are a few things we have to do:

  1. Ignore the Blade component's main element so that Livewire does not mess with the Javascript within.
  2. Add a file input and make a reference to use in Alpine.
  3. Include the Filepond CDN using @once.
  4. Use Alpine to initialize the Filepond component.
  5. Use the Filepond API to upload the file(s) using the built-in Livewire @this.upload function.
  6. Use the Filepond API to remove the uploaded file using the built-in Livewire @this.removeUpload function.

1. Tell Livewire to ignore the main component

When working with Javascript components inside Livewire, you tend to have issues when Livewire tries to dom-diff a component who's DOM has been modified by a Javascript plugin.

Livewire has a solution for this, and that is wire:ignore.

This tells Livewire to ignore this subset of HTML when doing its DOM manipulations.

<!-- views/livewire/forms/update-profile.blade.php -->

<div wire:ignore></div>

Now our filepond component will be left alone.

2. Adding a reference and a file input

Next we add a regular HTML file input to the component and give it a reference using Alpine (just like in Vue) that we can use to access that element from within the Javascript.

<!-- views/livewire/forms/update-profile.blade.php -->

<div
    wire:ignore
    x-data
>
    <input type="file" x-ref="input" />
</div>

Note: We have to add x-data in order for Alpine to initialize this element as an Alpine component. We then have access to x-ref. We can now target the file element in the x-data and x-init functions using $refs.input.

3. Include the Filepond CDN using @once

When using reusable components that have external Javascript dependencies you have a couple options:

  1. Install using npm and include in your bundle, so it's available where you need it.
  2. Use a CDN on the pages you need to access it.

We are going to go with option 2, but instead of adding it to the page itself, we are going to add it to the component.

However, that brings up another issue, if we have more than one of the same component on a page the CDN calls will be duplicated.

Laravel provides the @once directive for this reason. It only includes that portion of the template once per rendering cycle.

<!-- views/livewire/forms/update-profile.blade.php -->

<div
    wire:ignore
    x-data
>
    <input type="file" x-ref="input" />
</div>

@push('styles')
    @once
        <link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
    @endonce
@endpush

@push('scripts')
    @once
        <script src="https://unpkg.com/filepond/dist/filepond.js"></script>
    @endonce
@endpush

Using Laravel stacks which I have defined in my master layout that this page is inheriting from, I can push these script calls to their respective location in the file where I want them to appear. We now have access to Filepond.

4. Use Alpine to initialize the Filepond component.

Now that we have access to Alpine and Filepond, we can finally use our reference to create an instance of Filepond:

<!-- views/livewire/forms/update-profile.blade.php -->

<div
    wire:ignore
    x-data
    x-init="() => {
        const post = FilePond.create($refs.input);
    }"
>
    <input type="file" x-ref="input" />
</div>

<!-- Stacks Here -->

Here we use a callback to the x-init call to run our code after Alpine has made its initial DOM updates.

We then use our reference to target our HTML file input and turn it into a Filepond component.

You should now see a default Filepond component in place of what was the HTML file input:

5. Use the Filepond API to upload the file(s)

Now we are finally going to make our component work, but to do so we need some things:

  1. We need to add a public property to our Livewire component to house our file(s).
  2. We need to add wire:model to our Blade component's call so we have access to it using Blade's attributes array inside the Blade component itself.
  3. We need to use the Filepond process API to tell Livewire how to sync the file to the backend component.
  4. We need to use Livewire's @this directive inside the Filepond process method (which conveniently have the same API) to tell Livewire to sync the file being uploaded to the property on the Livewire component which matches the wire:model passed through from the main view.
  5. We need to do the same as above, but this time for un-syncing the file from the backend property and deleting the temporary stored file.

Adding a public property to the Livewire component to house the file:

<?php

...

class UpdateProfile extends Component
{
    use WithFileUploads;
    
    public $image;

    ...
}

Using wire:model to pass it into the Blade component:

<!-- views/livewire/forms/update-profile.blade.php -->

...
<x-forms.filepond
    wire:model="image"
/>
...

Using wire:model inside the Blade component and calling the Filepond API to upload the file using Livewire's @this.upload:

<!-- views/livewire/forms/update-profile.blade.php -->

<div
    wire:ignore
    x-data
    x-init="() => {
        const post = FilePond.create($refs.input);
        post.setOptions({
            server: {
                process:(fieldName, file, metadata, load, error, progress, abort, transfer, options) => {
                    @this.upload('{{ $attributes->whereStartsWith('wire:model')->first() }}', file, load, error, progress)
                },
            }
        });
    }"
>
    <input type="file" x-ref="input" />
</div>

<!-- Stacks Here -->

As you can see above we are hooking into the Filepond's server call which is triggered when a file is selected or dropped onto the component.

From there we are using Livewire's @this.upload to tell the parent component to sync this file using the wire:model name passed in to that property ($image) from the Blade components $attributes array.

Note: @this is a Blade directive included in Livewire that compiles to Livewire.find([component-id]).

The upload method is also supplied by Livewire, and the API mimics that of Filepond's API.

If you browse and select a file, or drag and drop a file on (we currently only support one, but will support multiple later in the article) then you should see the Filepond component sync the file with the Livewire backend. It will even show you a progress indicator due to the process parameter of Filepond that we passed to Livewire.

5. Removing the temporary file and un-syncinc the $image property when the Filepond remove button is clicked:

Much like the process call above, both Filepond and Livewire have an identical API to remove the sync'd file from the backend component using Filepond's revert callback and Livewire's removeUpload method:

<!-- views/livewire/forms/update-profile.blade.php -->

<div
    wire:ignore
    x-data
    x-init="() => {
        const post = FilePond.create($refs.input);
        post.setOptions({
            server: {
                process:(fieldName, file, metadata, load, error, progress, abort, transfer, options) => {
                    @this.upload('{{ $attributes->whereStartsWith('wire:model')->first() }}', file, load, error, progress)
                },
                revert: (filename, load) => {
                    @this.removeUpload('{{ $attributes->whereStartsWith('wire:model')->first() }}', filename, load)
                },
            }
        });
    }"
>
    <input type="file" x-ref="input" />
</div>

<!-- Stacks Here -->

This will remove the temporary file that Livewire has stored. This works with both single file uploads and file upload arrays.

That's it! We can now sync and un-sync a temporary file to the Livewire component.

Now lets build the Livewire component functionality to validate and store the file using Spatie Media Library.

3. Save the file(s)

Note: Spatie Media Library is not required at all for this tutorial, but it is by far my favorite way to associate files with models in Laravel.

These are the steps we need to accomplish for this next section:

  1. Install Spatie Media Library.
  2. Set up a model to associate a media collection with.
  3. Validate our file request.
  4. Add validation error text to the page if fired.
  5. Store the file onto the model using the Spatie library.

1. Installing Spatie Media Library

Installing this package is as easy as any other, just require the package with composer:

composer require "spatie/laravel-medialibrary:^9.0.0"

Then follow the instruction on setting up your model.

2. Adding a media collection to our model

For this example, we are uploading a single photo and we will call the collection 'image':

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;

class Post extends Model implements HasMedia
{
    use InteractsWithMedia;
    
    /**
     * Register the media collections
     */
    public function registerMediaCollections(): void
    {
        $this->addMediaCollection('image')
            ->singleFile()
            ->acceptsMimeTypes(['image/jpg', 'image/jpeg', 'image/png', 'image/gif']);
    }
}

Note: You do not have to specify the acceptsMimeTypes array on the collection, as we will also validate that with Laravel, but I like to do it as a backup.

3. Validate the file request

In our uploadProfile method, we need to do some validation on the file to make sure it exists and is an image:

public function updateProfile(): void
{
    $this->validate([
        'image' => ['required', 'image', 'max:10000'],
    ]);
    
    // File is an image that is < 10mb
}

4. Add validation error text to the page

Underneath the file input we are going to check for errors and show the message if need be:

<!-- views/livewire/forms/update-profile.blade.php -->

<div>
    <form wire:submit.prevent="updateProfile">
        <x-forms.filepond
            wire:model="image"
        />
        
        @error('image')
            <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
        @enderror
        
        <button type="submit">Save</button>
    </form>
</div>

Note: We could put the error check inside the Blade component, but sometimes I like the added flexibility of showing errors in different places depending on the context of what i'm building.

5. Store the file onto the model using the Spatie library

public function updateProfile(): void
{
    // Validation Here
    
    // We have no other fields, but we still need a post object to work with
    $post = Post::create([]);
    
    // Add the file to the collection
    $post->addMedia($this->image->getRealPath())->toMediaCollection('image');
    
    // Redirect or send back success message
}

As you can see we created an empty Post object, since we have no other fields to save. Then we get the path of the temporary image and save it to the image media collection.

Note: Since we specified singleFile on the media collection, if we were to save another image to this collection it would replace the current one.

That's it! You now have a fully working Filepond component run by Livewire, Alpine, and Spatie Media Library.

Uploading Multiple Files

Livewire and Filepond make uploading multiple files at a time really simple.

We need to modify the following:

  1. Rename our $image property to $images and make it an array.
  2. Add support for the allowMultiple property on our Blade component and pass the value to Filepond.
  3. Remove the singleFile attribute from our media collection.
  4. Modify the Livewire components updateProfile method to handle multiple images.

1. Add an images array

<?php

...

class UpdateProfile extends Component
{
    use WithFileUploads;
    
    public $images = [];

    ...
}

2. Add support for the allowMultiple property on Filepond

<!-- views/livewire/forms/update-profile.blade.php -->

<div
    wire:ignore
    x-data
    x-init="() => {
        const post = FilePond.create($refs.input);
        post.setOptions({
            allowMultiple: {{ $attributes->has('multiple') ? 'true' : 'false' }},
            server: {
                ...
            }
        });
    }"
>
    <input type="file" x-ref="input" />
</div>

By default, multiple will be off, but we turn it on at the component level on a per use basis:

<!-- views/livewire/forms/update-profile.blade.php -->

<x-forms.filepond
    wire:model="images"
    multiple
/>

3. Remove the singleFile property from our media collection:

public function registerMediaCollections(): void
{
    $this->addMediaCollection('images')
        // ->singleFile()
        ->acceptsMimeTypes(['image/jpg', 'image/jpeg', 'image/png', 'image/gif']);
}

4. Modify updateProfile to handle multiple images

public function updateProfile(): void
{
    // Validation Here
    
    // We have no other fields, but we still need a post object to work with
    $post = Post::create([]);
    
    // Add the files to the collection
    collect($this->images)->each(fn($image) =>
        $post->addMedia($image->getRealPath())->toMediaCollection('images')
    );
    
    // Redirect or send back success message
}

Adding Plugins

Adding plugins is as easy as including the plugin via CDN, and passing the options from the parent to the Blade component:

Here is one example, but you will find multiple in the completed implementation below.

The steps you need to take to add a plugin are:

  1. Get the plugins CDN links and add them to the stacks in the component.
  2. Add support for the selected components options in the Javascript
  3. Pass the options into the component if needed, or if not make sure there are sensible defaults.

First we need to find a plugin to use.

For this example I am going to install the image preview plugin, but the other plugins follow the same flow.

1. Add the plugin CDN

...

@push('styles')
    @once
        <link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
        <link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet">
    @endonce
@endpush

@push('scripts')
    @once
        <script src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"></script>
        <script src="https://unpkg.com/filepond/dist/filepond.js"></script>
        <script>
            FilePond.registerPlugin(FilePondPluginImagePreview);
        </script>
    @endonce
@endpush

2. Add support for the plugin in the Javascript

<div
    wire:ignore
    x-data
    x-init="
        () => {
            const post = FilePond.create($refs.{{ $attributes->get('ref') ?? 'input' }});
            post.setOptions({
                ...
                
                allowImagePreview: {{ $attributes->has('allowFileTypeValidation') ? 'true' : 'false' }},
                imagePreviewMaxHeight: {{ $attributes->has('imagePreviewMaxHeight') ? $attributes->get('imagePreviewMaxHeight') : '256' }},
            });
        }
    "
>
    <input type="file" x-ref="{{ $attributes->get('ref') ?? 'input' }}" />
</div>

...

In the above we have added a flag called allowImagePreview to enable the plugin if it exists on the component. We then can pass in imagePreviewMaxHeight to the component, or if we leave it out it will use the default height of 256.

3. Pass the options into the component:

<x-input.filepond
    wire:model="image"
    allowImagePreview
    imagePreviewMaxHeight="200"
/>

These are the minimum steps needed to add a plugin, different plugins have different amount of options that may require different implementation.

You may also want to do some modifications to ensure that plugin CDN links are not loaded unless the plugin is actually being used, but that is outside the scope of this tutorial.

Completed Implementation

Filepond Component:

<div
    wire:ignore
    x-data
    x-init="
        () => {
            const post = FilePond.create($refs.{{ $attributes->get('ref') ?? 'input' }});
            post.setOptions({
                allowMultiple: {{ $attributes->has('multiple') ? 'true' : 'false' }},
                server: {
                    process:(fieldName, file, metadata, load, error, progress, abort, transfer, options) => {
                        @this.upload('{{ $attributes->whereStartsWith('wire:model')->first() }}', file, load, error, progress)
                    },
                    revert: (filename, load) => {
                        @this.removeUpload('{{ $attributes->whereStartsWith('wire:model')->first() }}', filename, load)
                    },
                },
                allowImagePreview: {{ $attributes->has('allowFileTypeValidation') ? 'true' : 'false' }},
                imagePreviewMaxHeight: {{ $attributes->has('imagePreviewMaxHeight') ? $attributes->get('imagePreviewMaxHeight') : '256' }},
                allowFileTypeValidation: {{ $attributes->has('allowFileTypeValidation') ? 'true' : 'false' }},
                acceptedFileTypes: {!! $attributes->get('acceptedFileTypes') ?? 'null' !!},
                allowFileSizeValidation: {{ $attributes->has('allowFileSizeValidation') ? 'true' : 'false' }},
                maxFileSize: {!! $attributes->has('maxFileSize') ? "'".$attributes->get('maxFileSize')."'" : 'null' !!}
            });
        }
    "
>
    <input type="file" x-ref="{{ $attributes->get('ref') ?? 'input' }}" />
</div>

@push('styles')
    @once
        <link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
        <link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet">
    @endonce
@endpush

@push('scripts')
    @once
        <script src="https://unpkg.com/filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.js"></script>
        <script src="https://unpkg.com/filepond-plugin-file-validate-size/dist/filepond-plugin-file-validate-size.js"></script>
        <script src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"></script>
        <script src="https://unpkg.com/filepond/dist/filepond.js"></script>
        <script>
            FilePond.registerPlugin(FilePondPluginFileValidateType);
            FilePond.registerPlugin(FilePondPluginFileValidateSize);
            FilePond.registerPlugin(FilePondPluginImagePreview);
        </script>
    @endonce
@endpush

Using the component with all options:

<x-input.filepond
    wire:model="images"
    multiple
    allowImagePreview
    imagePreviewMaxHeight="200"
    allowFileTypeValidation
    acceptedFileTypes="['image/png', 'image/jpg']"
    allowFileSizeValidation
    maxFileSize="4mb"
/>

The rest of the implementation such as the Livewire component, validation, storing the files, etc. are up to your preference, but you can use the above code as a base.

We Did It!

I hope you enjoyed this relatively in-depth tutorial on how all of these technologies can work together. If you did, feel free to like this post and share on social media and check back weekly for new original articles.


Also need Image Editing?

Doka.js is what you're looking for. It's a Modern JavaScript Image Editor, Doka supports setting crop aspect ratios, resizing, rotating, cropping, and flipping images. Above all, it integrates beautifully with FilePond.

Learn more about Doka

Anthony Rappa

By Anthony Rappa

Hello! I'm a full stack developer from Long Island, New York. Working mainly with Laravel, Tailwind, Livewire, and Alpine.js (TALL Stack). I share everything I know about these tools and more, as well as any useful resources I find from the community. You can find me on GitHub and LinkedIn.