Skip to main content

Tree component

Tree is designed for product categories, organization charts, folders, regions, and other hierarchical data. You can maintain a small tree directly in the page structure, or load roots and children from an API. The same component also supports search, node-level actions, and drag-and-drop reordering.

This guide covers how to:

  • Build a small static navigation tree
  • Load a complete hierarchy or fetch children on demand
  • Map API fields to the node title, value, leaf state, and secondary text
  • Search locally or through a backend endpoint
  • Use click, check, expand, load, and drop context
  • Add a shared More menu and desktop context menu
  • Persist a new hierarchy through onDrop
  • Control the tree with reloadTree, expandAll, and collapseAll

1. Choose the right setup​

Start with the way the hierarchy is maintained:

ScenarioRecommended setupRead next
A short, stable list maintained by page authorsStatic Tree NodesStatic nodes, Refresh content when a node is selected
Data owned by a business systemTree Data SourceDynamic data source, Search
Add, edit, delete, or contextual commandsDynamic source + Node ActionsConfigure node actions
Reorder siblings or move branchesDynamic source + onDropDrag and persist changes

The two Spark Sample pages below illustrate the static and dynamic approaches. You do not need access to the Sample application to reproduce them.

1.1 Tree: static product categories​

The Tree page keeps a small product taxonomy in the page structure. Selecting a node writes its category to Page Context, then reloads the ProTable on the right.

Screen
├── Navigation Tree
│ └── Tree
│ └── Tree Nodes
│ ├── All Products (ALL)
│ ├── Computing (COMPUTING)
│ ├── Displays (DISPLAY)
│ └── Accessories (ACCESSORY)
└── Panels
└── ProTable

The important part is the order of the interaction chain:

  1. Store [[treeNode.name]] in selectedCategory with Set Page Context.
  2. Call the ProTable reloadTable function with Component API.
  3. Map selectedCategory to the category request parameter in the ProTable Query Context.

See Refresh content when a node is selected for the full editor setup.

1.2 Tree02: dynamic loading and hierarchy editing​

Tree02 combines a backend-powered Tree with a ProTable and node commands:

CapabilitySample behavior
Roots and childrenLoads roots when the page opens, then requests direct children with parentId
SearchSends the term to the server and keeps the ancestors of matching nodes
Detail panelWrites id and category to Page Context before reloading the ProTable
Node commandsUses the same Edit, Add Child, View Details, and Delete items for More and right-click
Drag-and-dropSaves through onDrop; the visual move is rolled back if persistence fails

Build the example in stages: verify the root response first, add lazy loading and search next, then configure click behavior and node actions. Turn on dragging only after the read path is working. This makes failures much easier to isolate.

2. Add a Tree​

  1. Open the target page in Page Designer.
  2. Select Screen in the page structure.
  3. Add Tree under the Screen's Navigation Tree child area.
  4. For static data, add entries under Tree Nodes.
  5. For API-backed data, add Tree Data Source under Data Source.
  6. Add ProTable, Form, or other content under Panels.

Navigation Tree is the dedicated left-hand region of Screen. Panels and Grids automatically form the content region, so there is no need to recreate this layout with a Grid.

3. Configure the Tree​

PropertyExamplePurpose
TitleDynamic OrganizationOptional heading above the tree
Width320pxDesktop width in pixels; the tree becomes a full row on narrow screens
Show SearchOnDisplays the search field
Show IconOnDisplays folder and leaf icons
SelectableOnAllows selection and fires onClick
Show Connecting LinesOnShows the parent-child guides
Show CheckboxOffEnables multi-selection when needed
Strict ModeOffStops parent and child checkbox states from affecting each other
Node DraggableOnAllows runtime reordering
Drag ScopeSAME_PARENTRestricts moves to siblings or allows cross-branch movement
Node Action TriggerMORE_BUTTON_AND_CONTEXT_MENUKeeps the More button and enables desktop right-click

SAME_PARENT allows only sibling reordering. ANY_LEVEL also allows moving a node to another branch; Tree still prevents a parent from being moved inside one of its descendants. Keep Node Draggable off when the tree is navigation-only.

4. Static nodes​

Each static Tree Node needs at least:

PropertyExamplePurpose
NameACCESSORYStable internal value used in context or API parameters
TitleAccessoriesUser-facing label

Keep Name stable across languages. Localize Title instead.

A node with Children is treated as a folder; a node without Children is a leaf. A dynamic Tree may still include a few static entries for design-time fallback. Once a dynamic request succeeds, its response becomes authoritative.

