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 :)

👍 5
avatar

Waiting for this.

avatar

Waiting also! :)

avatar

will it be available soon? thank you so much!

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

avatar

This just gives me an error:

Illuminate  \  Contracts  \  Container  \  BindingResolutionException Target class [App\Providers\Vite] does not exist.

avatar

@Argemiro Dias it is working but, did you noticed the js errors? "Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry': the name "hex-color-picker" has already been used with this registry"

avatar

I don't know why but when I put the scripts in the 'base.blade.php' file, my turbo.js is loaded correctly, but my sidebar totally breaks, I cannot use the toggle button and hide/show the sidebar...any idea?

Can you provide an example of 'base.blade.php' file with this configuration?

avatar

You can try this code resources/views/vendor/filament/components/layouts/base.blade.php

 
```php
@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' }};
--collapsed-sidebar-width: {{ config('filament.layout.sidebar.collapsed_width') ?? '5.4rem' }};
}
</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
 
@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::getThemeLink() }}
 
{{ \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 = @json(\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>
 
@if (config('filament.broadcasting.echo'))
<script defer src="{{ route('filament.asset', [
'id' => Filament\get_asset_id('echo.js'),
'file' => 'echo.js',
]) }}"></script>
 
<script>
window.addEventListener('DOMContentLoaded', () => {
window.Echo = new window.EchoFactory(@js(config('filament.broadcasting.echo')))
 
window.dispatchEvent(new CustomEvent('EchoLoaded'))
})
</script>
@endif
 
@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
 
 
{{ \Filament\Facades\Filament::renderHook('head.end') }}
</head>
 
<body @class([
'filament-body bg-gray-100 text-gray-900',
'dark:text-gray-100 dark:bg-gray-900' => config('filament.dark_mode'),
])>
{{ \Filament\Facades\Filament::renderHook('body.start') }}
 
{{ $slot }}
 
 
@stack('scripts')
 
{{ \Filament\Facades\Filament::renderHook('scripts.end') }}
 
{{ \Filament\Facades\Filament::renderHook('body.end') }}
</body>
</html>
 
avatar

Great idea, but I would love to see an up to date version of the article, in order to get Filament to use Turbo and behave like an SPA without full page reloads. Laravel uses vite rather than webpack, and I can't get this working without errors. It would be amazing to get a working, up to date version of this trick! Many thanks! Ben.

👍 1
avatar

Getting console error

module.esm.js:12243 Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry': the name "hex-color-picker" has already been used with this registry at 97 (http://localhost:8000/filament/assets/app.js?id=ec835eb3e622d56b7f9a6fdd6e61611e:27:413325) at i (http://localhost:8000/filament/assets/app.js?id=ec835eb3e622d56b7f9a6fdd6e61611e:81:141622) at http://localhost:8000/filament/assets/app.js?id=ec835eb3e622d56b7f9a6fdd6e61611e:81:142595 at i.O (http://localhost:8000/filament/assets/app.js?id=ec835eb3e622d56b7f9a6fdd6e61611e:81:141888) at http://localhost:8000/filament/assets/app.js?id=ec835eb3e622d56b7f9a6fdd6e61611e:81:142644 at http://localhost:8000/filament/assets/app.js?id=ec835eb3e622d56b7f9a6fdd6e61611e:81:142650

Filament version => v2.17.44 Laravel version => v10.3.2

👍 1
avatar

I've figured out how to solve the "Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry': the name "hex-color-picker" has already been used with this registry" error.

You need to publish the views like said above and add

data-turbo-eval="false"

to every script tag like so for example:

<script data-turbo-eval="false"
defer
src="{{
route('filament.asset', [
'id' => Filament\get_asset_id('app.js'),
'file' => 'app.js',
])
}}"
></script>