Installation:
composer require nben/filament-record-nav
Publish config (optional):
php artisan vendor:publish --tag=filament-record-nav-config
First Use Case:
Add actions to getHeaderActions() in your ViewRecord or EditRecord page:
use Nben\FilamentRecordNav\Actions\{NextRecordAction, PreviousRecordAction};
protected function getHeaderActions(): array {
return [
PreviousRecordAction::make(),
NextRecordAction::make(),
];
}
Where to Look First:
config/filament-record-nav.php for global ordering settingsActions/NextRecordAction.php and Actions/PreviousRecordAction.php for API referencegetPages() method for custom route namesDefault Integration:
// ViewRecord.php
protected function getHeaderActions(): array {
return [
PreviousRecordAction::make()
->label('Previous')
->icon('heroicon-o-chevron-left'),
NextRecordAction::make()
->label('Next')
->icon('heroicon-o-chevron-right'),
];
}
Page-Specific Navigation:
// EditRecord.php
protected function getHeaderActions(): array {
return [
PreviousRecordAction::make()
->navigateTo(NavigationPage::View),
NextRecordAction::make()
->navigateTo(NavigationPage::View),
];
}
Custom Route Integration:
// Resource.php
public static function getPages(): array {
return [
'index' => ListPosts::route('/'),
'view' => ViewPost::route('/{record}'),
'edit' => EditPost::route('/{record}/edit'),
'publish' => PublishPost::route('/{record}/publish'),
];
}
// ViewRecord.php
protected function getHeaderActions(): array {
return [
NextRecordAction::make()
->navigateTo(NavigationPage::custom('publish')),
];
}
Action Styling: Use Filament's action modifiers:
PreviousRecordAction::make()
->color('gray')
->size(Size::Small)
->tooltip('Previous record')
Conditional Visibility:
PreviousRecordAction::make()
->visible(fn ($livewire) => $livewire->record && $livewire->record->is_archived)
Key Bindings:
NextRecordAction::make()
->keyBindings(['mod+right'])
Resource-Wide Setup: Create a base page class:
// BaseRecordPage.php
abstract class BaseRecordPage extends ViewRecord {
protected function getHeaderActions(): array {
return [
PreviousRecordAction::make(),
NextRecordAction::make(),
];
}
}
Disabled Buttons:
order_column values (e.g., created_at with same timestamp)id column:
'order_column' => 'id',
Custom Route Mismatch:
InvalidArgumentExceptiongetPages() exactly:
// Correct:
NavigationPage::custom('verified-view')
// Resource must define:
'verified-view' => VerifiedViewPage::route('/{record}/verified')
Trait Not Working:
usedpublic function getPreviousRecord(): ?Model
Performance on Large Tables:
order_columngetPreviousRecord()/getNextRecord():
protected ?Model $cachedPreviousRecord;
public function getPreviousRecord(): ?Model {
if ($this->cachedPreviousRecord) return $this->cachedPreviousRecord;
$this->cachedPreviousRecord = $this->getRecord()
->newQuery()
->where('status', 'published')
->orderBy('published_at')
->first();
return $this->cachedPreviousRecord;
}
Log Queries:
public function getPreviousRecord(): ?Model {
\Log::debug('Finding previous record for ID: ' . $this->getRecord()->id);
return $this->getRecord()
->newQuery()
->where('created_at', '<', $this->getRecord()->created_at)
->first();
}
Verify Cache: Add debug output to actions:
PreviousRecordAction::make()
->url(fn ($livewire) => {
$record = $livewire->resolveRecordNavigation('previous');
\Log::debug('Resolved previous record:', ['id' => $record?->id]);
return $record ? static::getResource()::getUrl('view', ['record' => $record]) : null;
})
Check Livewire Context:
PreviousRecordAction::make()
->url(fn ($livewire) => {
\Log::debug('Livewire context:', [
'record_id' => $livewire->record?->id,
'page' => get_class($livewire),
]);
// ...
})
Dynamic Order Column:
// In your page class
public function getOrderColumn(): string {
return $this->getRecord()->is_draft ? 'draft_order' : 'published_at';
}
// Override the action's resolve method
public function getPreviousRecord(): ?Model {
$column = $this->getOrderColumn();
return $this->getRecord()
->newQuery()
->where($column, '<', $this->getRecord()->{$column})
->orderBy($column, 'desc')
->first();
}
Multi-Table Navigation:
public function getNextRecord(): ?Model {
return Post::whereHas('author', fn ($q) => $q->where('id', $this->getRecord()->author_id))
->where('published_at', '>', $this->getRecord()->published_at)
->orderBy('published_at')
->first();
}
Soft Deletes Handling:
public function getPreviousRecord(): ?Model {
return $this->getRecord()
->newQuery()
->withTrashed()
->where('deleted_at', '<', $this->getRecord()->deleted_at ?? now())
->orderBy('deleted_at', 'desc')
->first();
}
Custom URL Generation:
public function getRecordNavigationUrl(Model $record, $page): string {
return route('admin.posts.show', [
'post' => $record->slug,
'tab' => $page->value === 'edit' ? 'content' : 'metadata'
]);
}
Timestamp Columns:
created_at/updated_at carefully - ensure distinct values:
'order_column' => 'created_at',
'previous_direction' => 'asc', // Reverse for timestamps
'next_direction' => 'desc',
Case Sensitivity:
NavigationPage::custom() are case-sensitive:
// Correct:
NavigationPage::custom('verified-view')
// Incorrect (throws exception):
NavigationPage::custom('VerifiedView')
Default Values:
id as fallback, but ensure your model's primary key is properly defined:
class Post extends Model {
public $primaryKey = 'post_id';
}
Query Caching: Leverage Laravel's query caching for large datasets:
public function getPreviousRecord(): ?Model {
return Cache::remember(
"filament_nav_prev_{$this->getRecord()->id}",
now()->addMinutes(5),
fn() => $this->getRecord()
->newQuery()
->where('status', 'published')
->orderBy('published_at')
->first()
);
}
Eager Loading:
public function getPreviousRecord(): ?Model {
return $this->getRecord()
->newQuery()
->with(['author', 'category'])
->where('published_at', '<', $this->getRecord()->published_at)
->orderBy('published_at', 'desc')
->first();
}
Cursor-Based Pagination: For
How can I help you explore Laravel packages today?