Tricks

Admin panel Turbolinks

Jun 13, 2022
Tina Hammar
Integration, Admin panel

You will have to override the base.blade.php file if you want to add Turbolinks to the Filament Admin panel.

Steps

  1. Install hotwire/turbo
  2. Create filament-turbo.js
  3. Add it to webpack.mix.js
  4. Publish and override Filament base.blade.php
npm install --save-dev @hotwired/turbo

(The paths are based on my application, you don't have to keep this file structure ;)

resources/filament/filament-turbo.js

import '../js/libs/turbo';

resources/js/libs/turbo.js

import * as Turbo from '@hotwired/turbo';
 
//start Livewire turbolinks, source https://github.com/livewire/turbolinks/blob/master/src/index.js v0.1.4
//removes the need for a cdn link in app.blade.php
if (typeof window.Livewire === 'undefined') {
throw 'Livewire Turbolinks Plugin: window.Livewire is undefined. Make sure @livewireScripts is placed above this script include'
}
 
let firstTime = true;
 
function wireTurboAfterFirstVisit () {
// We only want this handler to run AFTER the first load.
if (firstTime) {
firstTime = false
 
return
}
 
window.Livewire.restart()
 
window.Alpine && window.Alpine.flushAndStopDeferringMutations && window.Alpine.flushAndStopDeferringMutations()
}
 
function wireTurboBeforeCache() {
document.querySelectorAll('[wire\\:id]').forEach(function(el) {
const component = el.__livewire;
const dataObject = {
fingerprint: component.fingerprint,
serverMemo: component.serverMemo,
effects: component.effects,
};
el.setAttribute('wire:initial-data', JSON.stringify(dataObject));
});
 
window.Alpine && window.Alpine.deferMutations && window.Alpine.deferMutations()
}
 
document.addEventListener("turbo:load", wireTurboAfterFirstVisit)
document.addEventListener("turbo:before-cache", wireTurboBeforeCache);
 
document.addEventListener("turbolinks:load", wireTurboAfterFirstVisit)
document.addEventListener("turbolinks:before-cache", wireTurboBeforeCache);
 
Livewire.hook('beforePushState', (state) => {
if (! state.turbolinks) state.turbolinks = {}
})
 
Livewire.hook('beforeReplaceState', (state) => {
if (! state.turbolinks) state.turbolinks = {}
})
//end Livewire turbolinks
 
//start turbo-laravel, source https://github.com/tonysm/turbo-laravel/blob/main/stubs/resources/js/libs/alpine.js v1.1.0
function initAlpineTurboPermanentFix() {
document.addEventListener('turbo:before-render', () => {
let permanents = document.querySelectorAll('[data-turbo-permanent]');
let undos = Array.from(permanents).map(el => {
el._x_ignore = true;
return () => {
delete el._x_ignore;
};
});
 
document.addEventListener('turbo:render', function handler() {
while(undos.length) undos.shift()();
document.removeEventListener('turbo:render', handler);
});
});
}
 
if (window.Alpine !== undefined) {
initAlpineTurboPermanentFix();
}
//end turbo-laravel
 
export default Turbo;

webpack.mix.js

mix.js('resources/filament/filament-turbo.js', 'public/js').extract(['@hotwired/turbo'], 'js/vendor-turbo.js');

Publish Filament views. Keep only resources/views/vendor/filament/components/layouts/base.blade.php, it is not recommended to keep files that you do not intend to override.

php artisan vendor:publish --tag=filament-views

In base.blade.php, you need to move the Filament scripts into the head tag.

@props([
'title' => null,
])
 
<!DOCTYPE html>
<html
lang="{{ str_replace('_', '-', app()->getLocale()) }}"
dir="{{ __('filament::layout.direction') ?? 'ltr' }}"
class="antialiased bg-gray-100 filament js-focus-visible"
>
<head>
{{ \Filament\Facades\Filament::renderHook('head.start') }}
 
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
 
@foreach (\Filament\Facades\Filament::getMeta() as $tag)
{{ $tag }}
@endforeach
 
@if ($favicon = config('filament.favicon'))
<link rel="icon" href="{{ $favicon }}">
@endif
 
<title>{{ $title ? "{$title} - " : null }} {{ config('filament.brand') }}</title>
 
{{ \Filament\Facades\Filament::renderHook('styles.start') }}
 
<style>
[x-cloak=""], [x-cloak="x-cloak"], [x-cloak="1"] { display: none !important; }
@media (max-width: 1023px) { [x-cloak="-lg"] { display: none !important; } }
@media (min-width: 1024px) { [x-cloak="lg"] { display: none !important; } }
:root { --sidebar-width: {{ config('filament.layout.sidebar.width') ?? '20rem' }}; }
</style>
 
@livewireStyles
 
@if (filled($fontsUrl = config('filament.google_fonts')))
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="{{ $fontsUrl }}" rel="stylesheet" />
@endif
 
{{ \Filament\Facades\Filament::getThemeLink() }}
 
@foreach (\Filament\Facades\Filament::getStyles() as $name => $path)
@if (\Illuminate\Support\Str::of($path)->startsWith(['http://', 'https://']))
<link rel="stylesheet" href="{{ $path }}" />
@elseif (\Illuminate\Support\Str::of($path)->startsWith('<'))
{!! $path !!}
@else
<link rel="stylesheet" href="{{ route('filament.asset', [
'file' => "{$name}.css",
]) }}" />
@endif
@endforeach
 
