Learn to implement the preview of the uploaded media by using jquery and spatie laravel media library without getting pro. Quick implement with the easy steps
Even today days, it is very difficult or irritate task to upload an media file and preview it inside the website for most of the developers when there is no Livewire or front-end JS framework is used. And when the form is submitted and error occurred then we loss the uploaded media and some time if user uploaded multiple files then developer faced the maximum upload file size error from the server side. Sometimes most senior developer avoid the use of the any latest technology or due to different reasons that they faced in their career and also be careful about scale of the software senior developer avoid extra technologies so it can be easy to manage in the future. So there are many possibilities exists to use the jQuery and simple Laravel framework features. We will cover the how to preview the uploaded media inside the browser with the use of the Laravel, Spatie-media Library and jQuery. So we can easily manage it and handle it.
We need to install the spatie-media library as we will manage the uploaded file using this powerful and most useful package in Lararavel.
composer require "spatie/laravel-medialibrary"
We need to create a new file system storage in Laravel to manage the temporary media files as user upload it and we will preview and then later on may be user change the media or cancel the submission in that case we can identify the junk of the storage and also it helps in controller to move the file to our appropriate Model.
Open file named with "filesystems.php" inside config folder and append below 'tmp_media' array inside the 'disks'.
// config/filesystem.php
<?php
return[
'disks' => [
'tmp_media' => [
'driver' => 'local',
'root' => storage_path('tmp_media'),
'url' => env('APP_URL').'/storage/tmp-storage',
'visibility' => 'public',
'throw' => false,
],
]
];
And in the same file "filesystems" you will find the "links" named key. Append the below line inside the "links",
public_path('storage/tmp-storage') => storage_path('tmp_media'),
For this first we will create a new controller with any name. I have created a controller "UploadMediaController" and inside this controller we will handle the file upload that we will receive from the jQuery ajax call.
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
class UploadMediaController extends Controller
{
//
public function handleTempMedia ( Request $request ) {
if( $request->has('media') ){
$files = $request->file('media');
$uploadedFiles = [];
foreach( $files as $file ){
$fileExtension = $file->getClientOriginalExtension();
$name = $file->getClientOriginalName();
$name = str_replace( '.'. $fileExtension, '', $name);
$name .= '_' . time() . Str::random(5) . '.' . $fileExtension;
$storageType = $request->input('type', 'tmp_media');
if( $file->storeAs( '.',$name, $storageType ) ){
$uploadedFiles[] = [
'id' => null,
'title' => $name,
'file_type' => $file->getMimeType(),
'url' => Storage::disk($storageType)->url($name )
];
}
}
$data = [
'files' => $uploadedFiles
];
return response()->json( $data, 200);
}else{
return response()->json( ['message' => 'Invalid Request'], 500);
}
}
}
Now, we have to link this method with the route and need to create a route for this. Here I have added a "Auth" middleware for the safe side and any bad outsider not take the advantage of it.
// Routes/web.php
use App\Http\Controllers\UploadMediaController;
Route::post('tmp-media/upload', [UploadMediaController::class, 'handleTempMedia'])->middleware('auth');
In this section, we will add the code that we can reuse or call the function of the jQuery as we call other jQuery function with the target element. Either you can use inside the global JS file of your project or you can create a new JS file for this.
For this, inside your app layout file you must have "csrf-token" meta tag that we will use in AJAX request. As our request will be POST we require a CSRF-TOKEN or you can except the CSRF request validation from the middleware
$(document).ready( function() {
const uploadFileSizeLimit = 3; // MB
$.fn.handleUploadAndPreviewFile = function ( options ){
const $this = $(this[0]);
const previewContainerClass = $this.data("img-preview-container");
const imgContainerClass = $this.data("img-container-class");
const imgClass = $this.data("img-class");
const files = this[0].files;
const isMultiple = $this.attr("multiple");
const replaceOnUpload = $this.data("replace-on-upload");
let inputName = $this.data("input-name");
if( !inputName ){
inputName = $this.attr("name");
}
if( files.length ){
let isLargeFile = false;
Array.from(files).forEach(file => {
const fileSize = (file.size / 1048576);
if( fileSize > uploadFileSizeLimit ){
isLargeFile = true
}
});
if(!isLargeFile){
var form = new FormData;
if( isMultiple ){
$.each( files, function( k, file){
form.append('media[]', file);
})
}else{
form.append('media[]', files[0]);
}
$.ajax({
url : serverUrl + "tmp-media/upload",
type : "post",
data : form,
headers : {
"X-CSRF-TOKEN" : $('meta[name="csrf-token"]').attr('content')
},
contentType: false,
processData: false,
encType: 'multipart/form-data',
success:function ( res ) {
$this.val("");
if( replaceOnUpload ){
$(previewContainerClass).html('');
}
if( "files" in res && res.files && res.files.length ){
if( options && options.custom_preview_handler ){
options.custom_preview_handler( res, $this);
}else{
let counter = $(".img-preview-container-parent").length;
$.each( res.files, function ( key, data){
let formInputFields = `
<input type="hidden" name="${inputName}[${counter}][id]" value="${data.title}" />
<input type="hidden" name="${inputName}[${counter}][title]" value="${data.title}" />
<input type="hidden" name="${inputName}[${counter}][url]" value="${data.url}" />
`;
if( !isMultiple ){
formInputFields = `
<input type="hidden" name="${inputName}[id]" value="${data.title}" />
<input type="hidden" name="${inputName}[title]" value="${data.title}" />
<input type="hidden" name="${inputName}[url]" value="${data.url}" />
`;
}
const imgHtml = `<div class="${imgContainerClass} img-preview-container-parent">
<div class="card">
${formInputFields}
<div class="card-header">
<h6>
${data.title}
</h6>
</div>
<div class="card-body">
<img class="${imgClass}" src="${data.url}">
</div>
<div class="card-footer">
<button class="btn btn-sm btn-danger remove-self-container" data-closest-el=".img-preview-container-parent" type="button">
<i class="ti ti-trash"></i>
</button>
</div>
</div>
</div>`;
$(previewContainerClass).append( imgHtml );
counter++;
});
}
}
},
error: function () {
alert('Oops! Something went wrong.');
},
complete: function (){
}
})
}else{
alert('Error! File size is more than 3 MB not allowed');
}
}
return this;
}
$.fn.uploadMediaPreview = function ( options ) {
$( this ).change( function(){
$(this).handleUploadAndPreviewFile(options);
})
}
// To handle the delete media action
$(document).on("click",".remove-self-container", function( e ){
e.preventDefault();
let $this = $(this);
var parentEl = $(this).data("closest-el");
$this.closest(parentEl).addClass("d-none").html('');
})
});
In above code, first thing we have validated the file size if it is more than 3 MB then we throws the error. Or you can change it by changing the value of "uploadFileSizeLimit" variable.
Here, I have defined some values and fetched the values from the "data-*" attribute of the "input" file type. And here is the list,
Or you can pass the "custom_preview_handler" value when you use it and can handle the uploaded files manually from the particular form js. We will cover it later in next steps.
It's time to use the above process and implement it inside the form where user will select the media and we will preview it using the jQuery. Now, we are in blade file and we are going to define it.
Here I create a basic form where I added the file input and then managed the media if we redirect the same page from the server. And in the below, I used our jQuery function to handle the image upload. Here I have added a file input with multiple so it will append the multiple media and we will gather multiple images.
// form.blade.php
<x-app-layout>
<div class="container">
<div class="card">
<div class="card-header">
<h3 class="card-title">Form Request with Media</h3>
</div>
<div class="card-body">
<form action="{{route('post_form_request')}}" method="post">
@csrf
<div class="form-group">
<label for="">Name</label>
<input type="text" name="name" placeholder="Enter name" value="{{old('name')}}" />
</div>
<div class="form-group">
<label for="">Upload Profile</label>
<input type="file"
class="form-control"
id="images"
name="images[]"
multiple
data-img-preview-container="#image-preview"
data-img-container-class="col-12 col-md-3"
data-img-class="img img-thumbnail w-100"
data-input-name="uploaded_media"
placeholder="Selct and Upload"
>
</div>
<div class="form-group" >
<div class="row" id="image-preview">
@foreach (old('uploaded_media', [] ) as $key => $item)
<div class="col-12 col-md-3 img-preview-container-parent">
<div class="card">
<input type="hidden" name="uploaded_media[{{ $key }}][id]" value="{{ data_get( $item, 'id') }}" />
<input type="hidden" name="uploaded_media[{{ $key }}][title]" value="{{ data_get( $item, 'title') }}" />
<input type="hidden" name="uploaded_media[{{ $key }}][url]" value="{{ data_get( $item, 'url') }}" />
<div class="card-header">
<h6>
{{data_get( $item, 'title', data_get($item, 'id'))}}
</h6>
</div>
<div class="card-body">
<img class="img img-thumbnail w-100" src="{{ data_get( $item, 'url') }}">
</div>
<div class="card-footer">
<button class="btn btn-sm btn-danger remove-self-container" data-closest-el=".img-preview-container-parent" type="button">
<i class="ti ti-trash"></i>
</button>
</div>
</div>
</div>
@endforeach
</div>
</div>
</form>
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready( function(){
$("#images").uploadMediaPreview();
})
</script>
</x-app-layout>
We can also use the jQuery function in other way that I previously mentioned that in case we have different layout then we can also manage it by passing the argument inside the function. Lets see,
$( document ).ready( function(){
$("#images").uploadMediaPreview({
custom_preview_handler: function( res, $this ){
let counter = $(".my-custom-container-class").length;
$.each( res.files, function ( key, data){
let formInputFields = `
<input type="hidden" name="uploaded_media[${counter}][id]" value="${data.title}" />
<input type="hidden" name="uploaded_media[${counter}][title]" value="${data.title}" />
<input type="hidden" name="uploaded_media[${counter}][url]" value="${data.url}" />
`;
// YOU CAN CHANGE THE LAYOUT HERE IN THIS VARIABLE
const imgHtml = `<div class="image-container my-custom-container-class">
<div class="card">
${formInputFields}
<div class="card-header">
</div>
<div class="card-body text-center">
<a href="${data.url}" target="_blank">
<i class="ti ti-file-dots display-1"></i>
</a>
</div>
<div class="card-footer">
<button class="btn btn-sm btn-danger remove-self-container" data-closest-el=".my-custom-container-class" type="button">
<i class="ti ti-trash"></i>
</button>
</div>
</div>
</div>`;
// HERE YOU CAN CHOOSE THE ELEMENT WHERE YOU WANT TO APPEND IT
$("#profile-image-preview").append( imgHtml );
counter++;
});
}
});
});
After setting up all the basic things and form its time to define the route and form handle method in controller. Here we will use the UserController and handle the uploaded preview media.
But before that let me ready User model to support our spatie media package method and can easily handle the upload media with our User model class.
// App/Models/User.php
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Image\Enums\Fit;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class User extends Authenticatable implements MustVerifyEmail, HasMedia
{
use HasApiTokens, HasFactory, Notifiable, InteractsWithMedia;
public function registerMediaCollections(Media $media = null): void
{
$this->addMediaCollection('profile')
->singleFile()
->registerMediaConversions(function (Media $media) {
$this
->addMediaConversion('thumbnail')
->keepOriginalImageFormat()
->fit(Fit::Contain, 75, 75)
->nonQueued();
$this
->addMediaConversion('logo')
->keepOriginalImageFormat()
->fit(Fit::Contain, 150, 150)
->nonQueued();
});
}
}
Now, we will define our code to hande the request and upload the media to our User model class.
// UserController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Str;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use App\Models\User;
class UserController extends Controller
{
//
public function create ( Request $request ) {
$request->validate(['name' => 'required']);
$user = new User;
$user->name = $request->input('name');
$user->save();
// IF uploaded media is array
$profileImages = $request->input( 'uploaded_media', [] );
if( is_array( $profileImages ) && !empty( array_filter($profileImages) ) ){
foreach ($profileImages as $item) {
$tmpFileNm = data_get( $item, 'id');
if( !empty( $tmpFileNm ) ){
if( Storage::disk('tmp_media')->has( $tmpFileNm ) ){
$user->addMediaFromDisk( $tmpFileNm, 'tmp_media')->toMediaCollection( 'profile' );
}
}
}
}
// If it is Single then we can use it....
$profileImages = $request->input( 'uploaded_media' );
$profileImageId = data_get( $profileImages, 'id');
if( !empty( $profileImageId ) && Storage::disk('tmp_media')->has( $profileImageId ) ){
$user->addMediaFromDisk( $profileImageId, 'tmp_media')->toMediaCollection( 'profile');
}
return redirect()->back()->with('success', 'Success!');
}
public function edit ( Request $request, User $user ) {
$request->validate(['name' => 'required']);
$user->name = $request->input('name');
$user->save();
// When it is an array
$profileImages = $request->input( 'uploaded_media', [] );
if( is_array( $profileImages ) && !empty( array_filter($profileImages) ) ){
$ids = array_filter( Arr::pluck( $profileImages, 'id') );
if( !empty( $ids ) ){
$agent->media()
->whereNotIn('id', $ids )
->where('collection_name', 'profile' )
->delete();
}
foreach ($profileImages as $item) {
$tmpFileNm = data_get( $item, 'id');
if( !empty( $tmpFileNm ) ){
if( Storage::disk('tmp_media')->has( $tmpFileNm ) ){
$user->addMediaFromDisk( $tmpFileNm, 'tmp_media')->toMediaCollection( 'profile' );
}
}
}
}
// When it is a single file
$profileImages = $request->input( 'uploaded_media' );
$profileImageId = data_get( $profileImages, 'id');
if( !empty( $profileImageId ) && Storage::disk('tmp_media')->has( $profileImageId ) ){
$media = $user->addMediaFromDisk( $profileImageId, 'tmp_media')->toMediaCollection( 'profile');
}else{
$user->media()
->where('id', '!=', $profileImageId )
->where('collection_name', 'profile' )
->delete();
}
return redirect()->back()->with('success', 'Success!');
}
}
Here, in the above controller, we have created two method: 1) Create and 2) Edit. And here we are going to use the our new storage "temp_media" that we have defined inside our config folder. Based on that we will attach the image to the user. I have implemented the both code to handle the multiple files or single. So, be careful and note down it while you are implementing this in your scenarios. Again for safety.
Now we have to add the routes in our web.php file.
// routes/web.php
<?php
use App\Http\Controllers\UserController;
Route::post('user/create', [UploadMediaController::class, 'create'])->middleware('auth')->name('post_form_request');
Route::post('user/{user:id}/edit', [UploadMediaController::class, 'edit'])->middleware('auth')->name('update_form_request');
Finally, we have implemented the preview of image with the Laravel Media Library that managed by the Spatie team and jQuery. By following this simple steps we can implement the media file preview inside our web application and can manage it easily and do not require to get the Pro version of the Spatie package.
Thanks for the reading.