Select Tree plugin screenshot
Dark mode ready
Multilingual support
Supports v5.x

Select Tree

The multi-level select field lets you pick one or multiple options from a list that's neatly organized into different levels.

Tags: Form Field
Supported versions:
5.x 4.x 3.x
CodeWithDennis avatar Author: CodeWithDennis

Documentation

Latest Version on Packagist Total Downloads

If you're using Filament 3.x, check out the compatible version of this package here.

This package adds a dynamic select tree field to your Laravel / Filament application, allowing you to create interactive hierarchical selection dropdowns based on relationships. It's handy for building selection dropdowns with various customization options.

thumbnail

#Installation

You can install the package via composer:

composer require codewithdennis/filament-select-tree:4.x
php artisan filament:assets

#Relationships

Use the tree for a BelongsToMany relationship

SelectTree::make('categories')
    ->relationship('categories', 'name', 'parent_id')

Use the tree for a BelongsTo relationship

SelectTree::make('category_id')
    ->relationship('category', 'name', 'parent_id')

#Usage without relationships

Use the tree without relationship

SelectTree::make('category_id')
    ->query(fn() => Category::query(), 'name', 'parent_id')

#Custom Query

Customize the parent query

SelectTree::make('categories')
    ->relationship(relationship: 'categories', titleAttribute: 'name', parentAttribute: 'parent_id', modifyQueryUsing: fn($query) => $query));

Customize the child query

SelectTree::make('categories')
    ->relationship(relationship: 'categories', titleAttribute: 'name', parentAttribute: 'parent_id', modifyChildQueryUsing: fn($query) => $query));

#Methods

Set a custom placeholder when no items are selected

->placeholder(__('Please select a category'))

Enable the selection of groups

->enableBranchNode()

Customize the label when there are zero search results

->emptyLabel(__('Oops, no results have been found!'))

Display the count of children alongside the group's name

->withCount()

Keep the dropdown open at all times

->alwaysOpen()

Add the list as a static DOM element so that it doesn't overlap content

->staticList()

Set nodes as dependent

->independent(false)

Expand the tree with selected values (only works if field is dependent)

->expandSelected(false)

Set the parent's null value to -1, allowing you to use -1 as a sentinel value (default = null)

->parentNullValue(-1)

All groups will be opened to this level

->defaultOpenLevel(2)

Specify the list's force direction. Options include: auto (default), top, and bottom.

->direction('top')

Display individual leaf nodes instead of the main group when all leaf nodes are selected

->grouped(false)

Return parent node value if all of its children are selected, instead of individual leaf nodes.

->isGroupedValue()

Hide tags and display a count of selected items instead

->showTags(false)

Customize the text shown if showTags is set to false, e.g. "{count} {tagsCountText}"

->tagsCountText('elements selected')

Hide the clearable icon

->clearable(false)

Activate the search functionality

->searchable();

Disable specific options in the tree

->disabledOptions([2, 3, 4])

Hide specific options in the tree

->hiddenOptions([2, 3, 4])

Allow soft deleted items to be displayed

->withTrashed()

Specify a different key for your model. For example: you have id, code and parent_code. Your model uses id as key, but the parent-child relation is established between code and parent_code

->withKey('code')

Store fetched models for additional functionality

->storeResults()

Now you can access the results in disabledOptions or hiddenOptions

->disabledOptions(function ($state, SelectTree $component) {
    $results = $component->getResults();
})

By default, the type of selection in the tree (single or multiple) is determined by the relationship type: BelongsTo for single selection and BelongsToMany for multiple selection. If you want to explicitly set the selection type, use:

->multiple(false)

you can change the tree key with the following method.

->treeKey('my-cool-tree')

If you need to prepend an item to the tree menu, use the prepend method. This method accepts an array or a closure. It is useful when the tree-select is used as a filter (see example below).

use Filament\Tables\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
use CodeWithDennis\FilamentSelectTree\SelectTree;
->filters([
    Filter::make('tree')
        ->form([
            SelectTree::make('category')
                ->relationship('categories', 'name', 'parent_id')
                ->enableBranchNode()
                ->multiple(false)
                ->prepend([
                    'name' => 'Uncategorized Records',
                    'value' => -1,
                    'parent' => null, // optional
                    'disabled' => false, // optional
                    'hidden' => false, // optional
                    'children' => [], // optional
                ])
        ])
        ->query(function (Builder $query, array $data) {
            return $query->when($data['categories'], function (Builder $query, $categories) {
                if (collect($categories)->contains('-1')) {
                    $query->whereDoesntHave('categories');
                }
                return $query->orWhereHas('categories',
                    fn(Builder $query) => $query->whereIn('id', $categories));
            });
        })
])

If you need to append an item to the tree menu, use the append method. This method also accepts an array or a closure.

->schema([
    SelectTree::make('category')
        ->relationship('categories', 'name', 'parent_id')
        ->enableBranchNode()
        ->multiple(false)
        ->append([
            'name' => 'Uncategorized Records',
            'value' => -1,
            'parent' => null, // optional
            'disabled' => false, // optional
            'hidden' => false, // optional
            'children' => [], // optional
        ])
    ])

If you need full control over the tree structure, you can use getTreeUsing to provide a custom tree array, or a closure that resolves to an array.

Using an array:

SelectTree::make('categories')
    ->getTreeUsing([
        [
            'name' => 'Parent Category',
            'value' => '1',
            'children' => [
                [
                    'name' => 'Child Category',
                    'value' => '2',
                    'children' => [],
                ],
            ],
        ],
        [
            'name' => 'Another Category',
            'value' => '3',
            'children' => [],
        ],
    ])

Using a closure for dynamic data:

SelectTree::make('categories')
    ->getTreeUsing(function () {
        return Category::query()
            ->get()
            ->map(fn ($category) => [
                'name' => $category->name,
                'value' => $category->id,
                'children' => $category->children->map(fn ($child) => [
                    'name' => $child->name,
                    'value' => $child->id,
                    'children' => [],
                ])->toArray(),
            ])
            ->toArray();
    })

The tree structure should follow this format:

[
    [
        'name' => 'Display Name',
        'value' => 'option_value',
        'disabled' => false, // optional
        'hidden' => false, // optional
        'children' => [], // optional
    ],
]

#Filters

Use the tree in your table filters. Here's an example to show you how.

use Filament\Tables\Filters\Filter;
use Illuminate\Database\Eloquent\Builder;
use CodeWithDennis\FilamentSelectTree\SelectTree;
->filters([
    Filter::make('tree')
        ->form([
            SelectTree::make('categories')
                ->relationship('categories', 'name', 'parent_id')
                ->independent(false)
                ->enableBranchNode(),
        ])
        ->query(function (Builder $query, array $data) {
            return $query->when($data['categories'], function ($query, $categories) {
                return $query->whereHas('categories', fn($query) => $query->whereIn('id', $categories));
            });
        })
        ->indicateUsing(function (array $data): ?string {
            if (! $data['categories']) {
                return null;
            }

            return __('Categories') . ': ' . implode(', ', Category::whereIn('id', $data['categories'])->get()->pluck('name')->toArray());
        })
])

#Screenshots

example-1 example-2 example-3

#Contributing

Please see CONTRIBUTING for details.

#Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

#Credits

#License

The MIT License (MIT). Please see License File for more information.