{{ \Filament\Facades\Filament::renderHook('styles.end') }}
 
@if (config('filament.dark_mode'))
<script>
const theme = localStorage.getItem('theme')
 
if ((theme === 'dark') || (! theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
}
</script>
@endif
 
{{-- Filament head.end position--}}
{{-- Filament body script start --}}
 
{{ \Filament\Facades\Filament::renderHook('scripts.start') }}
 
@livewireScripts
{{-- bokaMarknad specific --}}
<script src="{{ mix('js/manifest.js') }}" defer></script>
<script src="{{ mix('js/vendor-turbo.js') }}" defer></script>
<script src="{{ mix('js/filament-turbo.js') }}" defer></script>
{{-- end bokaMarknad specific --}}
 
{{-- Filament body continued --}}
<script>
window.filamentData = @js(\Filament\Facades\Filament::getScriptData());
</script>
 
@foreach (\Filament\Facades\Filament::getBeforeCoreScripts() as $name => $path)
@if (\Illuminate\Support\Str::of($path)->startsWith(['http://', 'https://']))
<script defer src="{{ $path }}"></script>
@elseif (\Illuminate\Support\Str::of($path)->startsWith('<'))
{!! $path !!}
@else
<script defer src="{{ route('filament.asset', [
'file' => "{$name}.js",
]) }}"></script>
@endif
@endforeach
 
@stack('beforeCoreScripts')
 
<script defer src="{{ route('filament.asset', [
'id' => Filament\get_asset_id('app.js'),
'file' => 'app.js',
]) }}"></script>
 
@foreach (\Filament\Facades\Filament::getScripts() as $name => $path)
@if (\Illuminate\Support\Str::of($path)->startsWith(['http://', 'https://']))
<script defer src="{{ $path }}"></script>
@elseif (\Illuminate\Support\Str::of($path)->startsWith('<'))
{!! $path !!}
@else
<script defer src="{{ route('filament.asset', [
'file' => "{$name}.js",
]) }}"></script>
@endif
@endforeach
 
@stack('scripts')
 
{{ \Filament\Facades\Filament::renderHook('scripts.end') }}
 
{{ \Filament\Facades\Filament::renderHook('head.end') }}
</head>
 
<body @class([
'bg-gray-100 text-gray-900 filament-body',
'dark:text-gray-100 dark:bg-gray-900' => config('filament.dark_mode'),
])>
{{ \Filament\Facades\Filament::renderHook('body.start') }}
 
{{ $slot }}
 
{{ \Filament\Facades\Filament::renderHook('body.end') }}
</body>
</html>
avatar

Thank you ,I was waiting this one.I have installed this and work as charm. There is no other way to include js files without changing base.blade.php( I mean something like https://filamentphp.com/docs/2.x/admin/appearance#including-frontend-assets)

thank you Tina

avatar

No because scripts needs to be pushed to head, I tried keeping it as is, but then things didn't work as expected. Also, once a script is pushed to the head it is only loaded the first time, Turbolinks keeps it there, speeding up all other page loads where the script is needed. If you keep the scripts in body, they will be reloaded on every page visit, which contradicts what we try to achieve w Turbolinks ...

👍 1
avatar

Hello, thank you for this great trick. Is it possible to update using Vite?

avatar

I will create another article for Vite :)

👍 4
avatar

Waiting for this.

avatar

Not having any luck. A little confusing as example appears to be either an older base or has other scripts in it that arent part of filament and wont work as well. Would love to see a stock filament repo with it simply already integrated in order to at least test or less vagueness. I really want get this going though.

avatar

For those who use Vite: In the ServiceProvider's Boot method:

Filament::registerScripts( [app(Vite::class)('resources/filament/filament-turbo.js')] );

In vite.config.js:

{...} export default defineConfig({ plugins: [ laravel({ input: [ {...} 'resources/filament/filament-turbo.js', {...} ], refresh: true, }), ], });

Note: It is not necessary to publish the base.blade.php view.

avatar

Are you using the turbolinks or the new version for this? And could you send me your source code if you don't mind?....

avatar

Nevermind I got it working, appreciate it