5. Dynamic data source​

Add Tree Data Source under the Tree's Data Source area. Configure the root request and mappings before adding lazy loading or remote search:

  1. Open Data Source and choose Add data source, or edit the existing source.
  2. Select Tree Data Source.
  3. Fill in the basic properties below.
  4. Select Test and confirm that the response produces nodes.
  5. Open Advanced and add child-loading and search settings.
  6. Test the root request again, then save.
PropertyTree02 valuePurpose
Root Data URL/spark/api/sample/navigation/tree/nodesReturns roots or a complete tree
Request MethodGETGET or POST
Request ParametersAs neededMay reference Page Context, components, and browser values
Response List Pathdata.nodesPath to the node array; leave blank to detect common shapes
Title FieldlabelUser-facing node text
Value FieldidStable, unique node value
Children FieldchildrenNested child array
Leaf FieldleafBoolean indicating that a node has no children
Disabled FielddisabledBoolean indicating that a node cannot be selected
Meta FieldcountSecondary text, such as the child count
Initial LoadOnRequests roots when the page opens

Root Data URL may be an application-relative path, an absolute URL, or a relative path resolved through Connection Key.

5.1 Test the request and inspect the response​

Test calls the configured Root Data URL. Spark automatically adds the current environment and design-preview mode. A test from a DEV page carries:

X-Spark-Page-Environment: DEV
X-Spark-Page-Render-Mode: DESIGN_PREVIEW

The result panel shows the HTTP status, duration, final URL, business parameters, Response List Path, masked JSON response, and parsed node count.

If the request returns 200 with zero parsed nodes, check Response List Path first. If nodes are found but labels are blank, check Title Field. For a browser-level failure, verify that CORS allows both X-Spark-Page-* headers.

Test, design preview, and runtime use the same request builder and response parser. Do not add the environment headers manually in Data Source. The full contract is described in Page request context and backend debugging.

The backend resolves the standard headers once and passes the resulting context to the service:

@GetMapping("/nodes")
public ApiResult<NavigationTreeDataResult> nodes(
@RequestParam(required = false) String parentId,
@RequestParam(required = false) String query,
@RequestHeader(value = "Accept-Language", required = false) String language,
@RequestHeader(value = SparkPageRequestHeaders.ENVIRONMENT,
required = false) String environment,
@RequestHeader(value = SparkPageRequestHeaders.RENDER_MODE,
required = false) String renderMode) {
Locale locale = SparkApiResultHelper.resolveLocale(language);
SparkPageRequestContext requestContext =
SparkPageRequestHeaders.resolve(environment, renderMode);
NavigationTreeDataResult result = new NavigationTreeDataResult(
treeService.query(parentId, query, requestContext), requestContext);
return SparkApiResultHelper.success(
locale, result, "SPARK-SAMPLE-NAVIGATION-0002");
}

The service works with shared enums rather than parsing header strings again:

private boolean isDevelopmentPreview(
SparkPageRequestContext requestContext) {
return requestContext != null
&& requestContext.getEnvironment() == SparkPageEnvironment.DEV
&& requestContext.getRenderMode()
== SparkPageRenderMode.DESIGN_PREVIEW;
}

Tree02 returns a deterministic preview hierarchy for DEV + DESIGN_PREVIEW; runtime requests use the data for their target environment. The context headers are routing and debugging signals, not a substitute for authentication, authorization, or tenant isolation.

5.2 Return a complete hierarchy​

When one request returns the entire hierarchy, place children in the configured Children Field:

{
"success": true,
"data": {
"nodes": [
{
"id": "catalog",
"label": "Product Catalog",
"count": "2",
"leaf": false,
"children": [
{ "id": "computing", "label": "Computing", "leaf": true, "children": [] },
{ "id": "display", "label": "Displays", "leaf": true, "children": [] }
]
}
]
}
}

Child Nodes URL can remain blank in this mode.

5.3 Load children on demand​

For large or deep hierarchies, configure:

PropertyExamplePurpose
Child Nodes URL/spark/api/sample/navigation/tree/nodesCalled when a folder is expanded; it may match Root Data URL
Parent ParameterparentIdReceives the current folder's Value
GET /spark/api/sample/navigation/tree/nodes
GET /spark/api/sample/navigation/tree/nodes?parentId=catalog

With lazy loading, Leaf Field must clearly identify nodes with no children. Otherwise Tree cannot know whether to display the expansion control.

