Laravel 9 Inertia Js SPA CRUD with Example

Hello Artisans, today we'll discuss about SPA (single page application) CRUD with Laravel & Inertia Js. Inertia Js isn't a framework, neither it's a replacement of your backend of frontend. It's just connect two of them. Currently it'll support three official client side adapters (React,Vue,Svelte) and for server side Laravel & Rails. For more you can check here. So, no more talk, let's see how we can easily crate a simple SPA CRUD application using Laravel & Inertia Js.

Note: Tested on Laravel 9.11

Table of Contents

  1. Install Laravel Application
  2. Create and Setup Controller
  3. Share Inertia Var Globally
  4. Define Routes
  5. Create and Setup Vue Template

Install Laravel Application

First of all make a fresh Laravel Application using the below commands.

composer global require laravel/installer

//and then run 

laravel new inertia --jet

Create and Setup Controller

First of all, create a controller so that we can write our logics or query to show the result. So, fire the below commands in the terminal.

 php artisan make:controller UserController

It'll create a controller under app\Http\Controllers called UserController.php. Open the file and replace with below codes.

app/Http/Controllers/UserController.php
<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Inertia\Inertia;

class UserController extends Controller
{
    public function index(User $user): \Inertia\Response
    {
        return Inertia::render(
            'users',
            [
                'data' => $user->latest()->get()
            ]
        );
    }

    public function store(Request $request)
    {
        Validator::make($request->all(), [
            'name' => ['required'],
            'email' => ['required'],
            'password' => ['required','min:6','confirmed',],
        ])->validate();

        $request['password'] = bcrypt($request->password);
        User::create($request->all());

        return redirect()->back()
            ->with('message', 'User Created Successfully.');
    }

    public function update(Request $request)
    {
        Validator::make($request->all(), [
            'name' => ['required'],
            'email' => ['required'],
            'password' => ['required','min:6','confirmed',],
        ])->validate();


        $request['password'] = bcrypt($request->password);
        User::find($request->id)->update($request->all());

        return redirect()->back()
                ->with('message', 'User Updated Successfully.');

    }

    public function destroy(Request $request): \Illuminate\Http\RedirectResponse
    {
        User::destroy($request->id);

        return redirect()->back()
            ->with('message', 'User deleted successfully.');
    }
}

Share Inertia Var Globally

Now we'll share the the message variable and errors variable globally through our ServiceProvider. So, that we can use these variable in our Components. So, open your file and replace with the following codes.

app\Providers\AppServiceProvider.php
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Inertia\Inertia;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }

    public function boot()
    {
        Inertia::share([
            'errors' => function () {
                return session()->get('errors')
                    ? session()->get('errors')->getBag('default')->getMessages()
                    : (object) [];
            },
        ]);

        Inertia::share('flash', function () {
            return [
                'message' => session()->get('message'),
            ];
        });
    }
}

Define Routes

Now we need to put the below routes in web.php. Replace it with below codes.

routes/web.php
<?php

use Illuminate\Support\Facades\Route;

Route::resource('users',\App\Http\Controllers\UserController::class)->except('create','show','edit');

Create and Setup Vue Template

Here, we'll create an vue template named users.vue, where we'll write code to list of articles and create and update model code. So create the component, open it and replace with following codes.

