Skip to main content
Tutorial Free

Filament: Table Tabs - Dynamically from Database

November 07, 2023
4 min read

Filament Tables allow you to implement tabs easily. But did you know you can specify the tab names/values dynamically from the database, also controlling the order in which they appear? Let's take a look.

Customer Tabs

In this example, we have the following database table schema and Models.

database/migrations/XXXXXX_create_tiers_table.php

Schema::create('tiers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('order_column');
$table->timestamps();
});

database/migrations/XXXXXX_create_customers_table.php

use App\Models\Tier;
 
// ...
 
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Tier::class)->nullable()->constrained();
$table->string('full_name');
$table->timestamps();
});

app/Models/Tier.php

class Tier extends Model
{
// ...
 
protected $guarded = [];
 
public function customers(): HasMany
{
return $this->hasMany(Customer::class);
}
}

app/Models/Customer.php

class Customer extends Model
{
// ...
 
protected $guarded = [];
 
public function tier(): BelongsTo
{
return $this->belongsTo(Tier::class);
}
}

To add tabs, we must define the getTabs() method to your List<Resource>.php file. In this case, it is ListCustomers.

app/Filament/Resources/CustomerResource/Pages/ListCustomers.php

namespace App\Filament\Resources\CustomerResource\Pages;
 
use App\Filament\Resources\CustomerResource;
use App\Models\Tier;
use Filament\Actions;
use Filament\Resources\Components\Tab;
use Filament\Resources\Pages\ListRecords;
 
class ListCustomers extends ListRecords
{
protected static string $resource = CustomerResource::class;
 
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
 
 
public function getTabs(): array
{
$tabs = ['all' => Tab::make('All')->badge($this->getModel()::count())];
 
$tiers = Tier::orderBy('order_column', 'asc')
->withCount('customers')
->get();
 
foreach ($tiers as $tier) {
$name = $tier->name;
$slug = str($name)->slug()->toString();
 
$tabs[$slug] = Tab::make($name)
->badge($tier->customers_count)
->modifyQueryUsing(function ($query) use ($tier) {
return $query->where('tier_id', $tier->id);
});
}
 
return $tabs;
}
}

When returning the tabs array, the index of that array is a value of the activeTab URL parameter. So, we can generate a slug from the Tier name instead of just having a number.

//... /admin/customers?activeTab=platinum
$slug = str($name)->slug()->toString();

Tabs can have badges. The badge() method also accepts closures to display data depending on your custom logic. In our case, we show the Customer count in that Tier.

->badge($tier->customers_count)

Eloquent Query is modified with the ->modifyQueryUsing() method to filter records. Here, we extend the query to select Customers with the tier_id of the activeTab.

->modifyQueryUsing(function ($query) use ($tier) {
return $query->where('tier_id', $tier->id);
});

Tier tabs are sorted by the order_column column in ascending order.

$tiers = Tier::orderBy('order_column', 'asc')

You can change the order of tabs by modifying the order_column value on the EditTier page. Optionally, you can reorder rows directly on the ListTiers page by adding these methods to the TierResource@table method.

->defaultSort('order_column')
->reorderable('order_column')

Reorder Tiers

Reordered Tabs

Implemented Tier and Customer Resources are as follows.

app/Filament/Resources/TierResource.php

use Filament\Forms\Components\TextInput;
use Filament\Tables\Columns\TextColumn;
 
class TierResource extends Resource
{
// ...
 
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('name')
->required(),
TextInput::make('order_column')
->required()
->numeric(),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('order_column'),
TextColumn::make('name'),
TextColumn::make('customers_count')
->counts('customers')
->badge(),
])
->defaultSort('order_column')
->reorderable('order_column')
// ...
}
}

app/Filament/Resources/CustomerResource.php

use App\Models\Tier;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Tables\Columns\TextColumn;
 
class CustomerResource extends Resource
{
// ...
 
public static function form(Form $form): Form
{
return $form
->schema([
TextInput::make('full_name')
->required(),
Select::make('tier_id')
->label('Tier')
->options(Tier::pluck('name', 'id')),
]);
}
 
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('full_name'),
TextColumn::make('tier.name')
->badge(),
])
// ...
}
}

Congratulations! You've successfully implemented dynamic Tabs for your Filament Table.


If you want more Filament examples, you can find more real-life projects on our FilamentExamples.com.

