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>
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)
},
})
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,
},
},
}),
],
});
Route::get('/', function () {
return inertia('Welcome');
});
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>
<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>
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 }
};
<Link href="/users" preserve-scroll>Refresh</Link>
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;
}
}
};
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>
<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>
<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'])
]);
});
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>
import throttle from "lodash/throttle"
watch(search, throttle( function(value) {
router.get(
'/users', { search: value }, {
preserveState: true,
replace: true,
}
)
}, 500))
import debounce from "lodash/debounce"
watch(search, debounce( function(value) {
router.get(
'/users', { search: value }, {
preserveState: true,
replace: true,
}
)
}, 500))
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()
),