DEV Community

Cover image for How to upload multiple images with preview using Laravel and Vue (SSR)
Mohamed Hafidi
Mohamed Hafidi

Posted on • Updated on

How to upload multiple images with preview using Laravel and Vue (SSR)

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.

Image description

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();
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

or via yarn

$ yarn add vue-media-upload
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

we should of course also create crud controllers:

└── PostController
    ├── index.php
    ├── Create.php
    ├── Update.php
    ├── Delete.php
    └── Upload.php
Enter fullscreen mode Exit fullscreen mode

Also make sure to create the laravel symbolic link using:

$ php artisan storage:link
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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/ and posts/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>
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Latest comments (3)

Collapse
 
mashraf1312 profile image
Mohamed Al-ashraf

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?

Collapse
 
ansezz profile image
ANASS EZ-ZOUAINE

Thanks you

Collapse
 
sharifcse57 profile image
Md. Shariful Islam (Mehedi)

Great article.. really appreciated man.