Filament plugin for working with tree data: treeselect input and treeview page. Based on Trilist package.
Do you like Filament Trilist? Please support me via Boosty.
- Treeselect input and treeview page
- Tree items can have multiple parents
- Works with relationship or custom hierarchical data
You can install the package via composer:
composer require beholdr/filament-trilist
Optionally, you can publish the views using
php artisan vendor:publish --tag="filament-trilist-views"
You can use hierarchical data from any source when it follows format:
[
['id' => 'ID', 'label' => 'Item label', 'children' => [
['id' => 'ID', 'label' => 'Item label', 'children' => [...]],
...
]
]
For example, you can use special library like staudenmeir/laravel-adjacency-list to get tree data:
Category::tree()->get()->toTree()
Or use custom relationship schema and methods, even with ManyToMany
(multiple parents) relationship.
Example for self-referencing entity
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('professions', function (Blueprint $table) {
$table->id();
$table->string('label');
});
Schema::create('profession_profession', function (Blueprint $table) {
$table->primary(['parent_id', 'child_id']);
$table->foreignId('parent_id')->constrained('professions')->cascadeOnDelete();
$table->foreignId('child_id')->constrained('professions')->cascadeOnDelete();
});
}
public function down(): void
{
Schema::dropIfExists('professions');
Schema::dropIfExists('profession_profession');
}
};
namespace App\Models;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Profession extends Model
{
protected $with = ['children'];
public function parents()
{
return $this->belongsToMany(Profession::class, 'profession_profession', 'child_id', 'parent_id');
}
public function children()
{
return $this->belongsToMany(Profession::class, 'profession_profession', 'parent_id', 'child_id');
}
public function scopeRoot(Builder $builder)
{
$builder->doesntHave('parents');
}
}
With given model you can generate tree data like this:
Profession::root()->get();
Import TrilistSelect
class and use it on your Filament form:
use Beholdr\FilamentTrilist\Components\TrilistSelect
// with custom tree data
TrilistSelect::make('category_id')
->options($treeData),
// or with relationship
TrilistSelect::make('categories')
->relationship('categories')
->options($treeData)
->multiple(),
Full options list:
TrilistSelect::make(string $fieldName)
->label(string $fieldLabel)
->placeholder(string | Closure $placeholder)
->disabled(bool | Closure $condition)
// array of tree items
->options(array | Closure $options),
// first argument defines name of the relationship, second can be used to modify relationship query
->relationship(string | Closure $relationshipName, ?Closure $modifyQueryUsing = null)
// array of ids (or single id) of disabled items
->disabledOptions(string | int | array | Closure $value)
// multiple selection mode, default: false
->multiple(bool | Closure $condition)
// animate expand/collapse, default: true
->animated(bool | Closure $condition)
// expand initial selected options, default: true
->expandSelected(bool | Closure $condition)
// in independent mode children auto selected when parent is selected, default: false
->independent(bool | Closure $condition)
// in leafs mode, the selected value is not grouped as the parent when all child elements are selected, default: false
->leafs(bool | Closure $condition)
// tree item id field name, default: 'id'
->fieldId(string | Closure $value)
// tree item label field name, default: 'label'
->fieldLabel(string | Closure $value)
// tree item children field name, default: 'children'
->fieldChildren(string | Closure $value)
// hook for generating custom labels, default: '(item) => item.label'
->labelHook(string | Closure $value)
// enable filtering of items, default: false
->searchable(bool | Closure $condition)
// enable autofocus on filter field, default: false
->autofocus(bool | Closure $condition)
// search input placeholder
->searchPrompt(string | Htmlable | Closure $message)
// select button label
->selectButton(string | Htmlable | Closure $message)
// cancel button label
->cancelButton(string | Htmlable | Closure $message)
If you want to customize labels you can use labelHook
method. It should return a string that will be processed as JS (pay attention to escaping quotes and special characters):
TrilistSelect::make('parent_id')
->labelHook(fn () => <<<JS
(item) => `\${item.label} \${item.data?.description ? '<div style=\'font-size: 0.85em; opacity: 0.5\'>' + item.data.description + '</div>' : ''}`
JS)
You can use treeselect in custom filter:
use App\Models\Category;
use Filament\Tables\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
Filter::make('category')
->form([
TrilistSelect::make('category_id')
->multiple()
->independent()
->options(Category::tree()->get()->toTree())
])
->query(function (Builder $query, array $data) {
$query->when(
$data['category_id'],
function (Builder $query, $values) {
$ids = Category::whereIn('id', $values)
->get()
->map(fn (Category $category) => $category
->descendantsAndSelf()
->pluck('id')
->toArray()
)->flatten();
$query->whereIn('category_id', $ids);
}
);
})
->indicateUsing(function (array $data) {
if (! $data['category_id']) return null;
return Category::whereIn('id', $data['category_id'])->pluck('name')->toArray();
}),
Create custom page class inside Pages
directory of your resource directory. Note that page class extends Beholdr\FilamentTrilist\Components\TrilistPage
:
namespace App\Filament\Resources\PostResource\Pages;
use App\Filament\Resources\PostResource;
use App\Models\Post;
use Beholdr\FilamentTrilist\Components\TrilistPage;
class TreePosts extends TrilistPage
{
protected static string $resource = PostResource::class;
// optional page and tab title
protected static ?string $title = 'Posts Tree';
// return array of tree items (see below about tree data)
public function getTreeOptions(): array
{
return Post::root()->get()->toArray();
}
}
Register created page in the static getPages()
method of your resource:
public static function getPages(): array
{
return [
// ...
'tree' => Pages\TreePosts::route('/tree'),
];
}
Add link for a newly created page to your panel navigation:
use App\Filament\Resources\PostResource\Pages\TreePosts;
use Beholdr\FilamentTrilist\FilamentTrilistPlugin;
use Filament\Navigation\NavigationItem;
class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->navigationItems(TreePosts::getNavigationItems())
}
}
You can set some tree options by overriding static methods in the custom page class:
class TreeCategories extends TrilistPage
{
public static function getFieldLabel(): string
{
return 'name';
}
}
getFieldId()
: tree item id field namegetFieldLabel()
: tree item label field namegetFieldChildren()
: tree item children field nameisAnimated()
: animate expand/collapse, default: trueisSearchable()
: enable filtering of items, default: falsegetSearchPrompt()
: search input placeholder
If you want to customize labels of the tree items, you can override getLabelHook()
method of the TrilistPage
.
Example
Say, model for your tree items has a description
field that you want to output below the item name. All additional properties of your model are under item.data
property, so description will be at item.data.description
:
public function getLabelHook(): string
{
if (! $editRoute = $this->getEditRoute()) {
return 'undefined';
}
$template = route($editRoute, ['record' => '#ID#'], false);
return <<<JS
(item) => `<a href='\${'{$template}'.replace('#ID#', item.id)}'>\${item.label}</a> \${item.data?.description ? '<div style=\'font-size: 0.85em; opacity: 0.5\'>' + item.data.description + '</div>' : ''}`
JS;
}
The MIT License (MIT). Please see License File for more information.