Build Modern Laravel Apps Using Inertia.js - Note

laravel new inertia
cd inertia
composer require inertiajs/inertia-laravel
php artisan inertia:middleware
npm install vue@next
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
    @vite('resources/js/app.js')
    @inertiaHead
  </head>
  <body>
    @inertia
  </body>
</html>
views/app.blade.php
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)
  },
})
js/app.js

create directory resources/js/Pages

npm install @vitejs/plugin-vue
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({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ],
});
vite.config.js
Route::get('/', function () {
    return inertia('Welcome');
});
routes/web.php

Create Welcome.vue

<template>
    Hello World
</template>

<script>
export default {};
</script>

To make shared component

<template>
    <h1>Home</h1>
    <Nav />
    
</template>

<script>
import Nav from '../Shared/Nav.vue';
export default {
    components: { Nav }
};
</script>
Home.vue
<template>
    <nav>
        <ul>
            <li><Link href="/">Home</Link></li>
            <li><Link href="/users">Users</Link></li>
            <li><Link href="/settings">Settings</Link></li>
        </ul>
    </nav>
</template>

<script>
import {Link} from "@inertiajs/vue3";
export default {
    components: {Link}
}
</script>
Shared/Nav.vue
Route::get('/users', function(){
    return Inertia::render('Users', [
        'time' => now()->toTimeString()
    ]);
});

<div style="margin-top: 400px;">
    <p>The current time is {{ time }}</p>
    <Link href="/users" preserve-scroll>Refresh</Link>
</div>

export default {
    props: { time: String }
};
to pass data
<Link href="/users" preserve-scroll>Refresh</Link>
to preserve scroll ( same as e.preventDefault() )

Shared data

public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'auth' => [
                'user' => [
                    'username' => 'John Doe'
                ]
            ]
        ]);
    }

<p>Welcome {{ username }}</p>

export default {
		...
    computed: {
        username() {
            return this.$page.props.auth.user.username;
        }
    }
};
middleware HandleInertiaRequests

Global Component

createApp({ render: () => h(App, props) })
      .use(plugin)
      .component('Link', Link)
      .mount(el)

Persistent Layout

export default {
    components : { Layout }
};

// to

export default {
    layout: Layout
};

Default Layout

import Layout from './Shared/Layout.vue'

createInertiaApp({
  resolve: name => {
    const pages = import.meta.glob('./Pages/**/*.vue', { eager: true })
    let page = pages[`./Pages/${name}.vue`]
    page.default.layout = page.default.layout || Layout
    return page
  },
  // ...
})

Dynamic title and meta tags

<template>
    <Head>
        <title>My app - Home</title>
    </Head>
    <h1 class="text-3xl">Home</h1>
</template>
<script setup>
import { Head } from '@inertiajs/vue3';
</script>

OR

<template>
    <Head title="App - Home" />
    <h1 class="text-3xl">Home</h1>
</template>
<script setup>
import { Head } from '@inertiajs/vue3';
</script>

OR

createInertiaApp({
  ...
  title: title => 'My App - ' + title
});

Listing users

Route::get('/users', function(){
    return Inertia::render('Users', [
        'users' => User::all()->map(fn($user) => [
            'name' => $user->name
        ])
    ]);
});

<template>
    <Head title="Users" />
    <h1 class="text-3xl">Users</h1>
    <ul>
        <li v-for="user in users" :key="user.id" v-text="user.name"></li>
    </ul>
</template>

<script setup>
defineProps({ users: Array });
</script>

Pagination

<template>
    <Head title="Users" />
    <h1 class="text-3xl">Users</h1>
    <div class="overflow-hidden rounded-lg border border-gray-200 shadow-md m-5">
    <table class="w-full border-collapse bg-white text-left text-sm text-gray-500">
    <tbody class="divide-y divide-gray-100 border-t border-gray-100">
        <tr class="hover:bg-gray-50" v-for="user in users.data" :key="user.id" >
        <th class="flex gap-3 px-6 py-4 font-normal text-gray-900">
            <div class="text-sm">
            <div class="font-medium text-gray-700">{{ user.name }}</div>
            </div>
        </th>
        
        <td class="px-6 py-4">
            <div class="flex justify-end gap-4">
            <Link x-data="{ tooltip: 'Delete' }" :href="`users/${user.id}/delete`">
                <svg
                xmlns="<http://www.w3.org/2000/svg>"
                fill="none"
                viewBox="0 0 24 24"
                stroke-width="1.5"
                stroke="currentColor"
                class="h-6 w-6"
                x-tooltip="tooltip"
                >
                <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
                />
                </svg>
            </Link>
            <Link x-data="{ tooltip: 'Edite' }" :href="`users/${user.id}/edit`">
                <svg
                xmlns="<http://www.w3.org/2000/svg>"
                fill="none"
                viewBox="0 0 24 24"
                stroke-width="1.5"
                stroke="currentColor"
                class="h-6 w-6"
                x-tooltip="tooltip"
                >
                <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
                />
                </svg>
            </Link>
            </div>
        </td>
        </tr>
    </tbody>
    </table>
    <Pagination :links="users.links" />
