Back to Course |
Vue.js 3 + Laravel 11 + Vite: SPA CRUD

Routing: Nav Links and Posts Create Page

So we have built our first page: list of the posts. Now let's build the second page to add a new post. For this lesson, I will introduce the vue-router.

create posts page


Installing and Enabling vue-router

First, we need to install the vue-router package.

npm install vue-router@latest

Then, we need to import vue-router.

resources/js/app.js:

import './bootstrap';
 
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import PostsIndex from './components/Posts/Index.vue'
 
createApp({})
.component('PostsIndex', PostsIndex)
.mount('#app')

Then, we need to build a list of routes, initialize the router, and enable that router inside createApp().

Also, we don't need to define components in the createApp() anymore, as they are already defined in the routes.

resources/js/app.js:

import './bootstrap';
 
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import PostsIndex from './components/Posts/Index.vue'
 
const routes = [
{ path: '/', component: PostsIndex },
]
 
const router = createRouter({
history: createWebHistory(),
routes
})
 
createApp({})
.component('PostsIndex', PostsIndex)
.use(router)
.mount('#app')

Vue Component for Second Page

Now, let's create a Vue component for the create post page and add some dummy text.

resources/js/components/Posts/Create.vue:

<template>
To-do.
</template>

Next, we can create a new route and point it to this Vue component.

resources/js/app.js:

import './bootstrap';
 
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import PostsIndex from './components/Posts/Index.vue'
import PostsCreate from './components/Posts/Create.vue'
 
const routes = [
{ path: '/', component: PostsIndex },
{ path: '/posts/create', component: PostsCreate },
]
 
const router = createRouter({
history: createWebHistory(),
routes
})
 
createApp({})
.use(router)
.mount('#app')

Changing Layout: From Blade to Vue

But now, we cannot add new links to the navigation, they are in the Laravel Blade files of Breeze and not in the Vue files. So, we need to change all the structure to be .vue files, including the main layout, so that we would be able to use <router-link>.

First, we will use the resources/views/dashboard.blade.php file as a main HTML file where we set id="app" in the <body> tag, instead of the previous <div id="app">.

resources/views/dashboard.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">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
 
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
 
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased" id="app">
</body>
</html>

Next, we need to have a root component: the main layout.

resources/js/layouts/App.vue:

<template>
<div class="min-h-screen bg-gray-100">
<nav class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="/">
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" class="block h-9 w-auto fill-current text-gray-800">
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
</svg>
</a>
</div>
 
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<router-link to="/" active-class="border-b-2 border-indigo-400" class="inline-flex items-center px-1 pt-1 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out">
Posts
</router-link>
<router-link to="/posts/create" active-class="border-b-2 border-indigo-400" class="inline-flex items-center px-1 pt-1 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out">
Create Post
</router-link>
</div>
</div>
</div>
</div>
</nav>
 
<!-- Page Heading -->
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Dashboard
</h2>
</div>
</header>
 
<!-- Page Content -->
<main>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200">
<router-view></router-view>
</div>
</div>
</div>
</div>
</main>
</div>
</template>

See those <router-link> and <router-view>? Those come from vue-router, replacing typical HTML <a href links. When someone clicks those links, it will not refresh the full page, but only the part that is inside of the router-view. This is the SPA behavior.

The <router-view> element is where all the main dynamic content will be loaded.

Now, we need to load this layout when creating a Vue app.

resources/js/app.js:

import './bootstrap';
 
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './layouts/App.vue'
import PostsIndex from './components/Posts/Index.vue'
import PostsCreate from './components/Posts/Create.vue'
 
// ...
 
createApp(App)
createApp({})
.use(router)
.mount('#app')

Now, after visiting the project, we have two links in the navigation.

vue navigation

And if you will click Create Post you will that the PostsCreate Vue component and the URL in the address bar changed.

create posts page


Underline Active Link

The underline below active link is made by specifying classes to the active-class inside the <router-link>.

<router-link to="/" active-class="border-b-2 border-indigo-400" class="inline-flex items-center px-1 pt-1 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out">
Posts
</router-link>

Prevent Loading in Browser Directly

Now we need just to fix one problem.

If you try to access /posts/create directly via URL, you would get a 404 error.

To fix it, we need to add one route to the Laravel app: to point any route to the dashboard view file.

routes/web.php:

Route::view('/', 'dashboard')->name('dashboard');
 
Route::view('/{any?}', 'dashboard')
->where('any', '.*');

Now, after visiting any page directly via browser URL, it will be loaded inside the Vue.