PHP code example of solution-forest / filament-nestable-tree

1. Go to this page and download the library: Download solution-forest/filament-nestable-tree library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

solution-forest / filament-nestable-tree example snippets


use SolutionForest\FilamentNestableTree\Filament\Pages\TreePage;
use SolutionForest\FilamentNestableTree\Tree;

class CategoryTreePage extends TreePage
{
    protected static ?string $navigationLabel = 'Categories';

    public function tree(Tree $tree): Tree
    {
        return $tree->model(Category::class)->labelField('title');
    }
}

use SolutionForest\FilamentNestableTree\Filament\Resources\Pages\TreePage;
use SolutionForest\FilamentNestableTree\Tree;

class ManageCategoryTree extends TreePage
{
    public static string $resource = CategoryResource::class;

    public function tree(Tree $tree): Tree
    {
        return parent::tree($tree)   // 

public static function getPages(): array
{
    return [
        'index' => ManageCategoryTree::route('/'),
    ];
}

use SolutionForest\FilamentNestableTree\Filament\Widgets\Tree as TreeWidget;
use SolutionForest\FilamentNestableTree\Tree;

class CategoryTreeWidget extends TreeWidget
{
    public function tree(Tree $tree): Tree
    {
        return $tree->model(Category::class)->labelField('title');
    }
}

use Livewire\Component;
use SolutionForest\FilamentNestableTree\Concerns\InteractsWithTree;
use SolutionForest\FilamentNestableTree\Tree;

class MyCustomPage extends Component
{
    use InteractsWithTree;

    public function tree(Tree $tree): Tree
    {
        return $tree->model(Category::class)->labelField('title');
    }
}

@    'wireNodesProperty'  => 'treeNodes',
    'treeKeyName'        => null,
    'treeConfig'         => $this->getCachedTree(),
    'isSearchable'       => $this->getCachedTree()->isSearchable(),
    'allowDragDrop'      => $this->getCachedTree()->isDraggable(),
    'allowCrossCategory' => $this->getCachedTree()->isCrossCategoryAllowed(),
    'toolbarActions'     => $this->getCachedTree()->getToolbarActions(),
    'lazy'               => $this->getCachedTree()->isLazy(),
    'hasNodeActions'     => ! empty($this->getCachedTree()->getNodeActions()),
])

use Kalnoy\Nestedset\NodeTrait;

class Category extends Model
{
    use NodeTrait;
}

public function tree(Tree $tree): Tree
{
    return $tree->model(Category::class)->labelField('title');
}

public function tree(Tree $tree): Tree
{
    return $tree
        ->model(Category::class)
        ->saveOrderUsing(function (array $nodes): void {
            // $nodes is the full nested array from Alpine
            foreach ($nodes as $index => $node) {
                Category::where('id', $node['id'])->update(['sort_order' => $index]);
            }
        });
}

use Filament\Actions\Action;

public function tree(Tree $tree): Tree
{
    return $tree
        ->appendToolbarActions([
            Action::make('save_order')
                ->label('Save')
                ->icon('heroicon-o-check')
                ->extraAttributes(['x-show' => 'hasUnsavedOrder', 'x-cloak' => true])
                ->action('saveOrder'),
        ]);
}

use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;

public function tree(Tree $tree): Tree
{
    return $tree
        ->model(Category::class)
        ->nodeActions([
            EditAction::make()
                ->iconButton()
                ->icon('heroicon-o-pencil')
                ->size('sm'),
            DeleteAction::make()
                ->iconButton()
                ->icon('heroicon-o-trash')
                ->size('sm')
                ->color('danger'),
        ]);
}

use Filament\Actions\Action;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;

->nodeActions(fn (Tree $tree) => [
    Action::make('rename')
        ->iconButton()
        ->icon('heroicon-o-pencil')
        ->schema([TextInput::make('title')->nts['nodeId'] is the node's primary key
            $record->update(['title' => $data['title']]);
        })
        ->after(fn ($livewire) => $livewire->dispatch('tree-refresh')),
])

->getRecordUsing(function (int|string $id, Tree $tree, $livewire): mixed {
    return Category::withTrashed()->find($id);
})

use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;