resources\js\Pages\users.vue
<template>
    <app-layout>
        <template #header>
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                Registered User List
            </h2>
        </template>
        <div class="py-12">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg px-4 py-4">
                    <div class="bg-teal-100 border-t-4 border-teal-500 rounded-b text-teal-900 px-4 py-3 shadow-md my-3" role="alert" v-if="$page.flash.message">
                        <div class="flex">
                            <div>
                                <p class="text-sm">{{ $page.flash.message }}</p>
                            </div>
                        </div>
                    </div>
                    <button @click="openModal()" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded my-3">Create New Post</button>
                    <table class="table-fixed w-full">
                        <thead>
                        <tr class="bg-gray-100">
                            <th class="px-4 py-2 w-20">No.</th>
                            <th class="px-4 py-2">Title</th>
                        </tr>
                        </thead>
                        <tbody>
                        <tr v-for="(row,i) in data" :key="row.id">
                            <td class="border px-4 py-2">{{ ++i }}</td>
                            <td class="border px-4 py-2">{{ row.name }}</td>
                            <td class="border px-4 py-2">
                                <button @click="edit(row)" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Edit</button>
                                <button @click="deleteRow(row)" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Delete</button>
                            </td>
                        </tr>
                        </tbody>
                    </table>
                    <div class="fixed z-10 inset-0 overflow-y-auto ease-out duration-400" v-if="isOpen">
                        <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">

                            <div class="fixed inset-0 transition-opacity">
                                <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
                            </div>

                            <span class="hidden sm:inline-block sm:align-middle sm:h-screen"></span>โ€‹
                            <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" role="dialog" aria-modal="true" aria-labelledby="modal-headline">
                                <form>
                                    <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
                                        <div class="">
                                            <div class="mb-4">
                                                <label for="name" class="block text-gray-700 text-sm font-bold mb-2">Name:</label>
                                                <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="name" placeholder="Enter Name" v-model="form.name">
                                                <div v-if="$page.errors.name" class="text-red-500">{{ $page.errors.name[0] }}</div>
                                            </div>
                                            <div class="mb-4">
                                                <label for="email" class="block text-gray-700 text-sm font-bold mb-2">Email:</label>
                                                <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="email" placeholder="Enter Email" v-model="form.email">
                                                <div v-if="$page.errors.email" class="text-red-500">{{ $page.errors.email[0] }}</div>
                                            </div>
                                            <div class="mb-4">
                                                <label for="password" class="block text-gray-700 text-sm font-bold mb-2">Password:</label>
                                                <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="password" placeholder="Enter Password" v-model="form.password">
                                                <div v-if="$page.errors.password" class="text-red-500">{{ $page.errors.password[0] }}</div>
                                            </div>
                                            <div class="mb-4">
                                                <label for="password_confirmation" class="block text-gray-700 text-sm font-bold mb-2">Confirm Password:</label>
                                                <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="password_confirmation" placeholder="Confirm Password" v-model="form.password_confirmation">
                                                <div v-if="$page.errors.password_confirmation" class="text-red-500">{{ $page.errors.password_confirmation[0] }}</div>
                                            </div>
                                        </div>
                                    </div>
                                    <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
                            <span class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
                              <button wire:click.prevent="store()" type="button" class="inline-flex justify-center w-full rounded-md border border-transparent px-4 py-2 bg-green-600 text-base leading-6 font-medium text-white shadow-sm hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green transition ease-in-out duration-150 sm:text-sm sm:leading-5" v-show="!editMode" @click="save(form)">
                                Save
                              </button>
                            </span>
                                        <span class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
                              <button wire:click.prevent="store()" type="button" class="inline-flex justify-center w-full rounded-md border border-transparent px-4 py-2 bg-green-600 text-base leading-6 font-medium text-white shadow-sm hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green transition ease-in-out duration-150 sm:text-sm sm:leading-5" v-show="editMode" @click="update(form)">
                                Update
                              </button>
                            </span>
                                        <span class="mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto">

                              <button @click="closeModal()" type="button" class="inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 bg-white text-base leading-6 font-medium text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue transition ease-in-out duration-150 sm:text-sm sm:leading-5">
                                Cancel
                              </button>
                            </span>
                                    </div>
                                </form>

                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

    </app-layout>
</template>

<script>

import AppLayout from '@/Layouts/AppLayout'
import Welcome from '@/Jetstream/Welcome'

export default {
    name: "users",
    components: {
        AppLayout,
        Welcome,
    },
    props: ['data', 'errors'],
    data() {
        return {
            editMode: false,
            isOpen: false,
            form: {
                name: null,
                email: null,
                password: null,
                password_confirmation: null,
            },
        }
    },
    methods: {
        openModal() {
            this.isOpen = true;
        },
        closeModal() {
            this.isOpen = false;
            this.reset();
            this.editMode=false;
        },
        reset() {
            this.form = {
                name: null,
                email: null,
                password: null,
                password_confirmation: null,
            }
        },
        save(data) {
            this.$inertia.post('/users', data)
            this.reset();
            this.closeModal();
            this.editMode = false;
        },
        edit(data) {
            this.form = Object.assign({}, data);
            this.editMode = true;
            this.openModal();
        },
        update(data) {
            data._method = 'PATCH';
            this.$inertia.post('/users/' + data.id, data)
            this.reset();
            this.closeModal();
        },
        deleteRow(data) {
            if (!confirm('Are you sure want to remove?')) return;
            data._method = 'DELETE';
            this.$inertia.post('/users/' + data.id, data)
            this.reset();
            this.closeModal();
        },
    },
}
</script>

<style scoped>

</style>

That's it for today. I hope you've enjoyed this tutorial. You can also download this tutorial from GitHub. Thanks for reading. ๐Ÿ™‚