Skip to content

Instantly share code, notes, and snippets.

@Shoghy
Created March 12, 2024 13:57
Show Gist options
  • Save Shoghy/6ea393e71302b334f3530e8b8449e077 to your computer and use it in GitHub Desktop.
Save Shoghy/6ea393e71302b334f3530e8b8449e077 to your computer and use it in GitHub Desktop.
How to use VueJS with Laravel 7

Note 1: I did this on Windows, I am not sure if this will work in other operating systems.

Note 2: I didn't create these files (beside this README.md), I just get them from the Laravel Breeze template and modified them to work on Laravel 7.

Note 3: In the codes shown in this README.md you will see comments that just have 3 dots, those comments are to hide some information of a file, meaning that there can be other code where the dots are.

Prerequisites

  • You need PHP 7.4 installed (I did it with PHP 7.4.9), you can download it here for windows.
  • You need NodeJS installed.
  • You need to have Composer installed. If you had composer installed before with a newer or older PHP version, re-install it with PHP 7.4.

Now, create a new Laravel 7 project, if you don't have one, using the comand:

composer create-project --prefer-dist laravel/laravel:^7.0 <project-name>

Guide

  1. Install InertiaJS: In your project's folder run the next command to install InertiaJS (I have the ^0.6.11 version installed):
composer require inertiajs/inertia-laravel
  1. In the ./app/Http/Middleware folder add the HandleInertiaRequests.php file that you can find in this Gist, and add the HandleInertiaRequests class to your web $middlewareGroups in the ./app/Http/Kernel.php, like this:
protected $middlewareGroups = [
  'web' => [
    //...
    \App\Http\Middleware\HandleInertiaRequests::class,
  ],

  'api' => [/*...*/],
];
  1. Add the Vite.php file of this Gist to the ./app/Http/Middleware just like HandleInertiaRequests.php. Don't add this one to ./app/Http/Kernel.php. This file will let you use Vite, so you don't need to build your JS files each time you want to make a change.

  2. Add the app.blade.php file of this Gist to the ./resources/views folder. This will be the blade view that InertiaJS will render.

  3. Install the next npm devDependencies, I will leave the versions I used. Also, in your ./package.json file, set the type to module.

"type": "module",
//...
"devDependencies": {
  "@inertiajs/vue3": "^1.0.15",
  "@vitejs/plugin-vue": "^5.0.4",
  "axios": "^1.6.7",
  //...
  "laravel-vite-plugin": "^1.0.2",
  "lodash": "^4.17.19",
  //...
  "vite": "^5.1.5",
  "vue": "^3.4.21"
}
  1. Add the vite.config.js and the jsconfig.json files of this Gist into the root folder of your project.

  2. Modify the ./resources/js/app.js and ./resources/js/bootstrap.js with the versions found in this Gist.

  3. Create ./resources/js/Pages folder and there you can add your Vue files.

  4. To make sure that everything is working, you can try to adding the Welcome.vue file of this Gist into the ./resources/js/Pages folder. In ./routes/web.php add the next code to run the Vue code:

<?php

//...
use Inertia\Inertia;

Route::get('/', function () {
  return Inertia::render('Welcome');
});
  1. Now, in your root folder, you just need to start vite with the command:
npx vite

or you can build the js files with:

npx vite build

Now start the Laravel server with:

php artisan serve

And you should be able to see an ugly Vue page with a counter.

How to send data to Vue from Laravel

That's as easy as sending data to a Blade template. In the render function of Inertia, after the name of the template you just need to add a second parameter, this being an array with the data you want to send, for example:

<?php

Route::get('/', function () {
  return Inertia::render('Welcome',
  [
    'canLogin' => Route::has('login'),
    'canRegister' => Route::has('register'),
    'laravelVersion' => Application::VERSION,
    'phpVersion' => PHP_VERSION,
  ]);
});

To be able to use it in Vue, you just need to define it with the defineProps function:

<script setup>
defineProps({
    canLogin: {
        type: Boolean,
    },
    canRegister: {
        type: Boolean,
    },
    laravelVersion: {
        type: String,
        required: true,
    },
    phpVersion: {
        type: String,
        required: true,
    },
});
</script>