By default, search filters only nodes already loaded in the browser and does not change the selection.

For branches that may not have been loaded, enable Server-side Search:

PropertyExamplePurpose
Search URL/spark/api/sample/navigation/tree/nodesOptional; falls back to Root Data URL
Search ParameterqueryParameter carrying the search term

Typing tier 2 produces:

GET /spark/api/sample/navigation/tree/nodes?query=tier%202

Return each match with its ancestors. A leaf without its path gives users no useful sense of location. Clearing the query reloads the roots.

7. Event context​

Tree publishes event context before running the configured interaction.

7.1 Node click: onClick​

treeNode.key
treeNode.value
treeNode.name
treeNode.title
treeNode.parentKey
treeNode.parentValue
treeNode.depth
treeNode.isLeaf
treeNode.data
treeNode.extData

For dynamic nodes, the original API record is available as treeNode.data:

[[treeNode.value]]
[[treeNode.data.category]]

Static-node extension fields remain available under treeNode.extData.

7.2 Checkbox change: onCheck​

treeCheckedKeys
treeCheckedNodes

7.3 Expand or collapse: onExpand​

treeExpand.expanded
treeExpand.node
treeExpand.expandedKeys

7.4 Data loaded: onLoad​

treeLoad.parentNode
treeLoad.query
treeLoad.nodeCount

parentNode is empty for a root load and identifies the expanded folder for a lazy child request.

8. Configure node actions​

Node Actions are defined once on Tree and apply to static nodes, dynamic roots, and lazy-loaded children. Visibility and disabled conditions can still vary by node.

8.1 Add actions​

  1. Select Tree.
  2. Add Tree Node Action under Node Actions.
  3. Enter Title and choose an icon in the shared Icon picker.
  4. Use DANGER for destructive commands.
  5. Enable Divider Before when the item starts a new group.
  6. Select the action and configure its onClick interaction.

The child-element order is the menu order. A divider is never rendered above the first visible item.

8.2 More and right-click​

Right-click is not a separate onContextMenu interaction. Both entry points use the same Node Actions:

  • MORE_BUTTON keeps only the visible More button.
  • MORE_BUTTON_AND_CONTEXT_MENU also opens the same menu on desktop right-click.

Tree02 uses:

Tree
└── Node Actions
├── Edit
├── Add Child
├── View Details
└── Delete
ActionAppearance and conditionInteraction chain
Editedit, DEFAULTFill the edit form from treeNode, then open its Drawer
Add Childlink, DEFAULTSet the parent from treeNode.value, clear the name, then open the create Drawer
View Detailsview, DEFAULTFill a read-only form and open the detail Drawer
Deletedelete, DANGER, Divider Before; hide when ![[treeNode.isLeaf]]Reminder, delete request, then reloadTree

Keep action interactions on the Tree Node Action itself, not on the Tree's onClick.

Edit sends PUT /spark/api/sample/navigation/tree/nodes/[[treeNode.value]]; Add Child sends POST /spark/api/sample/navigation/tree/nodes; Delete sends:

DELETE /spark/api/sample/navigation/tree/nodes/[[treeNode.value]]

Only reload after a successful request. The backend must enforce permissions and delete rules independently; hiding a command is not a security boundary.

8.3 Trigger behavior and keyboard access​

More and context-menu triggers do not bubble to selection, checking, expansion, or dragging. Right-click suppresses the browser menu only when the current node has at least one visible action. Touch devices always use More rather than long-press.

Keyboard users can press Enter or Space on More, or Menu / Shift+F10 on a node. Arrow keys move through the menu; Enter or Space runs an item; Escape closes the menu and restores focus.

8.4 Show or disable actions by node​

Advanced exposes:

PropertyEffectExample
Hide This ActionHides the item when true[[treeNode.isLeaf]]
Disable This ActionLeaves the item visible but unavailable[[treeNode.data.locked]]

Expressions may use !, &&, ||, and common comparisons. An invalid Hide expression fails closed by hiding the item; an invalid Disable expression keeps it disabled. Page Designer also reports a configuration diagnostic.

8.5 Action context​

Opening a menu writes the target to treeNode and selectedTreeNode without firing Tree onClick. Choosing an item also publishes:

treeAction.id
treeAction.elementId
treeAction.title
treeAction.tone

Design preview can show the menu and conditions, but it does not run the action chain. Test Drawer, Modal, Reminder, and request behavior after saving the page.

9. Drag and persist changes​

