Skip to content

lattice-php/tree

Repository files navigation

Lattice Tree

Tree view component for Lattice — hierarchy rendering from inline nodes, callbacks, or Eloquent adjacency-list sources, with full keyboard navigation (roving tabindex, typeahead), per-node icons, badges, links, and actions, and lazy child loading over a signed endpoint.

Extracted from the Lattice core package to grow on its own; drag & drop reordering is next on the roadmap.

Installation

composer require lattice-php/tree

That is the whole integration: the package ships its React renderer as source and Lattice's lattice() Vite plugin compiles it into your app's bundle via virtual:lattice/plugins. The PHP classes are picked up by Lattice's discovery and TypeScript generation automatically.

Usage

use Lattice\Tree\Tree;
use Lattice\Tree\TreeNode;

Tree::make('categories')->nodes([
    TreeNode::make('electronics', 'Electronics')
        ->icon('cpu')
        ->children([
            TreeNode::make('electronics-laptops', 'Laptops'),
            TreeNode::make('electronics-phones', 'Phones')->href('/products/phones'),
        ]),
])->defaultExpanded(['electronics']);

Nodes compile their conveniences (->icon(), ->badge(), ->href(), ->action()/->actions()) into a canonical body schema of core components — an icon, a text-or-link label, a badge, and an end-floated action stack. ->badge($label, $color) accepts any Lattice color.

Custom node content

For content the conveniences do not cover, ->schema() replaces the composed body outright:

use Lattice\Lattice\Ui\Components\Avatar;
use Lattice\Lattice\Ui\Components\Badge;
use Lattice\Lattice\Ui\Components\Text;

TreeNode::make('acme-corp', 'Acme Corp')
    ->schema([
        Avatar::make('/avatars/acme.png'),
        Text::make('Acme Corp'),
        Badge::make('Pro')->color('purple'),
    ]);

->schema() is an escape hatch, not an addition — it replaces the default body entirely. label is still required and keeps driving typeahead and aria announcements even when the schema does not render it. On Enter/Space, a node without an href activates the first button found in its body, so a custom schema should put its primary action's button first — or expect it to receive Enter.

Or back it with an Eloquent adjacency list (a self-referencing parent_id column):

use Lattice\Tree\EloquentTreeSource;

Tree::make('categories')->source(
    EloquentTreeSource::make(Category::class)
        ->scope(fn ($query) => $query->where('active', true)),
);

Any other backing store implements the two-method Lattice\Tree\TreeSource contract.

Lazy loading

Serializing a large hierarchy eagerly is wasteful. Register a tree definition and let expansion fetch one level per request instead:

use Lattice\Tree\AsTree;
use Lattice\Tree\EloquentTreeSource;
use Lattice\Tree\TreeDefinition;
use Lattice\Tree\TreeSource;

#[AsTree('categories')]
class CategoryTree extends TreeDefinition
{
    public function source(): TreeSource
    {
        return EloquentTreeSource::make(Category::class);
    }
}
Tree::use(CategoryTree::class)->lazy();     // roots eager, deeper levels fetched on expand
Tree::use(CategoryTree::class)->lazy(2);    // two levels eager
Tree::use(CategoryTree::class)->lazy(0);    // bare skeleton — even the roots are fetched

The definition is discovered like any Lattice definition (#[AsTree] + Lattice's discovery paths), and the serialized tree carries a sealed reference — the same signing machinery Lattice tables use — that the package's lattice/trees/{tree} endpoint verifies before resolving the definition again with the identical context. authorize() on the definition gates both the initial render and every fetch. The route's middleware and path follow Lattice's group conventions: config('lattice.trees.middleware', ['web', 'auth']) and config('lattice.trees.endpoint', 'lattice/trees/{tree}').

An EloquentTreeSource behind the endpoint automatically switches to per-level queries (WHERE parent_id = ? plus a scoped EXISTS probe for hasChildren) instead of loading the whole table. Inline ->nodes() / ->source() trees stay eager-only — without a registry key there is nothing to seal — so ->lazy() on them throws.

Translations

The component's strings ship with inline English defaults. With bambamboole/laravel-i18next enabled, the plugin's tree namespace is loaded automatically and serves the bundled en/de translations (override them like any Laravel package translation).

Development

composer install && npm install
composer check          # pint --test, phpstan, pest (Unit + Feature)
npm run typecheck && npm test
composer test:browser   # rebuilds the workbench bundle, then runs the Playwright suite
composer serve          # workbench demo app: /tree (eager) and /tree-lazy (lazy endpoint)

About

Tree view component for Lattice — hierarchy rendering with lazy loading and drag & drop on the roadmap

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages