قبل كل شيء، أود شكر صديقي معاذ السوادي الذي علّمني هذه المعلومة بعد أن حاول فيها مرارًا حتى ضُبطت بهذا الشكل..
في البداية، من أجمل أدوات تسريع عمل لوحة التحكم في لارافل هي مكتبة filament php، فهي تشبه Nova ولكنها مبنية على TALL stack ومجانية، وإذا لم تكن قد جربتها فأدعوك لتجربتها بشدة..
ولكن هناك ميزة لتحسينها أكثر، وجعل التنقل بين عناصر التنقل navigation items دون إعادة تحميل كاملة للصفحة، عن طريق مكتبة hotwire، بحيث تبدو الانتقالات بين عناصر التنقل سلسلة وأشبه ما تكون بتطبيقات الصفحة الواحدة single page applications.
والخطوات كالتالي:
1- أضف @hotwired/turbo
لاعتماديات package.json عن طريق الأمر
npm i -D @hotwired/turbo
أو yarn add -D @hotwired/turbo
إذا كنت تستخدم yarn.
2- إضافة الملف بهذا المسار resources/filament/filament-turbo.js
ونكتب بداخله هذا السطر
import '../js/libs/turbo'
3- إضافة الملف بهذا المسار 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
4- وفي ملف vite.config.js
نضيف resources/filament/filament-turbo.js
فتصير كالتالي:
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
'resources/filament/filament-turbo.js', // add this line
],
refresh: true,
}),
],
})
5- نضيف هذا الملف الذي سيجعل filament تقرأ منه وليس من الملف الافتراضي بحيث تتجاوزه override، والملف في هذا المسار resources/views/vendor/filament/components/layouts
ومحتواه كالتالي
@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
<style>
.turbo-progress-bar {
height: 3px;
background: rgb(245 158 11/var(--tw-bg-opacity));
}
</style>
{{ \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 --}}
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/filament/filament-turbo.js'])
{{-- <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>
وبهذا صار التنقل أسلسل وأجمل وأشبه ما يكون بتطبيقات الصفحة الواحدة..
أرجو أن تكونوا استفدتم من هذا الدرس، وأي تصويب فهو مرحب به..