public function tree(Tree $tree): Tree
{
    return $tree
        ->model(Category::class)
        ->appendToolbarActions([
            CreateAction::make('create_node')
                ->model(Category::class)
                ->schema(fn (Schema $schema) => $this->form($schema))
                ->after(fn ($livewire) => $livewire->dispatch('tree-refresh'))
                ->extraAttributes(['style' => 'margin-left: auto;']),

            ActionGroup::make([
                Action::make('import')->label('Import'),
                Action::make('export')->label('Export'),
            ])->label('More'),
        ]);
}

use SolutionForest\FilamentNestableTree\Filament\Pages\TreePage;
use SolutionForest\FilamentNestableTree\Tree;

class MultiTreePage extends TreePage
{
    public function trees(): array
    {
        return [
            'categories' => Tree::make()->model(Category::class)->searchable()->labelField('title'),
            'tags'       => Tree::make()->model(Tag::class)->searchable()->labelField('name'),
        ];
    }
}

class MultiTreePage extends TreePage
{
    public function trees(): array
    {
        $electronicsId = Category::where('title', 'Electronics')->value('id');
        $clothingId    = Category::where('title', 'Clothing')->value('id');

        $createAction = fn (string $category) => CreateAction::make('create_' . $category)
            ->iconButton()
            ->icon('heroicon-o-plus')
            ->after(fn ($livewire) => $livewire->dispatch('tree-refresh'))
            ->schema([
                TextInput::make('name')->ategoryId,
                ]);
            });

        return [
            'technology' => Tree::make()
                ->records(fn () => Tag::where('category_id', $electronicsId)
                    ->defaultOrder()->get()->toTree()->toArray())
                ->labelField('name')
                ->allowCrossCategory()
                ->saveOrderUsing(fn (array $nodes) => Tag::rebuildTree($nodes))
                ->appendToolbarActions([$createAction('technology')]),

            'science' => Tree::make()
                ->records(fn () => Tag::where('category_id', $clothingId)
                    ->defaultOrder()->get()->toTree()->toArray())
                ->labelField('name')
                ->allowCrossCategory()
                ->saveOrderUsing(fn (array $nodes) => Tag::rebuildTree($nodes))
                ->appendToolbarActions([$createAction('science')]),
        ];
    }

    /**
     * Called automatically when a node is dragged from one tree to another.
     */
    public function handleCrossTreeMove(
        string $fromTreeKey,
        string $toTreeKey,
        int|string $nodeId,
        mixed $destinationParentId = null,
    ): void {
        $tag = Tag::find($nodeId);

        if (! $tag) {
            return;
        }

        $newCategoryTitle = $this->treeCategories[$toTreeKey] ?? null;
        $newCategoryId    = $newCategoryTitle
            ? Category::where('title', $newCategoryTitle)->value('id')
            : null;

        if ($destinationParentId) {
            $parent = Tag::find($destinationParentId);
            if ($parent) {
                $tag->appendToNode($parent)->save();
            }
        } else {
            $tag->saveAsRoot();
        }

        if ($newCategoryId) {
            $tag->update(['category_id' => $newCategoryId]);
        }

        $this->dispatch('tree-refresh');
    }
}

class CategoryPartitionedTreePage extends TreePage
{
    /** Flat node store — replace with database reads in production. */
    public static array $nodes = [];

    private const TREE_CATEGORY_MAP = ['tree1' => 1, 'tree2' => 2];

    protected $listeners = ['tree-cross-move' => 'handleCrossTreeMove'];

    public function trees(): array
    {
        return [
            'tree1' => Tree::make()
                ->labelField('title')
                ->allowCrossCategory()
                ->records(fn () => $this->asTree(
                    collect(static::$nodes)->where('category_id', 1)->values()->all()
                ))
                ->saveOrderUsing($this->saveOrderForCategory(1)),

            'tree2' => Tree::make()
                ->labelField('title')
                ->allowCrossCategory()
                ->records(fn () => $this->asTree(
                    collect(static::$nodes)->where('category_id', 2)->values()->all()
                ))
                ->saveOrderUsing($this->saveOrderForCategory(2)),
        ];
    }

    /**
     * Flatten + tag each saved node with its category, then merge back with
     * nodes that belong to other categories so nothing gets lost on save.
     */
    private function saveOrderForCategory(int $categoryId): Closure
    {
        return function (array $nodes) use ($categoryId): void {
            $saved  = collect($this->asFlatten($nodes))
                ->map(fn ($n) => array_merge($n, ['category_id' => $categoryId]))
                ->all();

            $others = collect(static::$nodes)
                ->filter(fn ($n) => ($n['category_id'] ?? null) != $categoryId)
                ->values()
                ->all();

            static::$nodes = array_merge($others, $saved);
        };
    }