Enjoyed This Tutorial?

Get access to all premium tutorials, video and text courses, and exclusive Laravel resources. Join our community of 10,000+ developers.

Comments & Discussion

KB
Kurt Bauman ✓ Link copied!

This is a great tutorial including Tabs. Is anyone aware of a solution where each tab can display only the needed table columns? My example would be to have a tab for income, another of expense, and a third for a complete list of both. I don't necessary need some of the colums listed in all three options. I apologize if the solution has already been addressed but I havent come accross it yet. Thank you and please keep up the good work!

M
Modestas ✓ Link copied!

Tabs are accessible as URL parameter, so you can add ->hidden() with a condition for all the tabs

KB
Kurt Bauman ✓ Link copied!

Thank you for point me in the right direction.

Z
Zoltes ✓ Link copied!

Hello! Thank you for the great tutorial! It works perfectly if I put the getTabs in the ListCustomers. But If I put the same tabs and the same table in the custom page they work but the tabs don't affect the query of the table. Can you please point me towards the right direction?

Z
Zoltes ✓ Link copied!

got it. I was lurking through the ListRecords and found ->modifyQueryUsing($this->modifyQueryWithActiveTab(...)) applied it to the $table and voila

RL
ryan li ✓ Link copied!

Hi ! Could you show me the demo code?

My approach

 
class ListMaterials
 
public function getTabs(): array
{
$restuls = [
null => Tab::make(__('All')),
];
 
$items = CustomField::queryAllTypeFor('material', 'ASC');
 
foreach ($items as $entry) {
$restuls[$entry->getSlug()] = Tab::make()
->query(
fn ($query) => $query->whereHas(
$entry->getSlug(),
fn ($query) => $query->where('typeable', $entry->getSlug())
)
)->extraAttributes([
'slug'=> $entry->getSlug(),
])
->label($entry->getName());
}
 
return $restuls;
}
 
public function table(Table $table): Table
{
$resource = static::getResource();
 
$resource::customFieldsSlugConfig($this->activeTab);
 
return $resource::table($table);
}
 
class MaterialResource extends Resource
{
....
protected static ?string $customFieldsSlug = null;
 
public static function table(Table $table): Table
{
return $table->columns([
 
BadgeableColumn::make('name'),
...static::previewTabForCustomExpand()
 
])
}
 
public static function customFieldsSlugConfig(?string $customFieldsSlug)
{
static::$customFieldsSlug = $customFieldsSlug;
}
 
public static function previewTabForCustomExpand(): array
{
if (! static::$customFieldsSlug) {
return [];
}
 
$custom = CustomField::queryTypeFor('material', static::$customFieldsSlug);
 
$items = $custom?->fieldItems->getColumnComponents($custom->getSlug());
 
return $items ?: [];
}
}

It works fine, but I need to click the button twice to get it to work. It seems to be cached. But I have found a good solution.

PH
Peter Hrobar ✓ Link copied!

Usually you have multiple filters on the table so the number in the badge should be updated accordingly. Although it seems trivial it is not.

You can use a helper function to remove the badge's where statement from the table query to show the correct numbers (reflecting the set filters of the table):

public function getTabs(): array
{
$tabs = [
'all' => Tab::make('All')
->badge(fn() => $this->filterTypesFromQuery(
$this->getFilteredTableQuery()->clone()
)->count()
)
];
....
$tabs[$slug] = Tab::make($name)
->badge(fn() =>
$this->filterTypesFromQuery(
$this->getFilteredTableQuery()->clone()
)
->where('type', $costType->value)
->count()
)
->modifyQueryUsing(function ($query) use ($costType) {
return $query->where('type', $costType->value);
});
...

the helper function look something like this ...

protected function filterTypesFromQuery($filteredTableQuery)
{
$wheres = array_filter(
$filteredTableQuery->getQuery()->wheres,
function($where, $key) use ($filteredTableQuery) {
if (isset($where['column'])) {
if ($where['column'] === 'type') {
unset($filteredTableQuery->getQuery()->bindings['where'][$key]);
return false;
}
return true;
}
return true;
}, ARRAY_FILTER_USE_BOTH);
 
$filteredTableQuery->getQuery()->wheres = $wheres;
 
return $filteredTableQuery;
}

We'd Love Your Feedback

Tell us what you like or what we can improve

Feel free to share anything you like or dislike about this page or the platform in general.