<template>
{{ laravelVersion }}
</template>
<!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>{{ 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" />
<!-- Scripts -->
{{ app("App\Http\Middleware\Vite")(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"]) }}
@inertiaHead
</head>
<body class="font-sans antialiased">
@inertia
</body>
</html>
import './bootstrap';
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el);
},
progress: {
color: '#4B5563',
},
});
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that is loaded on the first page visit.
*
* @var string
*/
protected $rootView = 'app';
/**
* Determine the current asset version.
*/
public function version(Request $request)
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
$value = [];
$parent = parent::share($request);
foreach($parent as $k => $r){
$value[$k] = $r;
}
$value["auth"] = [
'user' => $request->user(),
];
return $value;
}
}
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["resources/js/*"]
}
},
"exclude": ["node_modules", "public"]
}
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
laravel({
input: 'resources/js/app.js',
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
});
<?php
namespace App\Http\Middleware;
use Exception;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Support\Collection;
use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
class Vite implements Htmlable
{
use Macroable;
/**
* The Content Security Policy nonce to apply to all generated tags.
*
* @var string|null
*/
protected $nonce;
/**
* The key to check for integrity hashes within the manifest.
*
* @var string|false
*/
protected $integrityKey = 'integrity';
/**
* The configured entry points.
*
* @var array
*/
protected $entryPoints = [];
/**
* The path to the "hot" file.
*
* @var string|null
*/
protected $hotFile;
/**
* The path to the build directory.
*
* @var string
*/
protected $buildDirectory = 'build';
/**
* The name of the manifest file.
*
* @var string
*/
protected $manifestFilename = 'manifest.json';
/**
* The custom asset path resolver.
*
* @var callable|null
*/
protected $assetPathResolver = null;
/**
* The script tag attributes resolvers.
*
* @var array
*/
protected $scriptTagAttributesResolvers = [];
/**
* The style tag attributes resolvers.
*
* @var array
*/
protected $styleTagAttributesResolvers = [];
/**
* The preload tag attributes resolvers.
*
* @var array
*/
protected $preloadTagAttributesResolvers = [];
/**
* The preloaded assets.
*
* @var array
*/
protected $preloadedAssets = [];
/**
* The cached manifest files.
*
* @var array
*/
protected static $manifests = [];
/**
* Get the preloaded assets.
*
* @return array
*/
public function preloadedAssets()
{
return $this->preloadedAssets;
}
/**
* Get the Content Security Policy nonce applied to all generated tags.
*
* @return string|null
*/
public function cspNonce()
{
return $this->nonce;
}
/**
* Generate or set a Content Security Policy nonce to apply to all generated tags.
*
* @param string|null $nonce
* @return string
*/
public function useCspNonce($nonce = null)
{
return $this->nonce = $nonce ?? Str::random(40);
}
/**
* Use the given key to detect integrity hashes in the manifest.
*
* @param string|false $key
* @return $this
*/
public function useIntegrityKey($key)
{
$this->integrityKey = $key;
return $this;
}
/**
* Set the Vite entry points.
*
* @param array $entryPoints
* @return $this
*/
public function withEntryPoints($entryPoints)
{
$this->entryPoints = $entryPoints;
return $this;
}
/**
* Set the filename for the manifest file.
*
* @param string $filename
* @return $this
*/
public function useManifestFilename($filename)
{
$this->manifestFilename = $filename;
return $this;
}
/**
* Resolve asset paths using the provided resolver.
*
* @param callable|null $urlResolver
* @return $this
*/
public function createAssetPathsUsing($resolver)
{
$this->assetPathResolver = $resolver;
return $this;
}
/**
* Get the Vite "hot" file path.
*
* @return string
*/
public function hotFile()
{
return $this->hotFile ?? public_path('/hot');
}
/**
* Set the Vite "hot" file path.
*
* @param string $path
* @return $this
*/
public function useHotFile($path)
{
$this->hotFile = $path;
return $this;
}
/**
* Set the Vite build directory.
*
* @param string $path
* @return $this
*/
public function useBuildDirectory($path)
{
$this->buildDirectory = $path;
return $this;
}
/**
* Use the given callback to resolve attributes for script tags.
*
* @param (callable(string, string, ?array, ?array): array)|array $attributes
* @return $this
*/
public function useScriptTagAttributes($attributes)
{
if (! is_callable($attributes)) {
$attributes = fn () => $attributes;
}
$this->scriptTagAttributesResolvers[] = $attributes;
return $this;
}
/**
* Use the given callback to resolve attributes for style tags.
*
* @param (callable(string, string, ?array, ?array): array)|array $attributes
* @return $this
*/
public function useStyleTagAttributes($attributes)
{
if (! is_callable($attributes)) {
$attributes = fn () => $attributes;
}
$this->styleTagAttributesResolvers[] = $attributes;
return $this;
}
/**
* Use the given callback to resolve attributes for preload tags.
*
* @param (callable(string, string, ?array, ?array): (array|false))|array|false $attributes
* @return $this
*/
public function usePreloadTagAttributes($attributes)
{
if (! is_callable($attributes)) {
$attributes = fn () => $attributes;
}
$this->preloadTagAttributesResolvers[] = $attributes;
return $this;
}
/**
* Generate Vite tags for an entrypoint.
*
* @param string|string[] $entrypoints
* @param string|null $buildDirectory
* @return \Illuminate\Support\HtmlString
*
* @throws \Exception
*/
public function __invoke($entrypoints, $buildDirectory = null)
{
$entrypoints = collect($entrypoints);
$buildDirectory ??= $this->buildDirectory;
if ($this->isRunningHot()) {
return new HtmlString(
$entrypoints
->prepend('@vite/client')
->map(fn ($entrypoint) => $this->makeTagForChunk($entrypoint, $this->hotAsset($entrypoint), null, null))
->join('')
);
}
$manifest = $this->manifest($buildDirectory);
$tags = collect();
$preloads = collect();
foreach ($entrypoints as $entrypoint) {
$chunk = $this->chunk($manifest, $entrypoint);
$preloads->push([
$chunk['src'],
$this->assetPath("{$buildDirectory}/{$chunk['file']}"),
$chunk,
$manifest,
]);
foreach ($chunk['imports'] ?? [] as $import) {
$preloads->push([
$import,
$this->assetPath("{$buildDirectory}/{$manifest[$import]['file']}"),
$manifest[$import],
$manifest,
]);
foreach ($manifest[$import]['css'] ?? [] as $css) {
$partialManifest = Collection::make($manifest)->where('file', $css);
$preloads->push([
$partialManifest->keys()->first(),
$this->assetPath("{$buildDirectory}/{$css}"),
$partialManifest->first(),
$manifest,
]);
$tags->push($this->makeTagForChunk(
$partialManifest->keys()->first(),
$this->assetPath("{$buildDirectory}/{$css}"),
$partialManifest->first(),
$manifest
));
}
}
$tags->push($this->makeTagForChunk(
$entrypoint,
$this->assetPath("{$buildDirectory}/{$chunk['file']}"),
$chunk,
$manifest
));
foreach ($chunk['css'] ?? [] as $css) {
$partialManifest = Collection::make($manifest)->where('file', $css);
$preloads->push([
$partialManifest->keys()->first(),
$this->assetPath("{$buildDirectory}/{$css}"),
$partialManifest->first(),
$manifest,
]);
$tags->push($this->makeTagForChunk(
$partialManifest->keys()->first(),
$this->assetPath("{$buildDirectory}/{$css}"),
$partialManifest->first(),
$manifest
));
}
}
[$stylesheets, $scripts] = $tags->unique()->partition(fn ($tag) => str_starts_with($tag, '<link'));
$preloads = $preloads->unique()
->sortByDesc(fn ($args) => $this->isCssPath($args[1]))
->map(fn ($args) => $this->makePreloadTagForChunk(...$args));
return new HtmlString($preloads->join('').$stylesheets->join('').$scripts->join(''));
}
/**
* Make tag for the given chunk.
*
* @param string $src
* @param string $url
* @param array|null $chunk
* @param array|null $manifest
* @return string
*/
protected function makeTagForChunk($src, $url, $chunk, $manifest)
{
if (
$this->nonce === null
&& $this->integrityKey !== false
&& ! array_key_exists($this->integrityKey, $chunk ?? [])
&& $this->scriptTagAttributesResolvers === []
&& $this->styleTagAttributesResolvers === []) {
return $this->makeTag($url);
}
if ($this->isCssPath($url)) {
return $this->makeStylesheetTagWithAttributes(
$url,
$this->resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)
);
}
return $this->makeScriptTagWithAttributes(
$url,
$this->resolveScriptTagAttributes($src, $url, $chunk, $manifest)
);
}
/**
* Make a preload tag for the given chunk.
*
* @param string $src
* @param string $url
* @param array $chunk
* @param array $manifest
* @return string
*/
protected function makePreloadTagForChunk($src, $url, $chunk, $manifest)
{
$attributes = $this->resolvePreloadTagAttributes($src, $url, $chunk, $manifest);
if ($attributes === false) {
return '';
}
$this->preloadedAssets[$url] = $this->parseAttributes(
Collection::make($attributes)->forget('href')->all()
);
return '<link '.implode(' ', $this->parseAttributes($attributes)).' />';
}
/**
* Resolve the attributes for the chunks generated script tag.
*
* @param string $src
* @param string $url
* @param array|null $chunk
* @param array|null $manifest
* @return array
*/
protected function resolveScriptTagAttributes($src, $url, $chunk, $manifest)
{
$attributes = $this->integrityKey !== false
? ['integrity' => $chunk[$this->integrityKey] ?? false]
: [];
foreach ($this->scriptTagAttributesResolvers as $resolver) {
$attributes = array_merge($attributes, $resolver($src, $url, $chunk, $manifest));
}
return $attributes;
}
/**
* Resolve the attributes for the chunks generated stylesheet tag.
*
* @param string $src
* @param string $url
* @param array|null $chunk
* @param array|null $manifest
* @return array
*/
protected function resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)
{
$attributes = $this->integrityKey !== false
? ['integrity' => $chunk[$this->integrityKey] ?? false]
: [];
foreach ($this->styleTagAttributesResolvers as $resolver) {
$attributes = array_merge($attributes, $resolver($src, $url, $chunk, $manifest));
}
return $attributes;
}
/**
* Resolve the attributes for the chunks generated preload tag.
*
* @param string $src
* @param string $url
* @param array $chunk
* @param array $manifest
* @return array|false
*/
protected function resolvePreloadTagAttributes($src, $url, $chunk, $manifest)
{
$attributes = $this->isCssPath($url) ? [
'rel' => 'preload',
'as' => 'style',
'href' => $url,
'nonce' => $this->nonce ?? false,
'crossorigin' => $this->resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)['crossorigin'] ?? false,
] : [
'rel' => 'modulepreload',
'href' => $url,
'nonce' => $this->nonce ?? false,
'crossorigin' => $this->resolveScriptTagAttributes($src, $url, $chunk, $manifest)['crossorigin'] ?? false,
];
$attributes = $this->integrityKey !== false
? array_merge($attributes, ['integrity' => $chunk[$this->integrityKey] ?? false])
: $attributes;
foreach ($this->preloadTagAttributesResolvers as $resolver) {
if (false === ($resolvedAttributes = $resolver($src, $url, $chunk, $manifest))) {
return false;
}
$attributes = array_merge($attributes, $resolvedAttributes);
}
return $attributes;
}
/**
* Generate an appropriate tag for the given URL in HMR mode.
*
* @deprecated Will be removed in a future Laravel version.
*
* @param string $url
* @return string
*/
protected function makeTag($url)
{
if ($this->isCssPath($url)) {
return $this->makeStylesheetTag($url);
}
return $this->makeScriptTag($url);
}
/**
* Generate a script tag for the given URL.
*
* @deprecated Will be removed in a future Laravel version.
*
* @param string $url
* @return string
*/
protected function makeScriptTag($url)
{
return $this->makeScriptTagWithAttributes($url, []);
}
/**
* Generate a stylesheet tag for the given URL in HMR mode.
*
* @deprecated Will be removed in a future Laravel version.
*
* @param string $url
* @return string
*/
protected function makeStylesheetTag($url)
{
return $this->makeStylesheetTagWithAttributes($url, []);
}
/**
* Generate a script tag with attributes for the given URL.
*
* @param string $url
* @param array $attributes
* @return string
*/
protected function makeScriptTagWithAttributes($url, $attributes)
{
$attributes = $this->parseAttributes(array_merge([
'type' => 'module',
'src' => $url,
'nonce' => $this->nonce ?? false,
], $attributes));
return '<script '.implode(' ', $attributes).'></script>';
}
/**
* Generate a link tag with attributes for the given URL.
*
* @param string $url
* @param array $attributes
* @return string
*/
protected function makeStylesheetTagWithAttributes($url, $attributes)
{
$attributes = $this->parseAttributes(array_merge([
'rel' => 'stylesheet',
'href' => $url,
'nonce' => $this->nonce ?? false,
], $attributes));
return '<link '.implode(' ', $attributes).' />';
}
/**
* Determine whether the given path is a CSS file.
*
* @param string $path
* @return bool
*/
protected function isCssPath($path)
{
return preg_match('/\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/', $path) === 1;
}
/**
* Parse the attributes into key="value" strings.
*
* @param array $attributes
* @return array
*/
protected function parseAttributes($attributes)
{
return Collection::make($attributes)
->reject(fn ($value, $key) => in_array($value, [false, null], true))
->flatMap(fn ($value, $key) => $value === true ? [$key] : [$key => $value])
->map(fn ($value, $key) => is_int($key) ? $value : $key.'="'.$value.'"')
->values()
->all();
}
/**
* Generate React refresh runtime script.
*
* @return \Illuminate\Support\HtmlString|void
*/
public function reactRefresh()
{
if (! $this->isRunningHot()) {
return;
}
$attributes = $this->parseAttributes([
'nonce' => $this->cspNonce(),
]);
return new HtmlString(
sprintf(
<<<'HTML'
<script type="module" %s>
import RefreshRuntime from '%s'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
HTML,
implode(' ', $attributes),
$this->hotAsset('@react-refresh')
)
);
}
/**
* Get the path to a given asset when running in HMR mode.
*
* @return string
*/
protected function hotAsset($asset)
{
return rtrim(file_get_contents($this->hotFile())).'/'.$asset;
}
/**
* Get the URL for an asset.
*
* @param string $asset
* @param string|null $buildDirectory
* @return string
*/
public function asset($asset, $buildDirectory = null)
{
$buildDirectory ??= $this->buildDirectory;
if ($this->isRunningHot()) {
return $this->hotAsset($asset);
}
$chunk = $this->chunk($this->manifest($buildDirectory), $asset);
return $this->assetPath($buildDirectory.'/'.$chunk['file']);
}
/**
* Get the content of a given asset.
*
* @param string $asset
* @param string|null $buildDirectory
* @return string
*
* @throws \Exception
*/
public function content($asset, $buildDirectory = null)
{
$buildDirectory ??= $this->buildDirectory;
$chunk = $this->chunk($this->manifest($buildDirectory), $asset);
$path = public_path($buildDirectory.'/'.$chunk['file']);
if (! is_file($path) || ! file_exists($path)) {
throw new Exception("Unable to locate file from Vite manifest: {$path}.");
}
return file_get_contents($path);
}
/**
* Generate an asset path for the application.
*
* @param string $path
* @param bool|null $secure
* @return string
*/
protected function assetPath($path, $secure = null)
{
if($this->assetPathResolver !== null){
return $this->assetPathResolver($path, $secure);
}
return asset($path, $secure);
}
/**
* Get the the manifest file for the given build directory.
*
* @param string $buildDirectory
* @return array
*
* @throws \Illuminate\Foundation\ViteManifestNotFoundException
*/
protected function manifest($buildDirectory)
{
$path = $this->manifestPath($buildDirectory);
if (! isset(static::$manifests[$path])) {
if (! is_file($path)) {
throw new Exception("Vite manifest not found at: $path");
}
static::$manifests[$path] = json_decode(file_get_contents($path), true);
}
return static::$manifests[$path];
}
/**
* Get the path to the manifest file for the given build directory.
*
* @param string $buildDirectory
* @return string
*/
protected function manifestPath($buildDirectory)
{
return public_path($buildDirectory.'/'.$this->manifestFilename);
}
/**
* Get a unique hash representing the current manifest, or null if there is no manifest.
*
* @param string|null $buildDirectory
* @return string|null
*/
public function manifestHash($buildDirectory = null)
{
$buildDirectory ??= $this->buildDirectory;
if ($this->isRunningHot()) {
return null;
}
if (! is_file($path = $this->manifestPath($buildDirectory))) {
return null;
}
return md5_file($path) ?: null;
}
/**
* Get the chunk for the given entry point / asset.
*
* @param array $manifest
* @param string $file
* @return array
*
* @throws \Exception
*/
protected function chunk($manifest, $file)
{
if (! isset($manifest[$file])) {
throw new Exception("Unable to locate file in Vite manifest: {$file}.");
}
return $manifest[$file];
}
/**
* Determine if the HMR server is running.
*
* @return bool
*/
public function isRunningHot()
{
return is_file($this->hotFile());
}
/**
* Get the Vite tag content as a string of HTML.
*
* @return string
*/
public function toHtml()
{
return $this->__invoke($this->entryPoints)->toHtml();
}
}
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<div>{{ count }}</div>
<button @click="count++" clas>Increment</button>
<div class="bg-dots-darker">Hello World</div>
</template>
<style>
.bg-dots-darker {
background-image: url("data:image/svg+xml,%3Csvg width='30' height='30' viewBox='0 0 30 30' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.22676 0C1.91374 0 2.45351 0.539773 2.45351 1.22676C2.45351 1.91374 1.91374 2.45351 1.22676 2.45351C0.539773 2.45351 0 1.91374 0 1.22676C0 0.539773 0.539773 0 1.22676 0Z' fill='rgba(0,0,0,0.07)'/%3E%3C/svg%3E");
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment