Image upload is one of the most popular features in modern websites. But from all the components that can make up a form, the image upload component could be one of the most frustrating for a lot of developers since it demand a lot of effort and styling. And that's why I created vue-media-upload package.
Vue-Media-Upload is an easy to setup Vue package for multiple images upload with preview that support the create and the update form with both SSR and CSR, this package handle the upload via axios requests.
For this tutorial, we will create a simple SSR-based CRUD where you can also upload images using Laravel 9, Vue 3 and Bootstrap 5.
As you can see, media-upload preview the images instead of just an html input file field.
Step 1: Database - Migrations
First of all, let's start with creating the migrations.
we will need two tables, the posts
table
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title', 255);
$table->string('content', 3000);
$table->timestamps();
});
and the images
table
Schema::create('images', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained('posts')->onDelete('cascade');
$table->string('name', 255);
$table->timestamps();
});
and don't forget to setup your Models too.
Step 2: Media-Upload installation
You can install media-upload via npm:
$ npm install vue-media-upload
or via yarn
$ yarn add vue-media-upload
after the installation you can import it on your app.js
file
import './bootstrap';
import {createApp} from 'vue/dist/vue.esm-bundler.js';
import Uploader from 'vue-media-upload';
const app = createApp({})
app.component('Uploader' , Uploader);
app.mount('#app')
Step 3: Setup Routes
As always we should create crud routes in the web.php
use App\Http\Controllers\PostController;
Route::get('/', [PostController\Index::class, 'index'])->name('posts');
Route::get('/posts/create', [PostController\Create::class, 'index'])->name('posts.create');
Route::post('/posts/create', [PostController\Create::class, 'store'])->name('posts.create');
Route::get('/posts/update/{post}', [PostController\Update::class, 'index'])->name('posts.update');
Route::put('/posts/update/{post}', [PostController\Update::class, 'update'])->name('posts.update');
Route::delete('/posts/delete/{post}', [PostController\Delete::class, 'destroy'])->name('posts.destroy');
and also we will need to create a route for our upload handler in the api.php
.
use App\Http\Controllers\PostController;
Route::post('/posts/media/upload', [PostController\Upload::class, 'store'])->name('posts.media.upload');
Step 4: Create/Add form
In our create.blade.php
we will create the title and the content inputs and include our <Upload />
component in the form
<form action="{{ route('posts.create') }}" method="POST" class="p-4">
@csrf
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control @error('title') is-invalid @enderror" id="title" name="title">
@error('title')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Content</label>
<textarea name="content" cols="30" rows="10" class="form-control @error('content') is-invalid @enderror" id='content'>{{ old('content') }}</textarea>
@error('content')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<div class="mb-4">
<label class="form-label">Media</label>
<div id="app">
<Uploader
server="/api/posts/media/upload"
:is-invalid="@error('media') true @else false @enderror"
/>
</div>
@error('media')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<button class="btn btn-primary">Submit</button>
</form>
we should of course also create crud controllers:
└── PostController
├── index.php
├── Create.php
├── Update.php
├── Delete.php
└── Upload.php
Also make sure to create the laravel symbolic link using:
$ php artisan storage:link
In the upload handler PostController\Upload.php
we will create a method store()
that temporary stores the uploaded image in app\public\tmp\uploads\
in the storage location.
public function store(Request $request){
$path = storage_path('app/public/tmp/uploads');
$file = $request->file('image');
$name = uniqid() . '_' . trim($file->getClientOriginalName());
$file->move($path, $name);
return ['name' => $name];
}
As a brief explanation: the store()
method will give the uploaded image a unique name and stores it in app\public\tmp\uploads\
, and it will return as a response the unique name to the <Upload />
component so it could continue its work.
Create Post Controller
And in our create controller PostController\Create.php
this is how the store()
function looks like
public function store(Request $request){
$validated = $request->validate([
'title' => 'required|max:255',
'content' => 'required|max:3000',
'media' => 'required'
]);
$post = Post::create([
'title'=>$request->title,
'content'=>$request->content,
]);
if(isset($request->media)){
foreach($request->added_media as $image){
$from = storage_path('app/public/tmp/uploads/' . $image);
$to = storage_path('app/public/posts/media/' . $image);
File::move($from, $to);
$post->images()->create([
'name' => $image,
]);
}
}
return redirect()->route('posts');
}
This code simply store the post and uses the unique images names to move the added images from the temporary location app/public/tmp/uploads/
file to its final location app/public/posts/media/
.
Note that
tmp/uploads/
andposts/media/
directories need to be created!
Step 5: Update/Edit form
In the update form update.blade.php
we will need to pass the stored images to the :media
prop, and we need to pass the temporary location where the images are being stored to the :location
prop.
<form action="{{ route('posts.update', $post->id) }}" method="POST" class="p-4">
@method('PUT')
@csrf
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control @error('title') is-invalid @enderror" id="title" name="title" value="{{ old('title', $post->title )}}">
@error('title')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Content</label>
<textarea name="content" cols="30" rows="10" class="form-control @error('content') is-invalid @enderror" id='content'>{{ old('content', $post->content) }}</textarea>
@error('content')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<div class="mb-4">
<label class="form-label">Media</label>
<div id="app">
<Uploader
server="/api/posts/media/upload"
:is-invalid="@error('media') true @else false @enderror"
:media="{{ json_encode($media) }}"
location="/storage/posts/media"
/>
</div>
@error('media')
<p class="text-danger">{{ $message }}</p>
@enderror
</div>
<button class="btn btn-primary">update</button>
</form>
Update post controller
This is what our Update controller PostController\Update.php
looks like:
class Update extends Controller
{
public function index(Post $post){
$media = $post->images()->get();
return view('posts.update', ['post'=>$post, 'media' => $media]);
}
public function update(Post $post, Request $request){
$validated = $request->validate([
'title' => 'required|max:255',
'content' => 'required|max:3000',
'media' => 'required'
]);
$post->update($request->all());
if(isset($request->added_media)){
foreach($request->added_media as $image){
$from = storage_path('app/public/tmp/uploads/' . $image);
$to = storage_path('app/public/posts/media/' . $image);
File::move($from, $to);
$post->images()->create([
'name' => $image,
]);
}
}
if(isset($request->removed_media)){
foreach($request->removed_media as $image){
File::delete(storage_path('app/public/posts/media/'.$image));
Image::where('name', $image)->delete();
}
}
return redirect()->route('posts');
}
}
this function simply update the post and add the added images and delete the removed images.
You can find this project in the Branch SSR of the package demo project.
You can also check how the package works with a Server-Rendered Form here.
Homework
In the situation when a user upload the images on the form but leave the form before the final submit, the temporary images are still stored on the server and won't get moved or deleted.
well it’s up to you how to deal with this situation, but I recommend you to schedule an artisan command using Laravel scheduling to cleanup all those images that have not been used.
Top comments (3)
Great article.. really appreciated man.
Thanks you
I am sending an upload request to my API but I need to send header parameters along with the request. Any Idea how can I do that?