</div>
</template>

<script setup>
import Pagination from "../Shared/Pagination.vue";
defineProps({ users: Object });
</script>
Users.vue
<template>
    <div class="mt-5">
        <Component 
        :is="link.url ? 'Link' : 'span'" 
        class="px-4"
        :class="{ 'text-gray-400' : !link.url, 'font-bold': link.active}"
        v-for="link in links" :href="link.url" v-html="link.label"></Component>
    </div>
</template>

<script>
export default {
    props: {
        links: Array
    }
}
</script>
Pagination.vue
<input type="text" placeholder="Search..." v-model="search" class="border px-2 rounded-lg">

<script setup>
import Pagination from "../Shared/Pagination.vue";
import { ref, watch } from "vue";
import { router } from '@inertiajs/vue3'
let props = defineProps({ 
    users: Object,
    filters: Object,
});
let search = ref(props.filters.search);
watch(search, value => {
    router.get(
        '/users', { search: value }, {
            preserveState: true,
            replace: true,
        }
    )
})
</script>

Route::get('/users', function(){
    return Inertia::render('Users', [
        'users' => User::query()
                ->when(Request::input('search'), function($query, $search) {
                    $query->where('name', 'like', '%'.$search.'%');
                })
                ->paginate(10)
                ->withQueryString()
                ->through(fn($user) => [
                    'id' => $user->id,
                    'name' => $user->name,
                ]),
        'filters' => Request::only(['search'])
    ]);
});
user index with search function
Route::get('/users/create', function(){
    return Inertia::render('Users/Create');
});

Route::post('/users', function(Request $request){
    $attributes = Validator::validate($request->all(), [
        'form.name' => ['required'],
        'form.email' => ['required', 'email'],
        'form.password' => ['required'],
    ]);

    User::create($attributes["form"]);
    return redirect('/users');
});

<template>
    <Head title="Users" />
    <h1 class="text-3xl">Create New Users</h1>
    <form @submit.prevent="submit" action="/" method="POST" class="max-w-md mx-auto mt-8">
        <div class="mb-5">
            <label for="Name">Name</label>
            <input v-model="form.name" type="text" name="name" class="w-full border">
            <div v-if="errors['form.name']" v-text="errors['form.name']" class="text-red-500 text-sm mt-1" required></div>
        </div>
        <div class="mb-5">
            <label for="Email">Email</label>
            <input v-model="form.email"  type="email" name="email" class="w-full border">
            <div v-if="errors['form.email']" v-text="errors['form.email']" class="text-red-500 text-sm mt-1" required></div>
        </div>
        <div class="mb-5">
            <label for="Password">Password</label>
            <input v-model="form.password"  type="password" name="password" class="w-full border">
            <div v-if="errors['form.password']" v-text="errors['form.password']" class="text-red-500 text-sm mt-1" required></div>
        </div>
        <div class="mb-5">
            <button type="submit">Submit</button>
        </div>
    </form>
</template>

<script setup >
import { reactive } from 'vue';
import { router } from '@inertiajs/vue3'

    defineProps({
        errors: Object
    })

    let form = reactive({
        name: '',
        email: '',
        password: ''
    });

    let submit = () => {
        router.post(
            '/users', { form }
        )
    }
</script>
user create
import throttle from "lodash/throttle"
watch(search, throttle( function(value) {
    router.get(
        '/users', { search: value }, {
            preserveState: true,
            replace: true,
        }
    )
}, 500))
Throttle (every 500ms)
import debounce from "lodash/debounce"
watch(search, debounce( function(value) {
    router.get(
        '/users', { search: value }, {
            preserveState: true,
            replace: true,
        }
    )
}, 500))
Debounce (after finishing typing)

Inertia and SPA Technique

Import Aliases

I’m using Vite so this is the code

const path = require('path');

export default defineConfig({
    resolve: {
        alias: {
            '@' : path.resolve('resources/js')
        },
    },
});
npx mix -p // production
mix.extract() // to extract node modules

Api Resource

php artisan make:resource UserResource

return UserResource::collection(User::all());

UserResource::collection(
    User::query()
    ->when(\\Request::input('search'), function($query, $search) {
        $query->where('name', 'like', '%'.$search.'%');
    })
    ->paginate(10)
    ->withQueryString()
),

Wrap In a Module

example Home.vue
move it to js/services/SyntaxHighlighting.js
back to Home.vue