In this article we will describe how to handle image upload in Laravel inertia and Vue 3 and we will show a preview of the image when upload success.
Inertia is a laravel package that allows to create client-side apps SPA without creating API’s, instead of returning json responses Inertia works by returning an inertia response from controller actions. Inertia can work alongside Vue2, Vue3 or React.
For this example i suppose that you already have a latest version of laravel project. Let’s install inertia dependencies:
server side dependencies:
composer require inertiajs/inertia-laravel
Next create the root template:
resources/views/app.blade.php
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Inertia Upload Demo</title> <!-- Fonts --> <link rel="preconnect" href="https://fonts.bunny.net"> <link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" /> @vite('resources/js/app.js') @inertiaHead </head> <body> <div> @inertia </div> </body> </html>
The root template is where your inertia app will be loaded. I included the @vite directive to load the compiled javascript and also the @inertiaHead and @inertia directives.
Next setup the Inertia middleware:
php artisan inertia:middleware
This will create the HandleInertiaRequestsMiddleware in app/Http/Middleware directory.
Register this middleware to bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) { $middleware->web(append: [ \App\Http\Middleware\HandleInertiaRequests::class ]); })
client side dependencies:
Install npm dependencies:
npm install
As we will be using Vue 3 along with inertia so install the Vue 3 Inertia adapter:
npm install @inertiajs/vue3
Also install the vite vue plugin:
npm install @vitejs/plugin-vue
Update vite.config.js by including the vue plugin:
import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import vue from "@vitejs/plugin-vue"; export default defineConfig({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true, }), vue() ], });
After installing add this code to resources/js/app.js
import './bootstrap'; import {createApp, h} from "vue"; import { createInertiaApp } from '@inertiajs/vue3' createInertiaApp({ resolve: name => { const pages = import.meta.glob('./Pages/**/*.vue', { eager: true }) return pages[`./Pages/${name}.vue`] }, setup({ el, App, props, plugin }) { createApp({ render: () => h(App, props) }) .use(plugin) .mount(el) }, })
The createInertiaApp() function boots the inertia app. The resolve() callback return the appropriate page according to the server response, while the setup() callback mounts and triggers the client-side app.Â
Compile the assets with:
npm run dev
Next create a new directory Pages/ inside of resources/js/ directory that will contain the app pages.
Creating Upload Controller
Create a new controller
php artisan make:controller UploadController
Open app/Http/Controllers/UploadController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Inertia\Inertia; class UploadController extends Controller { public function index() { return Inertia::render('Upload'); } public function store(Request $request) { } }
I added a method index() that render an inertia page called “Upload” and the method store() that will process the uploading of the file. Let’s create the upload page as a vue component
resources/js/Pages/Upload.vue
<script setup> </script> <template> <div> Inertia upload demo </div> </template> <style scoped> </style>
Update the routes to point to this page:
routes/web.php
Route::get('/', [\App\Http\Controllers\UploadController::class, "index"]); Route::post('/upload', [\App\Http\Controllers\UploadController::class, "store"]);
You may need to run php artisan optimize so that the routes cleared from cache.
Now launch the project using php artisan serve, you will see the “inertia upload demo” text.
Database Migration
Create a db migration to store the uploaded file:
php artisan make:migration create_uploads_table
Then open the migration file located at database/migrations/ directory and update the up() method like so:
public function up(): void { Schema::create('uploads', function (Blueprint $table) { $table->id(); $table->string('file'); $table->timestamps(); }); }
I added a new field “file” represent the uploaded file name.
Now migrate the database:
php artisan migrate
This will run the migrations and create the db tables.
Create a new model for the uploads table:
php artisan make:model Upload
app/Models/Upload.php
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Upload extends Model { use HasFactory; protected $table = "uploads"; protected $fillable = ["file"]; protected $appends = ["file_url"]; public function getFileUrlAttribute() { return $this->file_url = $this->file ? url('/uploads/' . $this->file) : ""; } }
I have set the $fillable and $table properties. I added a custom attribute file_url that contain the full url of the uploaded file.
Upload Form
Let’s go to resources/js/Pages/Upload.vue and update with this code:
<script setup> import {ref} from "vue"; import {router} from '@inertiajs/vue3' const file = ref(null); // to get the validation errors defineProps({ errors: Object }); const onChange = event => { file.value = event.target.files[0]; } const handleSubmit = () => { router.post('/upload', { file: file.value }); } </script> <template> <div> <h2>Image Upload</h2> <div class="errors" v-if="errors && errors.file"> {{errors.file}} </div> <div v-if="$page.props.flash.success" class="success"> {{ $page.props.flash.success }} </div> <form method="post" @submit.prevent="handleSubmit"> <div class="field"> <label><b>Image</b></label> <input type="file" name="file" accept="image/*" @input="onChange" /> </div> <img :src="$page.props.extra_data.file_url" v-if="$page.props.extra_data && $page.props.extra_data.file_url" /> <div> <input type="submit" /> </div> </form> </div> </template> <style scoped> * { font-family: Arial, sans-serif; } .field { display: flex; gap: 5px; } input[type=file] { font-size: 16px; color: #14c7d9; } input[type=submit] { margin-top: 34px; background: #14c7d9; font-size: 18px; border-radius: 3px; border: 4px solid #14c7d9; padding: 4px; cursor: pointer; } input[type=submit]:hover { background: #0da4b3; } .errors { background-color: #e37070; padding: 11px 11px; border-radius: 3px; margin-bottom: 27px; } .success { background-color: #70e37a; padding: 11px 11px; border-radius: 3px; margin-bottom: 27px; } img { width: 300px; height: 300px; margin-top: 12px; margin-bottom: 8px; border: 1px solid #cbc8c8; } </style>
In this code i have added some basic styling in the <style scoped> tag. Next in the <template> i added the upload form html. The form contains an input file and a submit button.
I created a ref variable file to store the selected file from the input using the onChange() function:
const onChange = event => { file.value = event.target.files[0]; }
In case you need to upload multiple files, you have to add the multiple attribute to <input type=”file”> tag like so:
<input type="file" multiple />
When submitting the form the handleSubmit() function triggered which invokes inertia router.post() method to post the data to the server and sent the file as the post body:
router.post('/upload', { file: file.value });
To display the validation errors, the errors can be obtained from vue defineProps() function like so:
defineProps({ errors: Object });
And then accessed in the template. Likewise i displayed the success message from the page props using $page.props.flash.success.
A preview of the image is displayed on upload success from the page props using $page.props.extra_data.file_url.
At this points let’s add the server side code for handling upload, so open UploadController.php and update as shown:
<?php namespace App\Http\Controllers; use App\Models\Upload; use Illuminate\Http\Request; use Inertia\Inertia; class UploadController extends Controller { public function index() { return Inertia::render('Upload'); } public function store(Request $request) { $request->validate([ 'file' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:10000' ]); // upload $fileName = time().'.'.$request->file->extension(); $request->file->move(public_path('uploads'), $fileName); $upload = Upload::create([ 'file' => $fileName ]); return redirect()->back() ->with('success', 'Image uploaded successfully') ->with('extra_data', $upload); } }
In the store() method first i made a simple validation on the file to make sure it’s required and an image file. Then i created a dummy name of the file before storing it in the database and using laravel $request->file->move() method to do the uploading.
To insert the file i used the Upload eloquent model create() function passing in the file attribute. We then redirect back along with success message and an extra data that contains the inserted object.
Update the HandleInertiaRequests.php share() method like so:
public function share(Request $request): array { return array_merge(parent::share($request), [ 'flash' => [ 'success' => fn () => $request->session()->get('success') ], 'extra_data' => fn() => $request->session()->get('extra_data') ]); }
The share() method shares global data to the client side app like flash messages so that we can capture it in the Vue page as described above like:
$page.props.extra_data.file_url