How to upload multiple images with preview using Laravel and Vue (SSR)
Tags: laravel, vue, webdev, javascript
Published: 2022-01-02 14:27:24

**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](https://github.com/saimow/vue-media-upload) package.

[Vue-Media-Upload](https://github.com/saimow/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ato4h5ff04hae8yvk1jq.PNG)

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
```php
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title', 255);
    $table->string('content', 3000);
    $table->timestamps();
});
```
and the `images` table
```php
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:
```sh
$ npm install vue-media-upload
```
or via yarn
```sh
$ yarn add vue-media-upload
```

after the installation you can import it on your `app.js` file
```js
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`
```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`.
```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
```html
<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:
```sh
$ 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.
```php
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
```php
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/` 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.
```html
<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:
```php
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.](https://github.com/saimow/media-upload-demo/tree/SSR)

You can also check how the package works with a Server-Rendered Form [here.](https://github.com/saimow/vue-media-upload#-how-it-works-in-a-server-rendered-form)

# 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](https://laravel.com/docs/9.x/scheduling) to cleanup all those images that have not been used.