    /**
     * Update the partition field (category_id) and parent_id when a node is
     * dragged between trees.  Silently ignored for unknown tree keys.
     */
    public function handleCrossTreeMove(
        string $fromTreeKey,
        string $toTreeKey,
        int|string $nodeId,
        mixed $destinationParentId = null,
    ): void {
        $destCategory = self::TREE_CATEGORY_MAP[$toTreeKey] ?? null;
        if ($destCategory === null) {
            return;
        }

        static::$nodes = collect(static::$nodes)
            ->map(function ($node) use ($nodeId, $destCategory, $destinationParentId) {
                if ((string) $node['id'] === (string) $nodeId) {
                    $node['category_id'] = $destCategory;
                    $node['parent_id']   = $destinationParentId;
                }
                return $node;
            })
            ->all();

        $this->dispatch('tree-refresh');
    }

    // ── Helpers ────────────────────────────────────────────────────────────────

    /** Flat parent_id array → nested children array. */
    private function asTree(array $flat): array
    {
        $map = [];
        foreach ($flat as $item) {
            $map[$item['id']] = $item + ['children' => []];
        }
        $tree = [];
        foreach ($map as $id => &$node) {
            if ($node['parent_id'] === null || ! isset($map[$node['parent_id']])) {
                $tree[] = &$node;
            } else {
                $map[$node['parent_id']]['children'][] = &$node;
            }
        }
        return $tree;
    }

    /** Nested children array → flat array (strips children key). */
    private function asFlatten(array $tree): array
    {
        $flat = [];
        foreach ($tree as $item) {
            $children = $item['children'] ?? [];
            unset($item['children']);
            $flat[] = $item;
            if (! empty($children)) {
                $flat = array_merge($flat, $this->asFlatten($children));
            }
        }
        return $flat;
    }
}

Tree::make()->model(Category::class)->lazy()

Tree::make()
    ->model(Category::class)
    ->asyncChildren(function (int|string $parentId): array {
        return Category::where('parent_id', $parentId)->get()->toArray();
    })

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->unsignedInteger('order')->default(0);
    $table->foreignId('parent_id')->nullable()->constrained('posts')->nullOnDelete();
    $table->timestamps();
});

class Post extends Model
{
    public function children(): HasMany
    {
        return $this->hasMany(Post::class, 'parent_id')->orderBy('order')->with('children');
    }
}

public function tree(Tree $tree): Tree
{
    return $tree
        ->model(Post::class)
        ->labelField('name')
        ->parentKeyField('parent_id')
        ->saveOrderUsing(function (array $nodes): void {
            $this->saveOrder($nodes);
        });
}

private function saveOrder(array $nodes, ?int $parentId = null, int $start = 0): void
{
    foreach ($nodes as $index => $node) {
        Post::where('id', $node['id'])->update([
            'parent_id' => $parentId,
            'order'     => $start + $index,
        ]);
        if (! empty($node['children'])) {
            $this->saveOrder($node['children'], (int) $node['id'], 0);
        }
    }
}

public function tree(Tree $tree): Tree
{
    return $tree
        ->labelField('name')
        ->records(fn () => $this->buildTree(Post::orderBy('order')->get()))
        ->saveOrderUsing(function (array $nodes): void {
            $this->saveOrder($nodes);
        });
}

private function buildTree(Collection $items, mixed $parentId = null): array
{
    return $items
        ->where('parent_id', $parentId)
        ->map(fn (Post $item) => array_merge($item->toArray(), [
            'children' => $this->buildTree($items, $item->id),
        ]))
        ->values()
        ->toArray();
}

->model(Post::class)
->asyncChildren(function (int|string $parentId): array {
    return Post::where('parent_id', $parentId)->orderBy('order')->get()->toArray();
})
css
> @source '../../../../vendor/solution-forest/filament-nestable-tree/resources/**/*.blade.php';
> 
bash
php artisan make:filament-tree-page CategoryTreePage
bash
php artisan make:filament-tree-resource-page ManageCategoryTree
bash
php artisan make:filament-tree-widget CategoryTreeWidget