Enable Node Draggable to move a node before, inside, or after another node. After the visual move, Tree fires onDrop and exposes:

treeDrop.dragNode
treeDrop.dropNode
treeDrop.position
treeDrop.oldParentKey
treeDrop.newParentKey
treeDrop.oldIndex
treeDrop.newIndex
treeDrop.treeData

treeDrop.position is BEFORE, INSIDE, or AFTER. The move initially affects only the current page. To persist it, add Send Request to onDrop:

POST /spark/api/sample/navigation/tree/reorder
ParameterValue
dragId[[treeDrop.dragNode.key]]
dropId[[treeDrop.dropNode.key]]
position[[treeDrop.position]]

If the request fails or is interrupted, Tree restores the previous structure. The backend must still validate both nodes, the new parent, cycles, permissions, and concurrent changes.

10. Refresh content when a node is selected​

Tree publishes the current node; it does not decide how another component should query data. Write the selection to Page Context first, then call the target component.

10.1 Static Tree​

For the static product-category example:

  1. Select Tree, open Interaction, and choose onClick.
  2. Add Set Page Context, named Set selected category.
  3. Add selectedCategory = [[treeNode.name]] in Action Config.
  4. Add Component API, named Reload product table.
  5. Select the ProTable and its reloadTable function.
  6. Keep Set Page Context before Component API.

In ProTable Query Context, map the category parameter to Page Context path selectedCategory. The backend can treat ALL as no category filter.

10.2 Dynamic Tree​

Tree02 follows the same sequence:

  1. Write [[treeNode.value]] to selectedNodeId and [[treeNode.data.category]] to selectedCategory.
  2. Call the ProTable reloadTable function.

Map those values to the request parameters nodeId and category. Category nodes filter by category; the sample backend maps business nodes such as Operations or Support to a suitable product view by nodeId.

11. Component functions​

Other interactions can call Tree through Component API:

FunctionBehavior
reloadTreeRequests the root nodes again and retains a still-valid selection
expandAllExpands every directory already loaded in the page
collapseAllCollapses all directories

expandAll does not fetch every unloaded branch of a large lazy tree.

12. Java backend reference​

The following contract matches Tree02 and targets Java 17 with Spring Boot 4. The sample uses in-memory data to keep the interaction visible; a production service can replace the storage while retaining the DTO and endpoint contract.

12.1 DTOs and request models​

Return a stable id and an explicit leaf flag. A non-leaf node may return an empty children list until it is expanded.

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class NavigationTreeNodeDTO {
private String id;
private String label;
private String parentId;
private String category;
private String count;
private boolean leaf;
private boolean disabled;
private List<NavigationTreeNodeDTO> children = new ArrayList<>();
}

@Getter
@Setter
public class NavigationTreeNodeWriteRequest {
private String parentId;
private String label;
private String category;
}

@Getter
@Setter
public class NavigationTreeReorderRequest {
private String dragId;
private String dropId;
private String position;
}

12.2 Endpoints and page request context​

UseMethod and path
Roots, children, and searchGET /spark/api/sample/navigation/tree/nodes
Add a childPOST /spark/api/sample/navigation/tree/nodes
Edit a nodePUT /spark/api/sample/navigation/tree/nodes/{nodeId}
Delete a nodeDELETE /spark/api/sample/navigation/tree/nodes/{nodeId}
Persist a dragPOST /spark/api/sample/navigation/tree/reorder

Resolve the standard headers once in the controller. The same endpoint can then return deterministic preview data for Test and environment-specific data at runtime.

@GetMapping("/nodes")
public ApiResult<NavigationTreeDataResult> nodes(
@RequestParam(required = false) String parentId,
@RequestParam(required = false) String query,
@RequestHeader(value = "Accept-Language", required = false) String language,
@RequestHeader(value = SparkPageRequestHeaders.ENVIRONMENT,
required = false) String environment,
@RequestHeader(value = SparkPageRequestHeaders.RENDER_MODE,
required = false) String renderMode) {
Locale locale = SparkApiResultHelper.resolveLocale(language);
SparkPageRequestContext context =
SparkPageRequestHeaders.resolve(environment, renderMode);
NavigationTreeDataResult result = new NavigationTreeDataResult(
treeService.query(parentId, query, context), context);
return SparkApiResultHelper.success(
locale, result, "SPARK-SAMPLE-NAVIGATION-0002");
}

12.3 Lazy loading, search, and preview data​

With no parentId, return root nodes. With a parentId, return direct children only. Search recursively and retain the ancestor path of every match.

