Skip to content

Expand/Collapse Subtree Example

since v1.4.0

This example shows a collapsible tree diagram. Each node with children has a toggle button. Collapsing a node hides its whole subtree with the model hidden flag, and ELK.js lays out the remaining visible nodes again.


import { type Edge, type Node } from 'ng-diagram';
import { NodeTemplateType, type TreeNodeData } from './types';
export const diagramModel: {
nodes: Node<TreeNodeData>[];
edges: Edge[];
} = {
nodes: [
{
id: 'root',
position: { x: 0, y: 0 },
data: { label: 'Application' },
type: NodeTemplateType.TreeNode,
},
{
id: 'frontend',
position: { x: 0, y: 0 },
data: { label: 'Frontend' },
type: NodeTemplateType.TreeNode,
},
{
id: 'components',
position: { x: 0, y: 0 },
data: { label: 'Components', collapsed: true },
type: NodeTemplateType.TreeNode,
},
// The "Components" subtree starts collapsed. Its children are visible in
// the initial model, so they are measured at init. LayoutService sets
// their `hidden` flag together with the first layout.
{
id: 'buttons',
position: { x: 0, y: 0 },
data: { label: 'Buttons' },
type: NodeTemplateType.TreeNode,
},
{
id: 'forms',
position: { x: 0, y: 0 },
data: { label: 'Forms' },
type: NodeTemplateType.TreeNode,
},
{
id: 'services',
position: { x: 0, y: 0 },
data: { label: 'Services' },
type: NodeTemplateType.TreeNode,
},
{
id: 'routing',
position: { x: 0, y: 0 },
data: { label: 'Routing' },
type: NodeTemplateType.TreeNode,
},
{
id: 'backend',
position: { x: 0, y: 0 },
data: { label: 'Backend' },
type: NodeTemplateType.TreeNode,
},
{
id: 'api',
position: { x: 0, y: 0 },
data: { label: 'API' },
type: NodeTemplateType.TreeNode,
},
{
id: 'rest',
position: { x: 0, y: 0 },
data: { label: 'REST' },
type: NodeTemplateType.TreeNode,
},
{
id: 'graphql',
position: { x: 0, y: 0 },
data: { label: 'GraphQL' },
type: NodeTemplateType.TreeNode,
},
{
id: 'database',
position: { x: 0, y: 0 },
data: { label: 'Database' },
type: NodeTemplateType.TreeNode,
},
{
id: 'auth',
position: { x: 0, y: 0 },
data: { label: 'Auth' },
type: NodeTemplateType.TreeNode,
},
{
id: 'devops',
position: { x: 0, y: 0 },
data: { label: 'DevOps' },
type: NodeTemplateType.TreeNode,
},
{
id: 'ci',
position: { x: 0, y: 0 },
data: { label: 'CI/CD' },
type: NodeTemplateType.TreeNode,
},
{
id: 'monitoring',
position: { x: 0, y: 0 },
data: { label: 'Monitoring' },
type: NodeTemplateType.TreeNode,
},
],
edges: [
{
id: 'e-root-frontend',
source: 'root',
sourcePort: 'port-bottom',
target: 'frontend',
targetPort: 'port-top',
data: {},
},
{
id: 'e-root-backend',
source: 'root',
sourcePort: 'port-bottom',
target: 'backend',
targetPort: 'port-top',
data: {},
},
{
id: 'e-root-devops',
source: 'root',
sourcePort: 'port-bottom',
target: 'devops',
targetPort: 'port-top',
data: {},
},
{
id: 'e-frontend-components',
source: 'frontend',
sourcePort: 'port-bottom',
target: 'components',
targetPort: 'port-top',
data: {},
},
{
id: 'e-frontend-services',
source: 'frontend',
sourcePort: 'port-bottom',
target: 'services',
targetPort: 'port-top',
data: {},
},
{
id: 'e-frontend-routing',
source: 'frontend',
sourcePort: 'port-bottom',
target: 'routing',
targetPort: 'port-top',
data: {},
},
{
id: 'e-components-buttons',
source: 'components',
sourcePort: 'port-bottom',
target: 'buttons',
targetPort: 'port-top',
data: {},
},
{
id: 'e-components-forms',
source: 'components',
sourcePort: 'port-bottom',
target: 'forms',
targetPort: 'port-top',
data: {},
},
{
id: 'e-backend-api',
source: 'backend',
sourcePort: 'port-bottom',
target: 'api',
targetPort: 'port-top',
data: {},
},
{
id: 'e-backend-database',
source: 'backend',
sourcePort: 'port-bottom',
target: 'database',
targetPort: 'port-top',
data: {},
},
{
id: 'e-backend-auth',
source: 'backend',
sourcePort: 'port-bottom',
target: 'auth',
targetPort: 'port-top',
data: {},
},
{
id: 'e-api-rest',
source: 'api',
sourcePort: 'port-bottom',
target: 'rest',
targetPort: 'port-top',
data: {},
},
{
id: 'e-api-graphql',
source: 'api',
sourcePort: 'port-bottom',
target: 'graphql',
targetPort: 'port-top',
data: {},
},
{
id: 'e-devops-ci',
source: 'devops',
sourcePort: 'port-bottom',
target: 'ci',
targetPort: 'port-top',
data: {},
},
{
id: 'e-devops-monitoring',
source: 'devops',
sourcePort: 'port-bottom',
target: 'monitoring',
targetPort: 'port-top',
data: {},
},
],
};
  • Hiding with the hidden flag: Collapsing a subtree sets hidden on its nodes in one updateNodes call. Edges do not need their own flag: an edge is hidden automatically when one of its endpoint nodes is hidden. Hidden elements are ignored by hit-testing, selection, zoomToFit bounds and measurement waits, so no CSS workarounds are needed.
  • Layout of visible nodes only: The ELK layout receives only visible nodes and edges, so a collapsed tree stays compact. Hidden nodes keep their size and position and are measured again automatically when they are expanded.
  • Measured before hidden: Every node is visible in the initial model, so all nodes are measured before the first layout. The subtrees marked collapsed are hidden in the same transaction as that layout. This way every node has a real size for each layout pass. A node that is already hidden in the initial model is never measured. If you need that, give the node a size and mark its parent as collapsed. Otherwise the layout has no size for the node after it is expanded.
  • Layout first, then one transaction: When a subtree is toggled, the layout for the new set of visible nodes is computed first. Then the collapsed flag, the hidden flags of the subtree and the new positions are saved in a single transaction. Nodes that appear are rendered at their final position right away.
  • Fresh reads after awaited updates: Right after an awaited update, the nodes() and edges() signals may not be refreshed yet. The layout service reads the latest committed state through getModel() instead (see State Management).
  • Toggle button derived from the edges: The node template shows the toggle button when the node has an outgoing edge. The check is a computed that reads the edges() signal, so the button appears and disappears as the user draws or deletes edges. No extra flag in the node data is needed. The edgeDrawEnded and selectionRemoved events only run the layout again.

Learn more: Conditional Visibility guide →