codewithdennis/filament-select-tree
Add a dynamic select tree field to Laravel and Filament apps. Build interactive hierarchical selection dropdowns from BelongsTo or BelongsToMany relationships, or custom queries. Customize parent and child queries, and toggle strict null-parent root nodes.
Architecture fit
This is not a platform dependency; it is a single Filament form-field class (SelectTree) that wraps the treeselectjs JavaScript widget in Livewire/Alpine glue. That framing drives the entire evaluation:
BelongsTo, BelongsToMany) or an arbitrary array via getTreeUsing() and renders a hierarchical picker. It does not own schema, migrations, or persistence. That makes adoption cheap and reversal cheap — the defining characteristic from a risk standpoint.parent_id-style column, configurable, or a distinct key pair via withKey('code')). There is no nested-set or closure-table support. If the domain already models hierarchies that way, the default path is frictionless; if not, getTreeUsing() is the escape hatch and you inherit responsibility for query efficiency.getTreeUsing() (array or closure), prepend/append, modifyQueryUsing/modifyChildQueryUsing, disabledOptions/hiddenOptions closures, and storeResults() mean the package rarely blocks a requirement. The cost is that complex behavior moves into application code, where it is your maintenance burden, not the maintainer's.Integration feasibility
Feasible in an afternoon for a Filament-fluent engineer. Two steps: composer require codewithdennis/filament-select-tree:4.x, then php artisan filament:assets. Configuration is fluent and documented extensively in the README (which is unusually complete for a package of this size — ~30 documented methods, filter examples, and a custom-tree contract).
The non-trivial work is not installation; it is the three places the package's defaults can diverge from your domain semantics:
enableBranchNode() and isGroupedValue() change what gets persisted. Whether "parent" selections store a parent ID, all descendant IDs, or a sentinel is a data-contract decision that must be made explicitly before the first record is written.strictNullParentRootNodes() exists precisely for this, and its introduction/repair in v4.2.0–v4.2.2 shows the semantics were actively in flux.multiple(false). Implicit behavior that depends on the model's declared relationship is easy to get wrong silently.Technical risk — overall low-to-moderate, and concentrated in a few named areas rather than diffuse:
filament:assets is a deploy-time step. In containerized or immutable deployments with a cached/read-only filesystem, a missed or misordered step yields a broken field with no server-side error. This is an operational failure mode dressed as a technical one.parent_id chains. A data-entry or import bug that creates a cycle is not defended against in the documented surface. Worth a validation rule at write time.Key questions to resolve before committing
filament:assets step in the deploy pipeline, and is it verified?parent_id, or do we need withKey()?Stack fit
Excellent fit for Laravel + Filament admin panels; no fit otherwise. The dependency chain is unremarkable — Composer for the PHP side, an npm-published JS library (treeselectjs) shipped as compiled assets. No queue workers, no additional services, no database objects, no runtime daemons. It sits entirely inside the Filament render path.
The package is a genuine UX upgrade over the alternatives for hierarchical selection: a flat Select with indented labels degrades badly past two levels, and a custom Livewire cascade of dependent selects multiplies round-trips. The tree widget solves search, multi-level drill-down, tag display, and keyboard/accessibility concerns in one field. That is the value proposition, and it is real but bounded.
Migration path
There is effectively no data migration — the field reads existing relations. Three realistic starting points:
Select on a self-referencing table: drop-in. Swap the field class, keep the relationship, verify selection-mode inference. Hours, not days.modifyQueryUsing/modifyChildQueryUsing/disabledOptions closures. Watch for the root-promotion default changing visible results.getTreeUsing() with an explicit array or cached closure. This is a supported but more bespoke integration; budget for it.If the schema does not currently have a self-referencing parent column, adopting this package is a data-modelling decision, not a UI decision, and should be treated as such.
Compatibility
codewithdennis/filament-select-tree:4.x (Filament 4) or :3.x (Filament 3). Do not float the constraint across the Filament major boundary.withTrashed(); confirm the intended default for archived nodes rather than inheriting it.Sequencing
parentNullValue(-1) if a sentinel is needed), and tenant/permission scoping rules.filament:assets step in staging and production before the first release that references the field. Treat a missing asset build as a release blocker.Select with flattened labels) if issues appear.parent_id chains and a test asserting scoped trees never surface out-of-scope nodes.Maintenance
Low effort, predictable shape. The package's own release cadence is dependency-bump-driven with occasional targeted fixes, which means staying current is cheap but also means you should not expect upstream to fix your edge case quickly. Two recurring maintenance chores:
Bus factor is the notable structural weakness: one primary maintainer, modest adoption, no dependents recorded. The offsetting factor is that the codebase is small, MIT-licensed, and the field is a thin wrapper — a fork is a day of work, not a quarter. Do not let this dependency accumulate deep application logic inside its callbacks; keep the closures thin so a fork or replacement stays cheap.
Support
Community-only: GitHub issues, no SLA, no vendor. Acceptable for an admin-panel widget on an internal tool; not acceptable if the field is on a revenue-critical or externally facing surface where a broken picker blocks transactions. Calibrate by surface, not by package quality.
Scaling
The meaningful constraint is client-side rendering of the full tree. The widget builds the entire node list in the DOM; defaultOpenLevel() and searchable() mitigate the UX cost but do not eliminate the payload. Practical guidance:
getTreeUsing() with a Cache::remember-backed array to remove per-render query cost, or scope trees per tenant/user so each instance is small.withCount() adds aggregate work per render; storeResults() retains hydrated models in memory per component instance — both are fine in moderation and both are easy to sprinkle indiscriminately.Query cost is the more common real-world issue than DOM cost: naive parent_id traversal is one query per level per render. Cache the tree array and the concern largely disappears.
Failure modes
| Failure | Symptom | Detection | Mitigation |
|---|---|---|---|
| Assets not published / stale | Field renders empty or broken, no server error | Staging smoke test post-deploy | Mandatory filament:assets in pipeline; assert on asset manifest |
| Filament major mismatch | Class-not-found or Livewire render errors | CI composer install + smoke test | Pin :N.x to the Filament major; coupled upgrade tickets |
| Unscoped child query | Cross-tenant options visible | Authorization test on scoped trees | Always pass modifyQueryUsing and modifyChildQueryUsing; use strictNullParentRootNodes() |
| Root-promotion surprise | Filtered tree shows orphaned nodes at root | Visual QA on filtered/filter-context views | strictNullParentRootNodes() wherever scoping is active |
Cyclic parent_id data |
Render hang or recursion error | Model validation + import checks | Prevent cycles at write time; do not rely on the field |
| Branch vs leaf ambiguity | Wrong IDs persisted; downstream reports wrong | Data assertions on saved records | Fix persistence contract before pilot; add a test |
| Maintainer abandonment | No upstream fixes | Quarterly dependency review | Keep closures thin; fork or replace is a day's work |
Ramp-up
Genuinely small. A Filament-fluent engineer is productive in under an hour from the README alone; the documentation is above average for the ecosystem, with worked examples for relationships, custom queries, filters, and custom tree arrays. The learning cost is not in using the field but in deciding the three semantic questions (branch selectability, selection mode, scoping) and in debugging the Alpine/asset seam when something renders wrong — that latter task requires frontend literacy that the backend-most engineer on the team may not have.
Overall TPM judgement: a tactical, low-blast-radius UI dependency with a clear, bounded value proposition and a small, well-understood risk set. Adopt it if — and only if — the application is on Filament and the domain is already adjacency-list modelled. Do not let it become load-bearing, keep the configuration closures thin, and put asset publishing on the release checklist.
How can I help you explore Laravel packages today?