public synchronized List<NavigationTreeNodeDTO> query(
String parentId,
String query,
SparkPageRequestContext context) {
Map<String, NodeRecord> source = isDevelopmentPreview(context)
? designPreviewNodes : nodes;
String keyword = StringUtils.trimToNull(query);
if (keyword != null) {
return children(source, null).stream()
.map(root -> filterTree(source, root, keyword))
.filter(Objects::nonNull)
.toList();
}
return children(source, StringUtils.trimToNull(parentId)).stream()
.map(node -> toDto(source, node))
.toList();
}

private boolean isDevelopmentPreview(SparkPageRequestContext context) {
return context != null
&& context.getEnvironment() == SparkPageEnvironment.DEV
&& context.getRenderMode() == SparkPageRenderMode.DESIGN_PREVIEW;
}

Tree02 Test therefore reads stable designPreviewNodes. Runtime requests use the repository selected for DEV, TEST, or PROD. Do not infer this state from custom query parameters when the standard headers already carry it.

12.4 Create, update, and delete​

Validate identifiers, parent nodes, labels, permissions, and delete rules on the server. This sample permits deletion only for leaf nodes. User-facing failures use LatticeRuntimeException(Message.code(...)); keep translated messages in server-side i18n resources instead of composing strings in the service.

public synchronized NavigationTreeNodeDTO delete(String nodeId) {
String id = StringUtils.trimToNull(nodeId);
NodeRecord node = nodes.get(id);
if (node == null || !children(nodes, id).isEmpty()) {
throw new LatticeRuntimeException(
Message.code("SPARK-SAMPLE-NAVIGATION-0008"));
}
nodes.remove(id);
return toDto(nodes, node);
}

12.5 Persist a drag​

Never trust the submitted position directly. Verify that both nodes exist, reject self-drops and descendant cycles, derive the new parent, and update sibling order in one transaction. Add permission and optimistic-lock checks for production data.

String newParentId = StringUtils.equals(position, "INSIDE")
? dropId : dropped.parentId;
if (!List.of("BEFORE", "INSIDE", "AFTER").contains(position)
|| isDescendant(dropId, dragId)) {
throw new LatticeRuntimeException(
Message.code("SPARK-SAMPLE-NAVIGATION-0006"));
}
// Rewrite the old and new sibling order in the same transaction.

Return a non-success result when persistence fails so Tree can roll back the visual move.

12.6 Filter the content component​

The ProTable request adds nodeId to the regular product query. Category nodes continue to use category; business nodes can be mapped on the server.

private void applyBusinessNodeFilter(NavigationProductQueryRequest query) {
String nodeId = StringUtils.lowerCase(
StringUtils.trimToEmpty(query.getNodeId()), Locale.ROOT);
switch (nodeId) {
case "operations" -> query.setStatus("DRAFT");
case "support" -> query.setStatus("DISCONTINUED");
case "enterprise" -> query.setMinPrice(BigDecimal.valueOf(1000));
default -> {
// Category nodes continue to use the category returned by Tree.
}
}
}

The product controller should resolve the same standard page request headers before calling the service, so preview and runtime queries follow the same environment contract.

13. Validate and troubleshoot​

Validate one layer at a time: data loading, selection, node commands, and finally drag persistence.

  1. Click Test and confirm two root nodes are parsed.
  2. Expand Product Catalog and confirm only its direct children are requested.
  3. Search for tier 2 and confirm ancestor paths remain visible.
  4. Select several category and business nodes and confirm the right-side list changes.
  5. Test More and right-click commands without accidentally firing Tree onClick.
  6. Complete View, Edit, Add Child, and Delete, then confirm Tree reloads.
  7. Move Accessories before Computing, refresh, and confirm the order persists.
  8. Simulate a failed reorder request and confirm the visual move rolls back.
SymptomCheck first
Test request failsURL, method, connection, authentication, and CORS
HTTP 200 but zero nodesWhether Response List Path points to an array
Counts appear but titles do notTitle Field and Value Field names
A directory has no expand controlWhether Leaf Field incorrectly returns true
Expanding does not load childrenChild Nodes URL, Parent Parameter, and parent Value
Search returns isolated leavesWhether the backend includes ancestor paths
Different nodes show the same listPage Context order and ProTable Query Context paths
Menu items are absentTrigger, Hide This Action, and current node data
Order resets after refreshonDrop parameters and backend persistence
A failed save does not roll backWhether Send Request returns a failure/interruption result