
Syncfusion Angular Treeview
- 214 installs
- Updated August 4, 2026
- syncfusion/angular-ui-components-skills
Use syncfusion-angular-treeview for development tasks
About
syncfusion-angular-treeview: A skill for development. This provides functionality for development workflows.
- syncfusion-angular-treeview
Syncfusion Angular Treeview by the numbers
- 214 all-time installs (skills.sh)
- +11 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,879 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/syncfusion/angular-ui-components-skills --skill syncfusion-angular-treeviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 214 |
|---|---|
| Last updated | August 4, 2026 |
| Repository | syncfusion/angular-ui-components-skills ↗ |
What it does
Use syncfusion-angular-treeview for development tasks
Files
Implementing TreeView in Angular
The TreeView component displays hierarchical data in a tree-like structure with built-in support for interactive features including checkboxes, drag-and-drop, in-place editing, multi-selection, filtering, sorting, templating, and keyboard navigation. It's ideal for displaying file systems, organizational charts, category hierarchies, navigation menus, and any nested data structure.
When to Use This Skill
- Building hierarchical UIs: Displaying parent-child data relationships
- File/folder browsers: Showing directory structures with expand/collapse
- Navigation structures: Creating menu systems or site hierarchies
- Data organization: Displaying categorized or nested data
- Interactive selection: Implementing multi-selection or checkbox-based selection
- Drag-and-drop interfaces: Reorganizing tree data by dragging nodes
- Search/filter functionality: Finding nodes in large tree structures
- Customized appearances: Styling nodes per level or with templates
Navigation Guide
Choose your task below to navigate to the relevant reference documentation:
Getting Started
📄 Read: references/getting-started.md
When to read: Setting up TreeView for the first time, installing packages, importing modules, basic component initialization, CSS imports, theme selection, creating your first tree.
Data Binding & Hierarchies
📄 Read: references/data-binding.md
When to read: Connecting data sources to TreeView, binding hierarchical or self-referential data, using DataManager, loading data from remote APIs, implementing lazy loading (load on demand), dynamically updating tree data.
Node Selection & Checkboxes
📄 Read: references/node-selection.md
When to read: Enabling checkboxes, managing checkbox states (checked/unchecked/tri-state), implementing multi-selection vs single-selection, using selection events, getting selected node IDs, disabling checkboxes, removing parent checkboxes.
Node Editing & Manipulation
📄 Read: references/node-editing.md
When to read: Enabling in-place editing, adding/removing/updating nodes programmatically, validating edited text, moving nodes, using node manipulation methods (addNodes, removeNodes, updateNode, moveNodes).
Drag and Drop
📄 Read: references/drag-and-drop.md
When to read: Enabling drag-and-drop functionality, restricting drops on specific nodes, handling drag events, customizing drop indicators, preventing invalid operations.
Templating & Styling Nodes
📄 Read: references/templating.md
When to read: Creating custom node templates, using dynamic icons, styling nodes based on level, customizing expand/collapse icons, showing tooltips, handling multi-line nodes, CSS customization.
Filtering & Sorting
📄 Read: references/filtering-sorting.md
When to read: Filtering nodes by text, implementing search functionality, sorting nodes globally or per level, organizing tree display order.
Context Menu Integration
📄 Read: references/context-menu.md
When to read: Adding context menu for node operations, handling right-click actions, implementing add/edit/delete via menu, creating custom menu items.
Accessibility, Advanced Features & API Migration
📄 Read: references/accessibility-advanced.md
When to read: Keyboard navigation (arrow keys, Enter, Space), ARIA attributes and accessibility compliance, RTL (right-to-left) support, getting child nodes, accordion behavior, advanced methods, migrating from EJ1 to EJ2, performance optimization.
Quick Start Example
Here's a minimal TreeView implementation with static hierarchical data:
import { Component } from '@angular/core';
import { TreeViewModule } from '@syncfusion/ej2-angular-navigations';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-tree-view',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview
id="treeView"
[fields]="treeFields"
[allowMultiSelection]="true">
</ejs-treeview>
`
})
export class TreeViewComponent {
// Hierarchical data structure
treeData = [
{
id: '01',
name: 'Documents',
hasChild: true,
expanded: true,
subChild: [
{ id: '01-01', name: 'Work Files' },
{ id: '01-02', name: 'Personal' }
]
},
{
id: '02',
name: 'Downloads',
hasChild: true,
subChild: [
{ id: '02-01', name: 'Images' },
{ id: '02-02', name: 'Videos' }
]
}
];
// Field mapping configuration
treeFields = {
dataSource: this.treeData,
id: 'id',
text: 'name',
child: 'subChild',
hasChildren: 'hasChild',
expanded: 'expanded'
};
}Key TreeView Methods
| Method | Purpose |
|---|---|
addNodes(nodes, target?, index?, preventTargetExpand?) | Add collection of nodes at target position |
removeNodes(nodeIds) | Remove nodes by ID |
updateNode(target, newText) | Replace node text (requires allowEditing enabled) |
moveNodes(sourceNodes, target, index?, preventTargetExpand?) | Move nodes to new parent and index position |
getTreeData(nodeId?) | Get all tree data or specific node data |
getAllCheckedNodes() | Get all checked node IDs including child nodes whether loaded or not |
checkAll(nodes?) | Check all or specific nodes |
uncheckAll(nodes?) | Uncheck all or specific nodes |
beginEdit(nodeId) | Start editing a node |
expandAll(nodes?, level?, excludeHiddenNodes?, preventAnimation?) | Expand all or specific nodes, optionally by level |
collapseAll(nodes?, level?, excludeHiddenNodes?) | Collapse all or specific nodes, optionally by level |
ensureVisible(nodeId) | Scroll to make node visible |
disableNodes(nodeIds) | Disable specific nodes |
enableNodes(nodeIds) | Enable specific nodes |
getNode(nodeId) | Get HTML element of node |
destroy() | Destroy TreeView component |
Key TreeView Events
| Event | Triggered When |
|---|---|
created | TreeView component is created |
dataBound | Data source binding is complete |
dataSourceChanged | Tree data is modified (add/remove/update) |
nodeClicked | User clicks on a node |
nodeSelected | Node is selected |
nodeSelecting | Before node selection (can prevent) |
nodeChecked | Checkbox state changes |
nodeChecking | Before checkbox changes (can prevent) |
nodeExpanding | Before node expansion |
nodeExpanded | Node is expanded |
nodeCollapsing | Before node collapse |
nodeCollapsed | Node is collapsed |
nodeEditing | Before node text editing |
nodeEdited | After node text is edited |
nodeDragStart | Drag operation begins |
nodeDragging | Node is being dragged |
nodeDragStop | Drag ends before drop |
nodeDropped | Node is dropped successfully |
drawNode | Before rendering each node (customize appearance) |
keyPress | User presses keyboard key |
destroyed | TreeView component is destroyed |
Common Patterns
Pattern 1: Checkbox-Based Selection
Enable checkboxes for multi-selection with hierarchical checkbox states:
treeFields = {
dataSource: this.treeData,
id: 'id',
text: 'name',
child: 'subChild',
hasChildren: 'hasChild'
};
showCheckBox = true; // Enable checkboxes
autoCheck = true; // Parent/child auto-checkPattern 2: File Browser with Drag-and-Drop
Create an interactive file browser with node reorganization:
<ejs-treeview
[fields]="treeFields"
[allowDragAndDrop]="true"
[allowMultiSelection]="true"
(nodeDragging)="onNodeDragging($event)"
(nodeDropped)="onNodeDropped($event)">
</ejs-treeview>Pattern 3: Search and Filter
Implement real-time filtering of tree nodes:
<input
type="text"
(keyup)="filterTree($event.target.value)"
placeholder="Search nodes...">
<ejs-treeview
#treeView
[fields]="filteredFields">
</ejs-treeview>Pattern 4: Editable Tree
Enable in-place node editing with validation:
<ejs-treeview
[fields]="treeFields"
[allowEditing]="true"
(nodeEditing)="onNodeEditing($event)"
(nodeEdited)="onNodeEdited($event)">
</ejs-treeview>Key TreeView Properties
| Property | Type | Purpose |
|---|---|---|
fields | Object | Configures data source mapping (id, text, child, parentID, hasChildren, expanded, isChecked, etc.) |
dataSource | Array | Hierarchical or flat data array |
allowMultiSelection | Boolean | Enable multiple node selection |
showCheckBox | Boolean | Display checkboxes before nodes |
autoCheck | Boolean | Auto-check children when parent is checked (default: true) |
allowDragAndDrop | Boolean | Enable drag-and-drop functionality |
allowEditing | Boolean | Enable in-place node editing |
loadOnDemand | Boolean | Load child nodes only when parent expands (default: true) |
sortOrder | String | Sort order: 'Ascending', 'Descending', or 'None' |
cssClass | String | Apply custom CSS class for styling |
enableRtl | Boolean | Enable right-to-left layout support |
enablePersistence | Boolean | Save and restore TreeView state (expanded nodes, selections) |
checkedNodes | Array | Array of node IDs that should be checked |
selectedNodes | Array | Array of node IDs that should be selected |
nodeTemplate | Template | Custom template for rendering nodes |
animation | Object | Configure expand/collapse animations |
allowKeyboardNavigation | Boolean | Enable keyboard navigation (default: true) |
Common Use Cases
Organizational Chart: Display employee hierarchy with parent-child relationships and custom templates showing roles.
File/Folder Browser: Show directory structure with drag-and-drop to move files between folders.
Category Navigation: Multi-level product categories with checkboxes for filtering and multi-selection.
Settings Menu: Hierarchical preferences or configuration options organized by topic.
Knowledge Base: Nested documentation topics with search and expand/collapse navigation.
Reference Files
getting-started.md- Installation, setup, basic initializationdata-binding.md- Data sources, hierarchies, remote data, lazy loadingnode-selection.md- Checkboxes, multi-select, selection eventsnode-editing.md- Editing, adding, removing, updating nodesdrag-and-drop.md- Drag-and-drop configuration and restrictionstemplating.md- Custom templates, icons, styling, tooltipsfiltering-sorting.md- Filter, search, and sort functionalitycontext-menu.md- Context menu integration and node operationsaccessibility-advanced.md- Keyboard navigation, ARIA, RTL, advanced methods
---
Next Steps: 1. Choose your use case from the navigation guide above 2. Read the relevant reference file 3. Adapt code examples to your data structure 4. Test with your data source
Accessibility, Advanced Features & API Migration
Table of Contents
- Keyboard Navigation
- Accessibility (ARIA)
- RTL Support
- Advanced Methods
- Performance Optimization
- Accordion Tree Pattern
- EJ1 to EJ2 Migration
---
Keyboard Navigation
Navigation Keys
TreeView supports standard keyboard navigation:
| Key | Action |
|---|---|
Arrow Up | Select previous node |
Arrow Down | Select next node |
Arrow Right | Expand parent node |
Arrow Left | Collapse parent node |
Enter | Select/toggle checkbox of current node |
Space | Toggle checkbox state |
Home | Select first node |
End | Select last node |
F2 | Edit current node (if allowEditing enabled) |
Delete | Delete current node (if allowEditing enabled) |
Example: Custom Keyboard Handling
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
import { NodeKeyPressEventArgs } from '@syncfusion/ej2-navigations';
@Component({
template: `
<ejs-treeview
#treeview
[fields]="treeFields"
(keyPress)="onKeyPress($event)">
</ejs-treeview>
`
})
export class KeyboardNavigationComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
treeFields = { /* ... */ };
onKeyPress(event: NodeKeyPressEventArgs): void {
// Custom keyboard handling
if (event.key === 'Delete') {
event.preventDefault();
if (confirm('Delete this node?')) {
this.treeViewComponent?.removeNodes([event.nodeData.id]);
}
}
}
}Disable Keyboard Navigation
onKeyPress(event: NodeKeyPressEventArgs): void {
// Prevent default keyboard behavior
event.cancel = true;
}Accessibility (ARIA)
Built-in ARIA Support
TreeView automatically includes ARIA attributes for screen readers:
<!-- Example rendered TreeView with ARIA -->
<div role="tree">
<div role="treeitem" aria-expanded="true" aria-level="1">
<span>Parent Node</span>
<div role="group">
<div role="treeitem" aria-expanded="false" aria-level="2">
<span>Child Node</span>
</div>
</div>
</div>
</div>Key ARIA Attributes
role="tree"- Container rolerole="treeitem"- Individual node rolerole="group"- Child nodes containeraria-expanded="true/false"- Expansion statearia-selected="true/false"- Selection statearia-level="1,2,3..."- Node hierarchy levelaria-checked="true/false/mixed"- Checkbox state
Test with Screen Readers
// Ensure all interactive elements are keyboard accessible
// Test with:
// - NVDA (Windows)
// - JAWS (Windows)
// - VoiceOver (macOS)
// - TalkBack (Android)Configure for Accessibility
@Component({
template: `
<ejs-treeview
id="accessible-tree"
[fields]="treeFields"
[allowMultiSelection]="true"
[showCheckBox]="true"
aria-label="Hierarchical navigation tree">
</ejs-treeview>
`
})
export class AccessibleTreeComponent {
treeFields = { /* ... */ };
}RTL Support
Enable Right-to-Left Layout
import { Component } from '@angular/core';
import { TreeViewModule } from '@syncfusion/ej2-angular-navigations';
@Component({
selector: 'app-rtl-tree',
standalone: true,
imports: [TreeViewModule],
template: `
<div dir="rtl">
<ejs-treeview
[fields]="treeFields"
[enableRtl]="true">
</ejs-treeview>
</div>
`,
styles: [`
:host {
direction: rtl;
}
`]
})
export class RTLTreeComponent {
treeFields = { /* ... */ };
}RTL CSS
/* RTL specific styles */
.e-rtl .e-treeview {
direction: rtl;
text-align: right;
}
.e-rtl .e-treeview .e-list-item {
padding-left: 20px;
padding-right: 0;
}
.e-rtl .e-treeview .e-icon-expandable {
margin-left: 8px;
margin-right: 0;
}Advanced Methods
Get Tree Data
Retrieve all tree data or specific node data:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<button (click)="getTreeData()">Get All Data</button>
<button (click)="getNodeData('1')">Get Node 1</button>
<ejs-treeview #treeview [fields]="treeFields"></ejs-treeview>
`
})
export class GetDataComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
getTreeData(): void {
const allData = this.treeViewComponent?.getTreeData();
console.log('All tree data:', allData);
}
getNodeData(nodeId: string): void {
const nodeData = this.treeViewComponent?.getTreeData(nodeId);
console.log('Node data:', nodeData?.[0]);
}
}Get Child Nodes
getChildNodes(parentId: string): void {
const allData = this.treeViewComponent?.getTreeData() || [];
const children = allData.filter((node: any) => node.parentID === parentId);
console.log('Child nodes:', children);
}Ensure Visibility
Scroll tree to make node visible:
ensureNodeVisible(nodeId: string): void {
this.treeViewComponent?.ensureVisible(nodeId);
}Get Node
getNodeElement(nodeId: string): void {
const nodeElement = this.treeViewComponent?.getNode(nodeId);
console.log('Node element:', nodeElement);
}expandAll Method
Expands all the collapsed TreeView nodes. You can expand specific nodes by passing array of nodes as argument.
Method Signature:
expandAll(nodes?: string[] | Element[], level?: number, excludeHiddenNodes?: boolean, preventAnimation?: boolean): voidParameters:
| Parameter | Type | Description |
|---|---|---|
nodes (optional) | `string[] \ | Element[]` |
level (optional) | number | Expand all nodes up to the given level |
excludeHiddenNodes (optional) | boolean | Exclude hidden nodes when expanding all nodes |
preventAnimation (optional) | boolean | Prevent the expand animation when expanding all nodes |
Returns: void
Examples:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview #treeview [fields]="treeFields"></ejs-treeview>
<button (click)="expandSpecificNode()">Expand Node</button>
<button (click)="expandAll()">Expand All</button>
<button (click)="expandByLevel()">Expand Level 1</button>
`
})
export class ExpandComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
// Expand specific node
expandSpecificNode(): void {
this.treeViewComponent?.expandAll(['2']);
}
// Expand all nodes
expandAll(): void {
this.treeViewComponent?.expandAll();
}
// Expand all nodes up to level 2
expandByLevel(): void {
this.treeViewComponent?.expandAll(undefined, 2);
}
// Expand without animation
expandWithoutAnimation(): void {
this.treeViewComponent?.expandAll(undefined, undefined, false, true);
}
// Expand excluding hidden nodes
expandExcludingHidden(): void {
this.treeViewComponent?.expandAll(undefined, undefined, true);
}
}---
collapseAll Method
Collapses all the expanded TreeView nodes. You can collapse specific nodes by passing array of nodes as argument.
Method Signature:
collapseAll(nodes?: string[] | Element[], level?: number, excludeHiddenNodes?: boolean): voidParameters:
| Parameter | Type | Description |
|---|---|---|
nodes (optional) | `string[] \ | Element[]` |
level (optional) | number | Collapse all nodes up to the given level |
excludeHiddenNodes (optional) | boolean | Exclude hidden nodes when collapsing all nodes |
Returns: void
Examples:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview #treeview [fields]="treeFields"></ejs-treeview>
<button (click)="collapseSpecificNode()">Collapse Node</button>
<button (click)="collapseAll()">Collapse All</button>
<button (click)="collapseByLevel()">Collapse Level 1</button>
`
})
export class CollapseComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
// Collapse specific node
collapseSpecificNode(): void {
this.treeViewComponent?.collapseAll(['2']);
}
// Collapse all nodes
collapseAll(): void {
this.treeViewComponent?.collapseAll();
}
// Collapse all nodes up to level 2
collapseByLevel(): void {
this.treeViewComponent?.collapseAll(undefined, 2);
}
// Collapse excluding hidden nodes
collapseExcludingHidden(): void {
this.treeViewComponent?.collapseAll(undefined, undefined, true);
}
}Accordion Behavior
Only one parent expanded at a time:
@Component({
template: `
<ejs-treeview
#treeview
[fields]="treeFields"
(nodeExpanding)="onNodeExpanding($event)">
</ejs-treeview>
`
})
export class AccordionTreeComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
currentExpandedNode: string = '';
onNodeExpanding(event: any): void {
// Collapse previous expanded node
if (this.currentExpandedNode && this.currentExpandedNode !== event.nodeData.id) {
this.treeViewComponent?.collapseAll([this.currentExpandedNode]);
}
this.currentExpandedNode = event.nodeData.id;
}
}Disable/Enable Nodes
disableNodes(nodeIds: string[]): void {
this.treeViewComponent?.disableNodes(nodeIds);
}
enableNodes(nodeIds: string[]): void {
this.treeViewComponent?.enableNodes(nodeIds);
}Animation Control
Disable or Customize Animations
import { Component } from '@angular/core';
import { AnimationSettings } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview
[fields]="treeFields"
[animation]="animationSettings">
</ejs-treeview>
`
})
export class AnimationComponent {
// Disable animations for better performance
animationSettings: AnimationSettings = {
expand: { duration: 0 },
collapse: { duration: 0 }
};
// Or customize animation duration (default: 400ms)
customAnimation: AnimationSettings = {
expand: { duration: 200, easing: 'ease-out' },
collapse: { duration: 150, easing: 'ease-in' }
};
}Performance Optimization
Lazy Loading (Load on Demand)
<ejs-treeview
[fields]="treeFields"
[loadOnDemand]="true"
(nodeExpanding)="onNodeExpanding($event)">
</ejs-treeview>
onNodeExpanding(event: any): void {
// Load child nodes from server only when needed
if (!event.nodeData.childrenLoaded) {
this.loadChildNodes(event.nodeData.id);
event.nodeData.childrenLoaded = true;
}
}
loadChildNodes(parentId: string): void {
// Fetch from API
this.apiService.getChildren(parentId).subscribe((children: any[]) => {
this.treeViewComponent?.addNodes(children, parentId);
});
}Optimize Data Structure
// Use flat self-referential structure for large datasets
const flatData = [
{ id: 1, name: 'Node 1', pid: null },
{ id: 2, name: 'Node 2', pid: 1 },
// ... thousands more
];
// Not: deeply nested hierarchical structure
const badStructure = {
id: 1,
name: 'Node 1',
child: [
{ id: 2, name: 'Node 2', child: [...] }
]
};Use enablePersistence
Cache tree state in browser:
<ejs-treeview
[fields]="treeFields"
[enablePersistence]="true">
</ejs-treeview>
// Automatically saves/restores:
// - Expanded nodes
// - Selected nodes
// - Scroll positionEJ1 to EJ2 Migration
Major API Changes
| Feature | EJ1 | EJ2 |
|---|---|---|
| Module | ejTreeView | TreeViewModule |
| Template | <div id="tree"></div> | <ejs-treeview></ejs-treeview> |
| Add Node | addNode() | addNodes() |
| Remove Node | removeNode() | removeNodes() |
| Update Text | updateText() | updateNode() |
| Move Node | moveNode() | moveNodes() |
| Get Text | getText() | getNode()['text'] |
| Keyboard | allowKeyboardNavigation | keyPress event |
| Event | create | created |
| Event | ready | dataBound |
Migration Example
EJ1:
$("#tree").ejTreeView({
fields: {
dataSource: data,
id: "id",
text: "text",
parentId: "parent"
}
});
var treeObj = $("#tree").data("ejTreeView");
treeObj.addNode("NewNode", "#node1");EJ2:
@Component({
template: `
<ejs-treeview
#treeview
[fields]="treeFields">
</ejs-treeview>
`
})
export class TreeViewComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
treeFields = {
dataSource: data,
id: 'id',
text: 'text',
parentID: 'parent'
};
addNewNode(): void {
this.treeViewComponent?.addNodes(
[{ id: 'new', text: 'NewNode' }],
'node1'
);
}
}Event Migration
| EJ1 | EJ2 |
|---|---|
beforeAdd | Not applicable |
nodeAdd | dataSourceChanged |
nodeClick | nodeClicked |
nodeDelete | dataSourceChanged |
beforeDelete | Not applicable |
nodeCheckChange | nodeChecked |
beforeCheckChange | nodeChecking |
create | created |
destroy | destroyed |
---
Accordion Tree Pattern
Single Expansion with Automatic Collapse
Implement accordion behavior where only one branch can be expanded at a time. When expanding a node, all previously expanded nodes are automatically collapsed:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
import { NodeSelectEventArgs } from '@syncfusion/ej2-navigations';
@Component({
selector: 'app-accordion-tree',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview
#treevalidate
[fields]="field"
(nodeSelected)="nodeSelect($event)"
[cssClass]="'accordiontree'">
</ejs-treeview>
`,
styles: [`
:host ::ng-deep .accordiontree {
max-height: 400px;
overflow-y: auto;
}
:host ::ng-deep .accordiontree .e-level-1 {
font-weight: 600;
}
`]
})
export class AccordionTreeComponent {
@ViewChild('treevalidate') tree?: TreeViewComponent;
public continents: Object[] = [
{
code: "AF", name: "Africa", countries: [
{ code: "NGA", name: "Nigeria" },
{ code: "EGY", name: "Egypt" },
{ code: "ZAF", name: "South Africa" }
]
},
{
code: "AS", name: "Asia", countries: [
{ code: "CHN", name: "China" },
{ code: "IND", name: "India", selected: true },
{ code: "JPN", name: "Japan" }
]
},
{
code: "EU", name: "Europe", countries: [
{ code: "DNK", name: "Denmark" },
{ code: "FIN", name: "Finland" },
{ code: "AUT", name: "Austria" }
]
},
{
code: "NA", name: "North America", countries: [
{ code: "USA", name: "United States of America" },
{ code: "CUB", name: "Cuba" },
{ code: "MEX", name: "Mexico" }
]
}
];
public field: Object = {
dataSource: this.continents,
id: "code",
text: "name",
child: "countries"
};
public nodeSelect(args: NodeSelectEventArgs): void {
// Check if clicked node is a level-1 (parent) node
if (args.node.classList.contains('e-level-1')) {
// Collapse all nodes first
this.tree?.collapseAll();
// Expand only the selected node
this.tree?.expandAll([args.node]);
// Disable auto-expand on click
(this.tree as TreeViewComponent).expandOn = 'None';
}
}
}Key Features:
- Only one parent node expanded at a time
- Child nodes automatically collapse when switching parents
- Better for compact interfaces with many top-level categories
- Prevents information overload
Use Cases:
- File explorer with mutually exclusive folders
- Settings menu with collapsible sections
- Categorized navigation systems
- FAQs or help documentation
Implementation Details: 1. Listen to nodeSelected event 2. Check if clicked node is a parent (level-1) 3. Collapse all nodes with collapseAll() 4. Expand only the selected node with expandAll([args.node]) 5. Set expandOn to 'None' to prevent auto-expand
---
Best Practices:
- Use keyboard navigation for accessibility
- Enable RTL for multilingual apps
- Test with screen readers regularly
- Implement lazy loading for 1000+ nodes
- Use flat data structure for large datasets
- Optimize render performance with virtual scrolling
- Plan migration timeline for EJ1 apps
- Maintain accessibility during migrations
Context Menu Integration
Table of Contents
---
Overview
TreeView integrates with Syncfusion ContextMenu component to provide right-click functionality for node operations like add, edit, delete, and custom actions.
Setup Context Menu
Package Installation
ContextMenu is part of the navigations package, so it's already available:
npm install @syncfusion/ej2-angular-navigations --saveNo separate installation needed - both TreeView and ContextMenu are in the same package.
Basic Integration
import { Component, ViewChild } from '@angular/core';
import { TreeViewModule, TreeViewComponent, ContextMenuModule, ContextMenuComponent } from '@syncfusion/ej2-angular-navigations';
import { FormsModule } from '@angular/forms';
import { NodeClickEventArgs, BeforeOpenCloseMenuEventArgs, MenuEventArgs, MenuItemModel } from '@syncfusion/ej2-navigations';
@Component({
selector: 'app-context-menu-tree',
standalone: true,
imports: [FormsModule, TreeViewModule, ContextMenuModule],
template: `
<div id="treeparent">
<ejs-treeview
#treevalidate
id="tree"
[fields]="field"
(nodeClicked)="nodeclicked($event)">
</ejs-treeview>
<ejs-contextmenu
#contentmenutree
id="contentmenutree"
target="#tree"
[items]="menuItems"
(beforeOpen)="beforeopen($event)"
(select)="menuclick($event)">
</ejs-contextmenu>
</div>
`
})
export class ContextMenuTreeComponent {
@ViewChild('treevalidate') treevalidate?: TreeViewComponent;
@ViewChild('contentmenutree') contentmenutree?: ContextMenuComponent;
public hierarchicalData: Object[] = [
{
id: '01', name: 'Local Disk (C:)', expanded: true, hasAttribute: { class: 'remove rename' },
subChild: [
{
id: '01-01', name: 'Program Files',
subChild: [
{ id: '01-01-01', name: 'Windows NT' },
{ id: '01-01-02', name: 'Windows Mail' },
{ id: '01-01-03', name: 'Windows Photo Viewer' },
]
},
{
id: '01-02', name: 'Users', expanded: true,
subChild: [
{ id: '01-02-01', name: 'Smith' },
{ id: '01-02-02', name: 'Public' },
{ id: '01-02-03', name: 'Admin' },
]
},
]
},
{
id: '02', name: 'Local Disk (D:)', hasAttribute: { class: 'remove' },
subChild: [
{
id: '02-01', name: 'Personals',
subChild: [
{ id: '02-01-01', name: 'My photo.png' },
{ id: '02-01-02', name: 'Rental document.docx' },
{ id: '02-01-03', name: 'Pay slip.pdf' },
]
}
]
}
];
public field: Object = { dataSource: this.hierarchicalData, id: 'id', text: 'name', child: 'subChild', htmlAttributes: 'hasAttribute' };
public nodeclicked(args: NodeClickEventArgs): void {
if (args.event.which === 3) {
(this.treevalidate as TreeViewComponent).selectedNodes = [args.node.getAttribute('data-uid') as string];
}
}
public menuItems: MenuItemModel[] = [
{ text: 'Add New Item' },
{ text: 'Rename Item' },
{ text: 'Remove Item' }
];
public index: number = 1;
public menuclick(args: MenuEventArgs): void {
let targetNodeId: string = this.treevalidate?.selectedNodes[0] as string;
if (args.item.text == "Add New Item") {
let nodeId: string = "tree_" + this.index;
let item: { [key: string]: Object } = { id: nodeId, name: "New Folder" };
this.treevalidate?.addNodes([item], targetNodeId, null as any);
this.index++;
this.treevalidate?.beginEdit(nodeId);
}
else if (args.item.text == "Remove Item") {
this.treevalidate?.removeNodes([targetNodeId]);
}
else if (args.item.text == "Rename Item") {
this.treevalidate?.beginEdit(targetNodeId);
}
}
public beforeopen(args: BeforeOpenCloseMenuEventArgs): void {
let targetNodeId: string = this.treevalidate?.selectedNodes[0] as string;
let targetNode: Element = document.querySelector('[data-uid="' + targetNodeId + '"]') as Element;
if (targetNode?.classList.contains('remove')) {
this.contentmenutree?.enableItems(['Remove Item'], false);
} else {
this.contentmenutree?.enableItems(['Remove Item'], true);
}
if (targetNode?.classList.contains('rename')) {
this.contentmenutree?.enableItems(['Rename Item'], false);
} else {
this.contentmenutree?.enableItems(['Rename Item'], true);
}
}
}Menu Items & Events
Customized Menu Items
contextMenuItems: MenuItemModel[] = [
{
text: 'Expand All',
iconCss: 'e-icons e-chevron-down',
id: 'expand-all'
},
{
text: 'Collapse All',
iconCss: 'e-icons e-chevron-up',
id: 'collapse-all'
},
{ separator: true },
{
text: 'Properties',
iconCss: 'e-icons e-settings',
id: 'properties'
}
];Submenu Items
contextMenuItems: MenuItemModel[] = [
{
text: 'File',
items: [
{ text: 'New Folder', id: 'new-folder' },
{ text: 'Import', id: 'import' },
{ text: 'Export', id: 'export' }
]
},
{
text: 'Edit',
items: [
{ text: 'Rename', id: 'rename' },
{ text: 'Move', id: 'move' },
{ text: 'Delete', id: 'delete' }
]
}
];Menu Item Events
onContextMenuSelect(event: MenuEventArgs): void {
const menuItem = event.item;
console.log('Selected menu item:', menuItem?.text);
console.log('Menu item ID:', menuItem?.id);
// Check if item is disabled
if (menuItem?.disabled) {
return;
}
// Handle the menu action
this.handleMenuAction(menuItem?.id as string);
}Node Operations
Create Folder via Menu
private createFolder(): void {
const folderName = prompt('Enter folder name:');
if (!folderName) return;
const newFolder = {
id: Date.now(),
name: folderName,
pid: this.selectedNode?.id,
hasChild: true,
expanded: true
};
this.treeViewComponent?.addNodes([newFolder], this.selectedNode?.id);
}Rename Node via Menu
private renameNode(): void {
this.treeViewComponent?.beginEdit(this.selectedNode?.id);
}Delete with Confirmation
private deleteWithConfirm(): void {
const message = `Delete "${this.selectedNode?.name}"?`;
if (confirm(message)) {
this.treeViewComponent?.removeNodes([this.selectedNode?.id]);
console.log('Node deleted successfully');
}
}Move to Different Parent
private moveNode(): void {
const newParentId = prompt('Enter target parent ID:');
if (newParentId) {
this.treeViewComponent?.moveNodes(
[this.selectedNode?.id],
newParentId
);
}
}Dynamic Menus
Show Different Menu Items Based on Node Type
onContextMenuOpen(event: BeforeOpenCloseMenuEventArgs): void {
const target = event.event?.target as HTMLElement;
const treeNode = target?.closest('.e-list-item');
const nodeId = treeNode?.getAttribute('data-uid');
this.selectedNode = this.treeViewComponent?.getTreeData(nodeId)?.[0];
// Update menu based on node type
this.updateContextMenu();
}
updateContextMenu(): void {
if (this.selectedNode?.isFile) {
// Show file-specific menu items
this.contextMenuItems = [
{ text: 'Open', id: 'open' },
{ text: 'Edit', id: 'edit' },
{ separator: true },
{ text: 'Delete', id: 'delete' }
];
} else {
// Show folder-specific menu items
this.contextMenuItems = [
{ text: 'Add', id: 'add' },
{ text: 'Rename', id: 'rename' },
{ separator: true },
{ text: 'Delete', id: 'delete' }
];
}
}Conditional Menu Items
contextMenuItems: MenuItemModel[] = [];
onContextMenuOpen(event: BeforeOpenCloseMenuEventArgs): void {
// Determine node type
const isFolder = this.selectedNode?.hasChild;
this.contextMenuItems = [
{ text: 'Rename', id: 'rename' },
...(isFolder ? [{ text: 'Add Item', id: 'add' }] : []),
{ separator: true },
{ text: 'Delete', id: 'delete' }
];
// Refresh context menu
this.contextMenuComponent?.refresh();
}Disabling Items
Disable Based on Conditions
onContextMenuOpen(event: BeforeOpenCloseMenuEventArgs): void {
// Disable certain actions based on user permissions
const canEdit = this.userCanEdit(this.selectedNode);
const canDelete = this.userCanDelete(this.selectedNode);
this.contextMenuItems = [
{ text: 'Edit', id: 'edit', disabled: !canEdit },
{ text: 'Delete', id: 'delete', disabled: !canDelete }
];
this.contextMenuComponent?.refresh();
}
userCanEdit(node: any): boolean {
return node?.editable !== false;
}
userCanDelete(node: any): boolean {
return node?.protected !== true;
}Disable Paste When Clipboard Empty
onContextMenuOpen(event: BeforeOpenCloseMenuEventArgs): void {
const hasClipboard = localStorage.getItem('clipboard') !== null;
this.contextMenuItems = [
{ text: 'Copy', id: 'copy' },
{ text: 'Cut', id: 'cut' },
{ text: 'Paste', id: 'paste', disabled: !hasClipboard }
];
this.contextMenuComponent?.refresh();
}Disable for Root Nodes
onContextMenuOpen(event: BeforeOpenCloseMenuEventArgs): void {
const isRootNode = this.selectedNode?.level === 0;
this.contextMenuItems = [
{ text: 'Add', id: 'add' },
{ text: 'Delete', id: 'delete', disabled: isRootNode },
{ text: 'Move', id: 'move', disabled: isRootNode }
];
this.contextMenuComponent?.refresh();
}---
Best Practices:
- Always confirm destructive actions (delete, cut)
- Disable menu items when actions aren't applicable
- Provide visual feedback for completed operations
- Use keyboard shortcuts for frequent operations
- Group related menu items with separators
- Handle copy/paste using browser clipboard API or localStorage
- Show context menu only on valid targets
- Provide undo functionality for reversible operations
Data Binding in TreeView
Table of Contents
- Overview
- Local Data Binding
- Hierarchical vs Self-Referential
- Remote Data (DataManager)
- Load On Demand (Lazy Loading)
- Dynamic Data Updates
- Events
---
Overview
TreeView supports binding to multiple data source types:
- Local arrays - Static JavaScript object arrays
- Hierarchical data - Nested objects with child arrays
- Self-referential data - Flat arrays with parent-child IDs
- Remote data - DataManager with OData, Web API, or URLs
- Dynamic loading - Load on demand for performance
Field Mapping Options
The fields property configures how TreeView maps your data to UI elements:
treeFields = {
dataSource: dataArray, // Data array or DataManager
id: 'id', // Unique identifier field
text: 'name', // Display text field
child: 'children', // Child array field (hierarchical only)
parentID: 'parentId', // Parent reference field (self-referential only)
hasChildren: 'hasChild', // Boolean indicating child existence
expanded: 'isExpanded', // Boolean for initial expand state
selected: 'isSelected', // Boolean for initial selection
isChecked: 'checked', // Boolean for initial checkbox state
iconCss: 'icon', // CSS class for custom icon
imageUrl: 'imageUrl', // Image URL for node
tooltip: 'tooltipText', // Tooltip text
htmlAttribute: 'htmlAttr', // HTML attributes to apply
linkAttribute: 'linkAttr', // Link-related attributes
imageAttribute: 'imgAttr', // Image attributes
tableName: null, // For multi-table datasources
query: null // OData query parameter
};Field Selection Guide:
- Use
childfor hierarchical (nested) data - Use
parentIDfor flat (self-referential) data - Combine
idandparentIDwithhasChildrenfor efficiency - Map
isChecked,selected,expandedfor initial states - Use custom fields for icons, images, and attributes
Local Data Binding
Hierarchical Data
Nested data structure with child arrays:
import { Component } from '@angular/core';
import { TreeViewModule } from '@syncfusion/ej2-angular-navigations';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-hierarchical-data',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview [fields]="treeFields"></ejs-treeview>
`
})
export class HierarchicalDataComponent {
treeFields = {
dataSource: [
{
code: 'AF',
name: 'Africa',
countries: [
{ code: 'NGA', name: 'Nigeria' },
{ code: 'EGY', name: 'Egypt' },
{ code: 'ZAF', name: 'South Africa' }
]
},
{
code: 'AS',
name: 'Asia',
expanded: true,
countries: [
{ code: 'CHN', name: 'China' },
{ code: 'IND', name: 'India' },
{ code: 'JPN', name: 'Japan' }
]
}
],
id: 'code',
text: 'name',
child: 'countries'
};
}Self-Referential Data
Flat array with parentID relationships:
export class SelfReferentialDataComponent {
treeFields = {
dataSource: [
{ id: 1, name: 'Parent 1', hasChild: true, expanded: true },
{ id: 2, pid: 1, name: 'Child 1' },
{ id: 3, pid: 1, name: 'Child 2' },
{ id: 4, pid: 1, name: 'Child 3' },
{ id: 5, name: 'Parent 2', hasChild: true },
{ id: 6, pid: 5, name: 'Child 2.1' },
{ id: 7, pid: 5, name: 'Child 2.2' }
],
id: 'id',
parentID: 'pid',
text: 'name',
hasChildren: 'hasChild',
expanded: 'expanded'
};
}Hierarchical vs Self-Referential
| Aspect | Hierarchical | Self-Referential |
|---|---|---|
| Data structure | Nested objects | Flat array |
| Use case | Org charts, categories | Databases, APIs |
| Performance | Fast for small data | Better for large data |
| Field mapping | Uses child property | Uses parentID property |
| Parent reference | Implicit in nesting | Explicit parentID field |
Choose hierarchical for tightly-coupled data structures you control.
Choose self-referential for database results or when parent-child is determined at runtime.
Remote Data (DataManager)
OData Service
import { DataManager, ODataAdaptor } from '@syncfusion/ej2-data';
export class RemoteDataComponent {
treeFields = {
dataSource: new DataManager({
url: 'url',
adaptor: new ODataAdaptor(),
crossDomain: true
}),
id: 'EmployeeID',
text: 'FirstName',
parentID: 'ReportsTo',
hasChildren: true
};
}Web API (RESTful Service)
import { DataManager, UrlAdaptor } from '@syncfusion/ej2-data';
export class WebApiComponent {
treeFields = {
dataSource: new DataManager({
url: '/api/employees',
adaptor: new UrlAdaptor(),
crossDomain: true,
headers: [
{ Authorization: 'Bearer token_here' } // For authentication
]
}),
id: 'id',
parentID: 'parentId',
text: 'name',
hasChildren: 'hasChildren'
};
}JSON URL
export class JsonUrlComponent {
treeFields = {
dataSource: new DataManager({
url: 'assets/data/tree-data.json'
}),
id: 'id',
text: 'name',
child: 'children'
};
}GraphQL Data Source
import { DataManager, GraphQLAdaptor } from '@syncfusion/ej2-data';
export class GraphQLComponent {
treeFields = {
dataSource: new DataManager({
url: 'url',
adaptor: new GraphQLAdaptor(),
query: `query GetEmployees {
employees {
id
name
manager_id
children {
id
name
}
}
}`
}),
id: 'id',
text: 'name',
parentID: 'manager_id'
};
}Custom Data Adapter
import { DataManager, Adaptor } from '@syncfusion/ej2-data';
class CustomAdaptor extends Adaptor {
processQuery(dm: DataManager, query: any): Promise<any> {
// Custom data fetching logic
return fetch(`/api/data?skip=${query.skip}&take=${query.take}`)
.then(res => res.json());
}
}
export class CustomDataComponent {
treeFields = {
dataSource: new DataManager({
adaptor: new CustomAdaptor()
}),
id: 'id',
text: 'name'
};
}Load On Demand (Lazy Loading)
By default, TreeView loads only first-level nodes initially. Child nodes load when parent expands:
import { Component, ViewChild } from '@angular/core';
import { TreeViewModule, TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
selector: 'app-lazy-loading',
standalone: true,
imports: [TreeViewModule],
template: `
<ejs-treeview
#treeview
[fields]="treeFields"
[loadOnDemand]="true"
(nodeExpanding)="onNodeExpanding($event)">
</ejs-treeview>
`
})
export class LazyLoadingComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
treeFields = {
dataSource: [
{ id: 1, name: 'Parent 1', hasChild: true },
{ id: 2, name: 'Parent 2', hasChild: true }
],
id: 'id',
text: 'name',
parentID: 'pid'
};
onNodeExpanding(event: any): void {
// Load child nodes dynamically
if (event.nodeData.id === 1) {
const childNodes = [
{ id: 3, pid: 1, name: 'Child 1.1' },
{ id: 4, pid: 1, name: 'Child 1.2' }
];
this.treeViewComponent?.addNodes(childNodes, event.nodeData.id);
}
}
}To disable lazy loading and render entire tree at once:
loadOnDemand = false; // Load all nodes upfrontNote: Disabling lazy loading may impact performance with large datasets.
Dynamic Data Updates
Adding Nodes
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview #treeview [fields]="treeFields"></ejs-treeview>
<button (click)="addNewNode()">Add Node</button>
`
})
export class AddNodeComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
treeFields = { /* ... */ };
addNewNode(): void {
const newNode = { id: 10, name: 'New Node', pid: 1 };
this.treeViewComponent?.addNodes([newNode]);
}
}Updating Nodes
updateNodeData(): void {
const updatedNode = { id: 2, name: 'Updated Name', pid: 1 };
this.treeViewComponent?.updateNode([updatedNode]);
}Removing Nodes
removeNode(): void {
this.treeViewComponent?.removeNodes(['2']); // Pass node IDs to remove
}Moving Nodes
moveNodeToParent(): void {
const nodeIds = ['3', '4']; // Nodes to move
const targetParent = '2'; // New parent node ID
this.treeViewComponent?.moveNodes(nodeIds, targetParent);
}Events
dataBound Event
Triggered when data source is fully loaded:
<ejs-treeview
[fields]="treeFields"
(dataBound)="onDataBound($event)">
</ejs-treeview>
onDataBound(event: any): void {
console.log('Tree data loaded successfully');
console.log('Total nodes:', this.treeViewComponent?.getTreeData().length);
}nodeExpanding Event
Triggered before node expansion (useful for lazy loading):
(nodeExpanding)="onNodeExpanding($event)"
onNodeExpanding(event: any): void {
console.log('Expanding node:', event.nodeData.name);
}dataSourceChanged Event
Triggered after data modifications (add, remove, update):
(dataSourceChanged)="onDataSourceChanged($event)"
onDataSourceChanged(event: any): void {
console.log('Data source updated');
}---
Best Practices:
- Use hierarchical data for UI-controlled relationships
- Use self-referential for database-driven trees
- Enable lazy loading for trees with 1000+ nodes
- Use DataManager for real-time remote data
- Handle nodeExpanding event for dynamic child loading
Drag and Drop in TreeView
Table of Contents
---
Overview
TreeView supports drag-and-drop functionality for reorganizing nodes within the tree structure. Users can drag nodes to change parent-child relationships or reorder siblings, with visual feedback showing where nodes will be dropped.
Enable Drag and Drop
Basic Setup
import { Component } from '@angular/core';
import { TreeViewModule } from '@syncfusion/ej2-angular-navigations';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-dnd-tree',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview
[fields]="treeFields"
[allowDragAndDrop]="true">
</ejs-treeview>
`
})
export class DragDropTreeComponent {
treeFields = {
dataSource: [
{
id: 1,
name: 'Music',
hasChild: true,
expanded: true,
child: [
{ id: 2, pid: 1, name: 'Gouttes.mp3' }
]
},
{
id: 3,
name: 'Videos',
hasChild: true,
child: [
{ id: 4, pid: 3, name: 'Naturals.mp4' },
{ id: 5, pid: 3, name: 'Wild.mpeg' }
]
}
],
id: 'id',
parentID: 'pid',
text: 'name',
hasChildren: 'hasChild'
};
}With Multi-Selection
<ejs-treeview
[fields]="treeFields"
[allowDragAndDrop]="true"
[allowMultiSelection]="true">
</ejs-treeview>Drag multiple selected nodes at once by dragging any selected node.
Drop Indicators
TreeView shows visual indicators when dragging nodes:
| Icon | Meaning |
|---|---|
| Plus icon | Dragged node will be added as child of target node |
| Between icon | Dragged node will be added as sibling (same level) |
| Minus/Restrict icon | Dragged node cannot be dropped at this location |
Indicators automatically appear as you hover over nodes during drag operation.
Drag and Drop Events
Drag and Drop Event Properties
All drag-and-drop events provide these properties:
interface DragAndDropEventArgs {
draggedNodeData: any; // Data of the node being dragged
draggedNode: HTMLElement; // HTML element being dragged
droppedNodeData?: any; // Data of target node (null if drop on empty area)
droppedNode?: HTMLElement; // HTML element of target node
dropIndicator?: string; // 'plus' (as child) | 'between' (as sibling) | 'restrict' (not allowed)
event?: MouseEvent; // Original mouse event
cancel?: boolean; // Set to true to prevent default action
}nodeDragStart Event
Triggered when drag operation begins. Use to prevent dragging certain nodes:
import { Component } from '@angular/core';
import { DragAndDropEventArgs } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview
[fields]="treeFields"
[allowDragAndDrop]="true"
(nodeDragStart)="onNodeDragStart($event)">
</ejs-treeview>
`
})
export class DragStartComponent {
onNodeDragStart(event: DragAndDropEventArgs): void {
console.log('Dragging node:', event.draggedNodeData.name);
// Prevent dragging specific nodes
if (event.draggedNodeData.id === '1') {
event.cancel = true; // Can't drag root node
}
// Check if dragged element is from this tree
if (event.event?.ctrlKey) {
console.log('Dragging with Ctrl key');
}
}
}nodeDragging Event
Triggered while node is being dragged. Use for visual feedback:
onNodeDragging(event: DragAndDropEventArgs): void {
console.log('Dragging over node:', event.droppedNodeData?.name);
// Customize drag element appearance
const dragElement = event.draggedNode as HTMLElement;
dragElement.style.opacity = '0.5';
}nodeDragStop Event
Triggered when drag ends but before drop completes. Use for validation:
onNodeDragStop(event: DragAndDropEventArgs): void {
console.log('Dropped on:', event.droppedNodeData?.name);
// Prevent certain drops
if (event.droppedNodeData?.isFolder === false) {
event.cancel = true; // Can't drop files into files
}
}nodeDropped Event
Triggered after successful drop. Use for post-processing:
onNodeDropped(event: DragAndDropEventArgs): void {
console.log('Node successfully dropped');
console.log('New parent:', event.droppedNodeData?.name);
// Update server or perform operations
this.saveNodeMove(event.draggedNodeData.id, event.droppedNodeData?.id);
}
saveNodeMove(nodeId: string, newParentId: string): void {
// Send change to server
}Restricting Operations
Restrict Drops on Specific Nodes
import { Component } from '@angular/core';
@Component({
template: `
<ejs-treeview
[fields]="treeFields"
[allowDragAndDrop]="true"
(nodeDragging)="onNodeDragging($event)"
(nodeDragStop)="onNodeDragStop($event)">
</ejs-treeview>
`
})
export class RestrictDropComponent {
treeFields = {
dataSource: [
{ id: 1, name: 'Music', type: 'folder', hasChild: true },
{ id: 2, pid: 1, name: 'Song1.mp3', type: 'file' },
{ id: 3, name: 'Document.txt', type: 'file' }
],
id: 'id',
parentID: 'pid',
text: 'name'
};
onNodeDragging(event: DragAndDropEventArgs): void {
// Show restrict icon when hovering over files (not folders)
if (event.droppedNodeData?.type === 'file') {
event.dropIndicator = 'restrict'; // Shows minus/restrict icon
}
}
onNodeDragStop(event: DragAndDropEventArgs): void {
// Prevent dropping into files
if (event.droppedNodeData?.type === 'file') {
event.cancel = true;
}
}
}Allow Drop Only on Folders
treeFields = {
dataSource: [
{ id: 1, name: 'Folder 1', isFolder: true, hasChild: true },
{ id: 2, pid: 1, name: 'File 1.txt', isFolder: false },
{ id: 3, name: 'Folder 2', isFolder: true }
],
id: 'id',
parentID: 'pid',
text: 'name'
};
onNodeDragStop(event: DragAndDropEventArgs): void {
// Only allow dropping on folders
if (event.droppedNodeData && !event.droppedNodeData.isFolder) {
event.cancel = true;
}
}Prevent Dragging into Own Children
onNodeDragStop(event: DragAndDropEventArgs): void {
const draggedId = event.draggedNodeData.id;
const targetId = event.droppedNodeData?.id;
// Check if target is descendant of dragged node
if (this.isDescendant(draggedId, targetId)) {
event.cancel = true; // Prevent creating circular hierarchy
alert('Cannot move node into its own children');
}
}
isDescendant(parentId: string, potentialChildId: string): boolean {
const allNodes = this.treeViewComponent?.getTreeData() || [];
const findDescendants = (id: string): string[] => {
const children = allNodes.filter((n: any) => n.parentID === id);
return [
...children.map((c: any) => c.id),
...children.flatMap((c: any) => findDescendants(c.id))
];
};
return findDescendants(parentId).includes(potentialChildId);
}Limit Depth of Tree
onNodeDragStop(event: DragAndDropEventArgs): void {
const maxDepth = 5;
const targetDepth = this.getNodeDepth(event.droppedNodeData?.id);
if (targetDepth >= maxDepth - 1) {
event.cancel = true;
alert(`Maximum nesting depth (${maxDepth}) reached`);
}
}
getNodeDepth(nodeId: string, depth = 0): number {
const allNodes = this.treeViewComponent?.getTreeData() || [];
const node = allNodes.find((n: any) => n.id === nodeId);
if (!node || !node.parentID) return depth;
return this.getNodeDepth(node.parentID, depth + 1);
}Copy vs Move
Default: Move Operation
By default, drag-and-drop moves nodes:
// Drag node 2 under node 3
// Node 2 moves from its current parent to node 3Implement Copy Behavior
Use events to implement copy instead of move:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview
#treeview
[fields]="treeFields"
[allowDragAndDrop]="true"
(nodeDropped)="onNodeDropped($event)">
</ejs-treeview>
`
})
export class CopyBehaviorComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
private draggedNodeId: string = '';
onNodeDropped(event: DragAndDropEventArgs): void {
// Save original node
const allNodes = this.treeViewComponent?.getTreeData() || [];
const originalNode = allNodes.find((n: any) => n.id === this.draggedNodeId);
if (originalNode) {
// Create copy with new ID
const copyNode = {
...originalNode,
id: 'copy_' + Date.now(),
name: originalNode.name + ' (Copy)'
};
// Add copy to new location
this.treeViewComponent?.addNodes([copyNode], event.droppedNodeData?.id);
}
}
}Conditional Copy vs Move
onNodeDropped(event: DragAndDropEventArgs): void {
// Check if Ctrl key was held (copy mode)
const isCopyMode = (event as any).event?.ctrlKey;
if (isCopyMode) {
// Implement copy behavior
this.copyNode(event.draggedNodeData, event.droppedNodeData?.id);
} else {
// Default move behavior (already done by TreeView)
console.log('Node moved');
}
}
copyNode(sourceNode: any, targetParentId: string): void {
const copyNode = {
...sourceNode,
id: 'copy_' + Date.now(),
name: sourceNode.name + ' (Copy)'
};
this.treeViewComponent?.addNodes([copyNode], targetParentId);
}---
Best Practices:
- Always validate drag-and-drop operations
- Prevent circular hierarchies (dragging parent into child)
- Restrict drops on leaf nodes if appropriate for your use case
- Provide visual feedback via drop indicators
- Log or sync changes to server after drop
- Implement depth limits to prevent deep nesting issues
Filtering & Sorting TreeView
Table of Contents
---
Overview
TreeView provides built-in sorting and filtering capabilities to organize and search hierarchical data effectively.
Sorting Nodes
Sort Entire Tree
Use sortOrder property to sort all nodes:
import { Component } from '@angular/core';
import { TreeViewModule } from '@syncfusion/ej2-angular-navigations';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-sorted-tree',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview
[fields]="treeFields"
[sortOrder]="'Ascending'">
</ejs-treeview>
`
})
export class SortedTreeComponent {
treeFields = {
dataSource: [
{ id: 1, name: 'Zebra', hasChild: true },
{ id: 2, name: 'Apple', hasChild: true },
{ id: 3, name: 'Mango' }
],
id: 'id',
text: 'name'
};
}Sort Order Options
sortOrder = 'None'; // No sorting (default)
sortOrder = 'Ascending'; // A-Z, 0-9
sortOrder = 'Descending'; // Z-A, 9-0Sort by Specific Property
import { DataManager, Query, Predicate } from '@syncfusion/ej2-data';
export class SortByPropertyComponent {
treeFields = {
dataSource: new DataManager(this.treeData).executeLocal(
new Query().sortBy('priority', false)
),
id: 'id',
text: 'name'
};
treeData = [
{ id: 1, name: 'High Priority', priority: 1 },
{ id: 2, name: 'Low Priority', priority: 3 },
{ id: 3, name: 'Medium Priority', priority: 2 }
];
}Level-Wise Sorting
Sort only specific tree levels:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
import { DataManager, Query } from '@syncfusion/ej2-data';
@Component({
template: `
<button (click)="sortFirstLevel()">Sort Parent Nodes</button>
<ejs-treeview #treeview [fields]="treeFields" (nodeExpanding)="onNodeExpanding($event)">
</ejs-treeview>
`
})
export class LevelWiseSortComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
treeFields = {
dataSource: [
{ id: 1, name: 'India', hasChild: true, expanded: true },
{ id: 2, name: 'Brazil', hasChild: true },
{ id: 3, name: 'Africa', hasChild: true }
],
id: 'id',
parentID: 'pid',
text: 'name'
};
sortFirstLevel(): void {
// Sort only root level
const sortedData = new DataManager(this.treeFields.dataSource).executeLocal(
new Query().sortBy('name')
);
this.treeFields.dataSource = sortedData;
}
onNodeExpanding(event: any): void {
// Sort children on expansion
const allData = this.treeViewComponent?.getTreeData() || [];
const childData = allData.filter((item: any) =>
item.parentID === event.nodeData.id
);
if (childData.length > 1) {
const sortedChildren = new DataManager(childData).executeLocal(
new Query().sortBy('name')
);
// Replace unsorted children with sorted
}
}
}Filtering Nodes
Basic Filter
Filter nodes by text:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent, FieldsSettingsModel } from '@syncfusion/ej2-angular-navigations';
import { DataManager, Query, Predicate } from '@syncfusion/ej2-data';
@Component({
selector: 'app-filter-tree',
template: `
<input
type="text"
placeholder="Search nodes..."
(keyup)="onFilterChange($event.target.value)"
class="filter-input">
<ejs-treeview #treeview [fields]="filteredFields"></ejs-treeview>
`,
styles: [`
.filter-input {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
`]
})
export class FilterTreeComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
originalData = [
{ id: 1, name: 'Australia', hasChild: true },
{ id: 2, pid: 1, name: 'New South Wales' },
{ id: 3, pid: 1, name: 'Victoria' },
{ id: 4, name: 'Brazil', hasChild: true },
{ id: 5, pid: 4, name: 'Paraná' }
];
filteredFields: any = {
dataSource: this.originalData,
id: 'id',
parentID: 'pid',
text: 'name'
};
onFilterChange(filterText: string): void {
if (!filterText) {
this.filteredFields.dataSource = this.originalData;
return;
}
// Filter nodes containing search text
const filtered = new DataManager(this.originalData).executeLocal(
new Query().where('name', 'contains', filterText, true)
);
// Include parent nodes of matching items
const result = this.getNodeWithParents(filtered);
this.filteredFields.dataSource = result;
}
getNodeWithParents(filteredNodes: any[]): any[] {
const nodeIds = filteredNodes.map((n: any) => n.id);
const withParents = [...filteredNodes];
// Add parent nodes for context
filteredNodes.forEach((node: any) => {
this.getAncestors(node.parentID, withParents, nodeIds);
});
return withParents;
}
getAncestors(parentId: any, result: any[], nodeIds: any[]): void {
if (!parentId) return;
const parent = this.originalData.find((n: any) => n.id === parentId);
if (parent && !nodeIds.includes(parent.id)) {
result.push(parent);
nodeIds.push(parent.id);
this.getAncestors(parent.parentID, result, nodeIds);
}
}
}Case-Insensitive Filter
onFilterChange(filterText: string): void {
const filtered = new DataManager(this.originalData).executeLocal(
new Query().where('name', 'contains', filterText.toLowerCase(), true)
);
this.filteredFields.dataSource = filtered;
}Filter by Multiple Criteria
filterByMultipleCriteria(filterText: string, status: string): void {
const predicate = new Predicate('name', 'contains', filterText)
.and(new Predicate('status', '==', status));
const filtered = new DataManager(this.originalData).executeLocal(
new Query().where(predicate)
);
this.filteredFields.dataSource = filtered;
}Search Functionality
Real-Time Search with Highlight
@Component({
template: `
<input
type="text"
placeholder="Search..."
(keyup)="onSearch($event.target.value)"
class="search-input">
<div class="result-count" *ngIf="searchResults.length > 0">
Found: {{searchResults.length}} results
</div>
<ejs-treeview #treeview [fields]="treeFields"></ejs-treeview>
`
})
export class SearchComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
originalData = [];
searchResults: any[] = [];
onSearch(query: string): void {
if (query.length < 2) {
// Reset to original data
return;
}
this.searchResults = this.performSearch(query);
this.highlightResults();
}
performSearch(query: string): any[] {
const allNodes = this.treeViewComponent?.getTreeData() || [];
return allNodes.filter((node: any) =>
node.name.toLowerCase().includes(query.toLowerCase())
);
}
highlightResults(): void {
this.treeViewComponent?.element?.querySelectorAll('.e-list-text').forEach(element => {
element.classList.remove('search-highlight');
});
this.searchResults.forEach(result => {
const nodeElement = this.treeViewComponent?.element?.querySelector(
`[data-uid="${result.id}"] .e-list-text`
);
if (nodeElement) {
nodeElement.classList.add('search-highlight');
}
});
}
}
/* CSS */
.search-highlight {
background-color: yellow !important;
font-weight: bold;
}
.result-count {
padding: 8px;
font-size: 12px;
color: #666;
}Search with Suggestion
@Component({
template: `
<div class="search-container">
<input
type="text"
placeholder="Type to search..."
(keyup)="onSearchInput($event)"
class="search-input">
<ul *ngIf="suggestions.length > 0" class="suggestions">
<li *ngFor="let suggestion of suggestions"
(click)="selectSuggestion(suggestion)">
{{suggestion.name}}
</li>
</ul>
</div>
<ejs-treeview #treeview [fields]="treeFields"></ejs-treeview>
`
})
export class SearchSuggestionComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
suggestions: any[] = [];
onSearchInput(event: any): void {
const query = event.target.value;
if (query.length < 1) {
this.suggestions = [];
return;
}
const allNodes = this.treeViewComponent?.getTreeData() || [];
this.suggestions = allNodes
.filter((n: any) => n.name.toLowerCase().includes(query.toLowerCase()))
.slice(0, 10); // Limit to 10 suggestions
}
selectSuggestion(suggestion: any): void {
this.treeViewComponent?.ensureVisible(suggestion.id);
this.treeViewComponent?.selectAll([suggestion.id]);
this.suggestions = [];
}
}Dynamic Filtering
Filter on Property Change
@Component({
template: `
<label>
Show only active items:
<input type="checkbox" (change)="toggleActiveOnly($event)">
</label>
<ejs-treeview [fields]="treeFields"></ejs-treeview>
`
})
export class DynamicFilterComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
originalData = [
{ id: 1, name: 'Item 1', status: 'active' },
{ id: 2, name: 'Item 2', status: 'inactive' }
];
treeFields: any = { /* ... */ };
toggleActiveOnly(event: any): void {
if (event.target.checked) {
const filtered = this.originalData.filter((n: any) => n.status === 'active');
this.treeFields.dataSource = filtered;
} else {
this.treeFields.dataSource = this.originalData;
}
}
}Performance Tips
Debounce Filter Input
import { Component } from '@angular/core';
@Component({
template: `
<input
type="text"
(keyup)="onFilterInput($event)"
placeholder="Search (debounced)...">
`
})
export class DebounceFilterComponent {
private filterTimeout: any;
onFilterInput(event: any): void {
// Clear previous timeout
if (this.filterTimeout) {
clearTimeout(this.filterTimeout);
}
// Set new timeout for 300ms after user stops typing
this.filterTimeout = setTimeout(() => {
this.applyFilter(event.target.value);
}, 300);
}
applyFilter(filterText: string): void {
// Perform actual filtering after debounce
}
}Alternative using RxJS:
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
export class RxJSDebounceComponent {
private filterSubject = new Subject<string>();
ngOnInit(): void {
this.filterSubject
.pipe(
debounceTime(300),
distinctUntilChanged()
)
.subscribe((filterText: string) => {
this.applyFilter(filterText);
});
}
onFilterInput(event: any): void {
this.filterSubject.next(event.target.value);
}
applyFilter(filterText: string): void {
// Perform filtering
}
}Lazy Load Filtered Results
loadMoreResults(): void {
// Load next batch of filtered results
const nextBatch = this.allFilteredResults.slice(
this.loadedCount,
this.loadedCount + 20
);
this.treeViewComponent?.addNodes(nextBatch);
this.loadedCount += 20;
}---
Best Practices:
- Debounce filter input for better performance
- Show parent nodes when filtering children for context
- Highlight matching results
- Limit suggestions/results display
- Use appropriate sort order for your use case
- Cache original data for fast resets
- Consider server-side filtering for very large datasets
Getting Started with TreeView
Table of Contents
---
Installation
Install the Syncfusion Angular navigations package containing the TreeView component:
npm install @syncfusion/ej2-angular-navigations --saveFor Angular versions below 12, use the ngcc (legacy) package:
npm install @syncfusion/ej2-angular-navigations@ngcc --saveModule Setup
Import TreeViewModule in your Angular component using standalone architecture:
import { Component } from '@angular/core';
import { TreeViewModule } from '@syncfusion/ej2-angular-navigations';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-treeview-demo',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `<ejs-treeview id="treeview" [fields]="treeFields"></ejs-treeview>`
})
export class TreeViewDemoComponent {
// Component code here
}For NgModule-based applications, add to your module:
import { NgModule } from '@angular/core';
import { TreeViewModule } from '@syncfusion/ej2-angular-navigations';
@NgModule({
imports: [TreeViewModule],
// ...
})
export class AppModule { }CSS Imports
Add Syncfusion theme CSS to your styles.css or Angular component. The recommended approach is to import in styles.css:
/* Material3 theme (latest, recommended) */
@import '../node_modules/@syncfusion/ej2-base/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-buttons/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-navigations/styles/material3.css';
@import '../node_modules/@syncfusion/ej2-inputs/styles/material3.css';All Available Themes:
material3.css- Modern Material Design 3 (recommended)material.css- Original Material Designfabric.css- Microsoft Fluent Design Systembootstrap5.css- Bootstrap 5 themebootstrap.css- Bootstrap 4 themefluent.css- Fluent Light themefluent-dark.css- Fluent Dark themehighcontrast.css- High contrast for accessibilitytailwind.css- Tailwind CSS themebootstrap-dark.css- Bootstrap 4 Dark theme
Theme Selection Guide:
- Use
material3.cssfor modern, contemporary applications - Use
bootstrap5.cssif your app already uses Bootstrap 5 - Use
fluent.css/fluent-dark.cssfor Microsoft/Windows-style apps - Use
highcontrast.cssfor accessibility compliance - Use
tailwind.cssfor Tailwind CSS projects
Or use in component metadata:
@Component({
selector: 'app-treeview',
styleUrls: ['../node_modules/@syncfusion/ej2-navigations/styles/material3.css']
})Custom Resource Generator (CRG):
For optimized bundle size, use CRG to include only the components and themes you need:
- Visit: https://crg.syncfusion.com/
- Select TreeView component
- Choose your theme
- Generate and download optimized CSS
Creating Your First TreeView
Here's a minimal TreeView with hierarchical data:
import { Component } from '@angular/core';
import { TreeViewModule } from '@syncfusion/ej2-angular-navigations';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-basic-tree',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<div id="treeparent">
<ejs-treeview
id="treeview"
[fields]="treeFields">
</ejs-treeview>
</div>
`
})
export class BasicTreeViewComponent {
treeFields: Object = {
dataSource: [
{
id: 1,
name: 'Folder 1',
hasChild: true,
expanded: true,
child: [
{ id: 2, name: 'Item 1.1' },
{ id: 3, name: 'Item 1.2' }
]
},
{
id: 4,
name: 'Folder 2',
hasChild: true,
child: [
{ id: 5, name: 'Item 2.1' }
]
}
],
id: 'id',
text: 'name',
child: 'child',
hasChildren: 'hasChild',
expanded: 'expanded'
};
}Basic Data Structure
TreeView works with hierarchical data. Two common approaches:
Hierarchical (Nested) Data
Data with nested child arrays:
const hierarchicalData = [
{
id: '01',
name: 'Desktop',
expanded: true,
child: [
{ id: '01-01', name: 'Document1.txt' },
{ id: '01-02', name: 'Document2.docx' }
]
},
{
id: '02',
name: 'Downloads',
child: [
{ id: '02-01', name: 'Image.png' }
]
}
];
treeFields = {
dataSource: hierarchicalData,
id: 'id',
text: 'name',
child: 'child'
};Self-Referential (Flat) Data
Flat array with parent-child IDs:
const flatData = [
{ id: 1, name: 'Desktop', pid: null, hasChild: true },
{ id: 2, name: 'Folder 1', pid: 1, hasChild: true },
{ id: 3, name: 'Folder 2', pid: 1, hasChild: false },
{ id: 4, name: 'File 1.txt', pid: 2 },
{ id: 5, name: 'File 2.txt', pid: 2 }
];
treeFields = {
dataSource: flatData,
id: 'id',
parentID: 'pid',
text: 'name',
hasChildren: 'hasChild'
};Field Mapping Configuration
| Property | Purpose |
|---|---|
dataSource | Array of data items |
id | Unique identifier field name |
text | Display text field name |
child | Child array property name (hierarchical) |
parentID | Parent ID field name (flat/self-referential) |
hasChildren | Boolean property indicating if node has children |
expanded | Boolean property for initial expand state |
selected | Boolean property for initial selection |
isChecked | Boolean property for checkbox state |
iconCss | CSS class for node icon |
imageUrl | Image URL for node |
tooltip | Tooltip text for node |
Rendering Output
After creating the component, TreeView renders with:
- Expand/collapse icons for parent nodes
- Hierarchical indentation for child nodes
- Text labels from mapped field
- Default Material theme styling
The component is now ready for adding features like checkboxes, drag-and-drop, editing, and more. See other reference files for feature-specific implementation.
Node Editing & Manipulation in TreeView
Table of Contents
---
In-Place Editing
Enable Editing
Allow users to edit node text directly in the tree:
import { Component } from '@angular/core';
import { TreeViewModule } from '@syncfusion/ej2-angular-navigations';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-editable-tree',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview
[fields]="treeFields"
[allowEditing]="true">
</ejs-treeview>
`
})
export class EditableTreeComponent {
treeFields = {
dataSource: [
{
id: 1,
name: 'Desktop',
hasChild: true,
expanded: true,
child: [
{ id: 2, pid: 1, name: 'Document1.txt' },
{ id: 3, pid: 1, name: 'Document2.docx' }
]
}
],
id: 'id',
parentID: 'pid',
text: 'name',
hasChildren: 'hasChild'
};
}Edit Activation
Users can edit nodes in three ways:
Double-click the node:
Double-click on text → text becomes editablePress F2 while node is selected:
Click node → Press F2 → text becomes editableProgrammatic editing:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<button (click)="editNode()">Edit Node</button>
<ejs-treeview #treeview [fields]="treeFields" [allowEditing]="true"></ejs-treeview>
`
})
export class ProgrammaticEditComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
editNode(): void {
this.treeViewComponent?.beginEdit('2'); // Start editing node with ID 2
}
}Save/Cancel Editing
- Press Enter: Save edited text
- Press Escape: Cancel edit, restore original text
- Click elsewhere: Save edited text automatically
Adding Nodes
addNodes Method
Adds the collection of TreeView nodes based on target and index position. If target node is not specified, nodes are added as children of given parentID or at root level.
Method Signature
addNodes(nodes: { [key: string]: Object }[], target?: string | Element, index?: number, preventTargetExpand?: boolean): voidParameters
| Parameter | Type | Description |
|---|---|---|
nodes | { [key: string]: Object }[] | Required. Collection of objects with node properties to be added to the TreeView. Each object can contain any properties based on your field mappings. |
target | `string \ | Element` |
index | number | Optional. The index position at which nodes will be inserted. If not specified, nodes are appended at the end. |
preventTargetExpand | boolean | Optional. When true, prevents automatic expansion of the parent/target node after adding children. Default is false (parent expands by default). |
Return Type
void
Examples
Example 1: Add Single Child Node
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<button (click)="addChild()">Add Child Node</button>
<ejs-treeview #treeview [fields]="treeFields"></ejs-treeview>
`
})
export class AddNodeComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
treeFields = { dataSource: [], id: 'id', parentID: 'pid', text: 'name' };
addChild(): void {
const newNode = {
id: 10,
name: 'New Item',
pid: 1
};
this.treeViewComponent?.addNodes([newNode], '1'); // Add as child of node 1
}
}Example 2: Add Multiple Nodes to Parent
addMultipleNodes(): void {
const newNodes = [
{ id: 10, name: 'New Item 1', pid: 1 },
{ id: 11, name: 'New Item 2', pid: 1 },
{ id: 12, name: 'New Item 3', pid: 1 }
];
this.treeViewComponent?.addNodes(newNodes, '1'); // Add all as children of node 1
}Example 3: Add Node at Root Level
addRootNode(): void {
const rootNode = {
id: 20,
name: 'New Root Folder',
hasChild: true
};
this.treeViewComponent?.addNodes([rootNode]); // No target = add at root level
}Example 4: Add Node at Specific Position
addNodeAtPosition(): void {
const nodeToAdd = { id: 5, name: 'Insert Here', pid: 1 };
const parentNodeId = '1';
const insertIndex = 2; // Insert at position 2 (0-indexed)
this.treeViewComponent?.addNodes([nodeToAdd], parentNodeId, insertIndex);
}Example 5: Add Nodes Without Expanding Parent
addNodesWithoutExpand(): void {
const newNodes = [
{ id: 15, name: 'Hidden Item 1', pid: 3 },
{ id: 16, name: 'Hidden Item 2', pid: 3 }
];
const parentNodeId = '3';
const insertIndex = 0;
const preventExpand = true;
this.treeViewComponent?.addNodes(
newNodes,
parentNodeId,
insertIndex,
preventExpand // Parent node will not auto-expand
);
}Removing Nodes
Remove Single Node
removeNode(): void {
this.treeViewComponent?.removeNodes(['3']); // Node ID as string or array
}Remove Multiple Nodes
removeMultipleNodes(): void {
this.treeViewComponent?.removeNodes(['2', '5', '8']);
}Updating Nodes
updateNode Method
Replaces the text of the TreeView node with the given text only when the allowEditing property is enabled.
Method Signature:
updateNode(target: string | Element, newText: string): voidParameters:
| Parameter | Type | Description |
|---|---|---|
target | `string \ | Element` |
newText | string | Specifies the new text of TreeView node |
Returns: void
Requirement: The allowEditing property must be enabled for updateNode to work.
Update Single Node Text
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview
#treeview
[fields]="treeFields"
[allowEditing]="true">
</ejs-treeview>
<button (click)="updateNodeText()">Update Node Text</button>
`
})
export class UpdateNodeComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
updateNodeText(): void {
// Update node with ID '2' to new text 'Updated Text'
this.treeViewComponent?.updateNode('2', 'Updated Node Text');
}
}Update Node Using Element Reference
updateNodeUsingElement(): void {
// Get the node element
const nodeElement = document.querySelector('[data-uid="2"]');
if (nodeElement) {
// Update using element reference
this.treeViewComponent?.updateNode(nodeElement as Element, 'New Text');
}
}Update Multiple Nodes Sequentially
updateMultipleNodes(): void {
const nodesToUpdate = [
{ id: '2', text: 'Updated Name 1' },
{ id: '3', text: 'Updated Name 2' },
{ id: '4', text: 'Updated Name 3' }
];
nodesToUpdate.forEach(node => {
this.treeViewComponent?.updateNode(node.id, node.text);
});
}Update Node with Conditions
updateNodeConditionally(): void {
const nodeId = '2';
const currentNodeData = this.treeViewComponent?.getTreeData(nodeId);
// Only update if certain conditions are met
if (currentNodeData && currentNodeData[0]?.name !== 'Protected') {
this.treeViewComponent?.updateNode(nodeId, 'Updated safely');
} else {
console.warn('Cannot update protected nodes');
}
}Update Node with Event Handling
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
import { NodeEditEventArgs } from '@syncfusion/ej2-navigations';
@Component({
template: `
<ejs-treeview
#treeview
[fields]="treeFields"
[allowEditing]="true"
(nodeEdited)="onNodeEdited($event)">
</ejs-treeview>
`
})
export class UpdateWithEventsComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
onNodeEdited(event: NodeEditEventArgs): void {
// After editing is complete, you can further update if needed
console.log('Node edited:', event.nodeData.name);
// Example: Convert to uppercase after editing
if (event.nodeData.name) {
this.treeViewComponent?.updateNode(
event.nodeData.id as string,
event.nodeData.name.toUpperCase()
);
}
}
}Refresh Node (Reload Data)
refreshNodeData(): void {
// Refresh specific node from data source
this.treeViewComponent?.refreshNode('2');
}
refreshAllNodes(): void {
// Refresh entire tree
const allData = this.treeViewComponent?.getTreeData() || [];
const allNodeIds = allData.map((node: any) => node.id);
allNodeIds.forEach((nodeId: any) => {
this.treeViewComponent?.refreshNode(nodeId);
});
}Moving Nodes
moveNodes Method
Moves the collection of nodes within the same TreeView based on target or its index position. Nodes can be repositioned to different parents or reorganized within the same parent.
Method Signature
moveNodes(sourceNodes: string[] | Element[], target: string | Element, index?: number, preventTargetExpand?: boolean): voidParameters
| Parameter | Type | Description |
|---|---|---|
sourceNodes | `string[] \ | Element[]` |
target | `string \ | Element` |
index | number | Optional. The index position at which nodes will be inserted under the target. If not specified, nodes are appended at the end. |
preventTargetExpand | boolean | Optional. When true, prevents automatic expansion of the target parent node after moving nodes. Default is false (target expands by default). |
Return Type
void
Examples
Example 1: Move Nodes to Different Parent
moveNodesToParent(): void {
const nodeIds = ['5', '6', '7']; // Nodes to move
const newParentId = '2'; // New parent node ID
this.treeViewComponent?.moveNodes(nodeIds, newParentId); // Move nodes as children of node 2
}Example 2: Move Single Node
moveSingleNode(): void {
const nodeToMove = ['5'];
const targetParentId = '3';
this.treeViewComponent?.moveNodes(nodeToMove, targetParentId);
}Example 3: Move Nodes to Specific Position
moveNodesToPosition(): void {
const sourceNodeIds = ['8', '9'];
const targetNodeId = '2';
const insertIndex = 1; // Insert at position 1 (0-indexed)
this.treeViewComponent?.moveNodes(sourceNodeIds, targetNodeId, insertIndex);
}Example 4: Move Nodes Without Target Expansion
moveNodesWithoutExpand(): void {
const sourceNodeIds = ['5', '6'];
const targetNodeId = '4';
const insertIndex = 0;
const preventExpand = true;
this.treeViewComponent?.moveNodes(
sourceNodeIds,
targetNodeId,
insertIndex,
preventExpand // Target node will not auto-expand
);
}Example 5: Move Nodes Using Element References
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview #treeview [fields]="treeFields"></ejs-treeview>
`
})
export class MoveNodeComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
moveNodesByElement(): void {
// Get element references from the TreeView
const sourceElements = this.treeViewComponent?.element.querySelectorAll('[data-id="5"], [data-id="6"]');
const targetElement = this.treeViewComponent?.element.querySelector('[data-id="3"]');
if (sourceElements && targetElement) {
this.treeViewComponent?.moveNodes(
Array.from(sourceElements) as Element[],
targetElement
);
}
}
}Validation & Events
nodeEditing Event (Before Edit)
Triggered before node editing starts. Use for validation or prevention:
<ejs-treeview
[fields]="treeFields"
[allowEditing]="true"
(nodeEditing)="onNodeEditing($event)">
</ejs-treeview>
onNodeEditing(event: any): void {
console.log('Old text:', event.text);
// Prevent editing first-level nodes
if (event.nodeData.level === 0) {
event.cancel = true;
}
}nodeEdited Event (After Edit)
Triggered after editing completes. Use for validation or post-processing:
import { NodeEditEventArgs } from '@syncfusion/ej2-navigations';
<ejs-treeview
[fields]="treeFields"
[allowEditing]="true"
(nodeEdited)="onNodeEdited($event)">
</ejs-treeview>
onNodeEdited(event: NodeEditEventArgs): void {
console.log('New text:', event.newText);
console.log('Old text:', event.oldText);
// Validate: prevent empty names
if (!event.newText || event.newText.trim() === '') {
event.cancel = true;
alert('Node name cannot be empty');
}
// Validate: prevent duplicates
const existingNodes = this.getNodeNames();
if (existingNodes.includes(event.newText)) {
event.cancel = true;
alert('Node name already exists');
}
}
getNodeNames(): string[] {
const allData = this.treeViewComponent?.getTreeData() || [];
return allData.map((node: any) => node.name);
}dataSourceChanged Event
Triggered after any data modification:
(dataSourceChanged)="onDataSourceChanged($event)"
onDataSourceChanged(event: any): void {
console.log('Tree data changed');
console.log('Current data:', this.treeViewComponent?.getTreeData());
}Edge Cases
Prevent Duplicate Node Names
onNodeEdited(event: NodeEditEventArgs): void {
const allNodes = this.treeViewComponent?.getTreeData() || [];
const isDuplicate = allNodes.some((node: any) =>
node.id !== event.nodeData.id &&
node.name === event.newText
);
if (isDuplicate) {
event.cancel = true;
this.updateNodeToOldValue(event.nodeData.id, event.oldText);
}
}Validate Node Name Format
onNodeEdited(event: NodeEditEventArgs): void {
const nameRegex = /^[a-zA-Z0-9\s\-_.]+$/; // Alphanumeric, spaces, hyphens, dots, underscores
if (!nameRegex.test(event.newText)) {
event.cancel = true;
alert('Invalid characters in node name');
}
}Prevent Editing During Specific Conditions
onNodeEditing(event: any): void {
// Don't allow editing if node is disabled
if (event.nodeData.disabled) {
event.cancel = true;
}
// Don't allow editing if user lacks permission
if (!this.userHasEditPermission()) {
event.cancel = true;
}
}
userHasEditPermission(): boolean {
// Check permissions logic here
return true;
}Revert Changes on Error
async onNodeEdited(event: NodeEditEventArgs): Promise<void> {
try {
// Send to server for validation
const result = await this.saveNodeName(event.nodeData.id, event.newText);
if (!result.success) {
event.cancel = true;
this.updateNodeToOldValue(event.nodeData.id, event.oldText);
}
} catch (error) {
event.cancel = true;
alert('Error saving node name');
}
}
saveNodeName(nodeId: string, newName: string): Promise<any> {
// Server API call here
return Promise.resolve({ success: true });
}---
Best Practices:
- Always validate user input in nodeEdited event
- Prevent special characters that might break data structure
- Disable editing for critical nodes
- Provide user feedback for validation errors
- Handle async operations (server validation) before saving
- Log all changes for audit purposes
Node Selection in TreeView
Table of Contents
- Overview
- Checkbox Selection
- Checkbox States
- Checkbox Properties
- Multi-Selection
- Single Selection
- Selection Events
- Getting Selected Nodes
- Advanced Checkbox Control
- Select One Child
- Remove Parent Checkbox
- Checkbox Toggle on Node Text Click
- Disable Checkbox on Specific Nodes
---
Overview
TreeView supports multiple selection modes:
- Checkbox selection: Hierarchical checkboxes with parent-child relationships
- Multi-selection: Multiple nodes with Ctrl+Click or Shift+Click
- Single selection: One node selected at a time (default)
Checkbox Selection
Enable Checkboxes
Add checkboxes before each tree node:
import { Component } from '@angular/core';
import { TreeViewModule } from '@syncfusion/ej2-angular-navigations';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-checkbox-tree',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview
[fields]="treeFields"
[showCheckBox]="true">
</ejs-treeview>
`
})
export class CheckboxTreeComponent {
treeFields = {
dataSource: [
{
id: 1,
name: 'Australia',
hasChild: true,
expanded: true,
child: [
{ id: 2, pid: 1, name: 'New South Wales' },
{ id: 3, pid: 1, name: 'Victoria' },
{ id: 4, pid: 1, name: 'South Australia' }
]
},
{
id: 5,
name: 'Brazil',
hasChild: true,
child: [
{ id: 6, pid: 5, name: 'Paraná' },
{ id: 7, pid: 5, name: 'Ceará' }
]
}
],
id: 'id',
parentID: 'pid',
text: 'name',
hasChildren: 'hasChild',
expanded: 'expanded'
};
}Checkbox States
Tri-State Checkboxes (Default)
When autoCheck is enabled (default):
- Checked: All child nodes are checked
- Unchecked: All child nodes are unchecked
- Intermediate (tri-state): Some child nodes are checked
autoCheck = true; // Parent/child auto-check enabled (default)Parent node automatically:
- Becomes checked when ALL children are checked
- Becomes unchecked when ALL children are unchecked
- Becomes tri-state when SOME children are checked
Independent Checkbox States
Make parent and child checkboxes independent:
autoCheck = false; // No auto-relationship
<ejs-treeview
[fields]="treeFields"
[showCheckBox]="true"
[autoCheck]="false">
</ejs-treeview>Checkbox Properties
Set Initially Checked Nodes
treeFields = {
dataSource: [
{ id: 1, name: 'Australia', hasChild: true, isChecked: true },
{ id: 2, pid: 1, name: 'New South Wales', isChecked: true },
{ id: 3, pid: 1, name: 'Victoria' }
],
id: 'id',
parentID: 'pid',
text: 'name',
isChecked: 'isChecked' // Map checkbox state
};Get Currently Checked Nodes
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview #treeview [fields]="treeFields" [showCheckBox]="true"></ejs-treeview>
<button (click)="getChecked()">Get Checked Nodes</button>
`
})
export class GetCheckedComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
getChecked(): void {
const checkedNodes = this.treeViewComponent?.getAllCheckedNodes();
console.log('Checked node IDs:', checkedNodes);
}
}Multi-Selection
Enable Multi-Selection
Select multiple nodes without checkboxes using keyboard:
<ejs-treeview
[fields]="treeFields"
[allowMultiSelection]="true">
</ejs-treeview>Selection methods:
Ctrl + Click: Toggle individual node selectionShift + Click: Select continuous range from last selected nodeClick: Single selection (replaces previous)
Single Selection
Single selection is default. Only one node can be selected at a time:
<ejs-treeview
[fields]="treeFields"
[allowMultiSelection]="false">
</ejs-treeview>Or explicitly:
allowMultiSelection = false;Selection Events
nodeSelected Event
Triggered when a node is selected:
<ejs-treeview
[fields]="treeFields"
[allowMultiSelection]="true"
(nodeSelected)="onNodeSelected($event)">
</ejs-treeview>
onNodeSelected(event: any): void {
console.log('Selected node:', event.nodeData.name);
console.log('Node ID:', event.nodeData.id);
}nodeSelecting Event
Triggered before selection. Allows canceling selection:
(nodeSelecting)="onNodeSelecting($event)"
onNodeSelecting(event: any): void {
// Prevent selection of specific nodes
if (event.nodeData.id === 'restricted') {
event.cancel = true; // Prevent this node from being selected
}
}nodeChecking Event
Triggered before checkbox changes:
(nodeChecking)="onNodeChecking($event)"
onNodeChecking(event: any): void {
console.log('About to check/uncheck node:', event.data[0].name);
// Can set event.cancel = true to prevent
}nodeChecked Event
Triggered after checkbox state changes:
(nodeChecked)="onNodeChecked($event)"
onNodeChecked(event: any): void {
console.log('Node checked:', event.data[0].name);
console.log('Checked state:', event.data[0].checked);
}Get Checked Node Information
const checkedIds = this.treeViewComponent?.getAllCheckedNodes();
checkedIds?.forEach(id => {
const nodeData = this.treeViewComponent?.getTreeData(id)[0];
console.log('Checked node:', nodeData.name);
});Get Checked Nodes with Full Data
getFullCheckedNodeData(): any[] {
const checkedIds = this.treeViewComponent?.getAllCheckedNodes() || [];
const allNodes = this.treeViewComponent?.getTreeData() || [];
return allNodes.filter((node: any) => checkedIds.includes(node.id));
}Export Checked Nodes
exportCheckedNodesAsJson(): string {
const checkedData = this.getFullCheckedNodeData();
return JSON.stringify(checkedData, null, 2);
}Advanced Checkbox Control
Disable Checkbox on Specific Nodes
Use the drawNode event to disable checkboxes for certain nodes:
import { DrawNodeEventArgs } from '@syncfusion/ej2-angular-navigations';
<ejs-treeview
[fields]="treeFields"
[showCheckBox]="true"
(drawNode)="onDrawNode($event)">
</ejs-treeview>
onDrawNode(event: DrawNodeEventArgs): void {
// Disable checkbox for parent nodes only
if (event.nodeData?.hasChild) {
const checkbox = event.node?.querySelector('.e-checkbox-wrapper');
checkbox?.classList.add('e-checkbox-disabled');
}
}Remove Parent Checkboxes (Leaf Only)
onDrawNode(event: DrawNodeEventArgs): void {
if (event.nodeData?.hasChild) {
const checkbox = event.node?.querySelector('.e-checkbox-wrapper');
checkbox?.style.display = 'none'; // Hide parent checkbox
}
}Toggle Checkbox on Node Click
import { NodeClickEventArgs } from '@syncfusion/ej2-navigations';
<ejs-treeview
[fields]="treeFields"
[showCheckBox]="true"
(nodeClicked)="onNodeClicked($event)">
</ejs-treeview>
onNodeClicked(event: NodeClickEventArgs): void {
const node = event.node as HTMLElement;
if (node?.classList.contains('e-fullrow')) {
const checkedNodes = this.treeViewComponent?.getAllCheckedNodes() || [];
const nodeId = event.nodeData.id;
if (checkedNodes.includes(nodeId)) {
this.treeViewComponent?.uncheckAll([nodeId]);
} else {
this.treeViewComponent?.checkAll([nodeId]);
}
}
}checkAll Method
Checks all unchecked nodes. You can also check specific nodes by passing array of unchecked nodes as argument.
Method Signature:
checkAll(nodes?: string[] | Element[]): voidParameters:
| Parameter | Type | Description |
|---|---|---|
nodes (optional) | `string[] \ | Element[]` |
Returns: void
Examples:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview
#treeview
[fields]="treeFields"
[showCheckBox]="true">
</ejs-treeview>
<button (click)="checkSpecificNodes()">Check Specific</button>
<button (click)="checkAll()">Check All</button>
`
})
export class CheckComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
// Check specific nodes
checkSpecificNodes(): void {
const nodesToCheck = ['1', '2', '5'];
this.treeViewComponent?.checkAll(nodesToCheck);
}
// Check all nodes
checkAll(): void {
this.treeViewComponent?.checkAll();
}
// Check using element references
checkByElements(): void {
const elements = [
document.querySelector('[data-uid="1"]') as Element,
document.querySelector('[data-uid="2"]') as Element
];
this.treeViewComponent?.checkAll(elements);
}
}---
uncheckAll Method
Unchecks all checked nodes. You can also uncheck specific nodes by passing array of checked nodes as argument.
Method Signature:
uncheckAll(nodes?: string[] | Element[]): voidParameters:
| Parameter | Type | Description |
|---|---|---|
nodes (optional) | `string[] \ | Element[]` |
Returns: void
Examples:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
@Component({
template: `
<ejs-treeview
#treeview
[fields]="treeFields"
[showCheckBox]="true">
</ejs-treeview>
<button (click)="uncheckSpecificNodes()">Uncheck Specific</button>
<button (click)="uncheckAll()">Uncheck All</button>
`
})
export class UncheckComponent {
@ViewChild('treeview') treeViewComponent?: TreeViewComponent;
// Uncheck specific nodes
uncheckSpecificNodes(): void {
const nodesToUncheck = ['1', '2'];
this.treeViewComponent?.uncheckAll(nodesToUncheck);
}
// Uncheck all nodes
uncheckAll(): void {
this.treeViewComponent?.uncheckAll();
}
// Uncheck using element references
uncheckByElements(): void {
const elements = [
document.querySelector('[data-uid="1"]') as Element,
document.querySelector('[data-uid="2"]') as Element
];
this.treeViewComponent?.uncheckAll(elements);
}
}Select One Child
Restrict Selection to One Child Per Parent
In scenarios where the application requires selecting only one child node at a time under a specific parent node while maintaining multi-selection capability across different parent branches, you can implement this using the nodeSelecting event:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
import { NodeSelectEventArgs } from '@syncfusion/ej2-navigations';
@Component({
selector: 'app-single-child',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview
#tree
[fields]="listfields"
[allowMultiSelection]="true"
(nodeSelecting)="onNodeSelecting($event)">
</ejs-treeview>
`
})
export class SingleChildSelectionComponent {
@ViewChild('tree') tree?: TreeViewComponent;
public localData: Object[] = [
{ id: 1, name: 'Parent 1', hasChild: true, expanded: true },
{ id: 2, pid: 1, name: 'Child 1' },
{ id: 3, pid: 1, name: 'Child 2' },
{ id: 4, pid: 1, name: 'Child 3' },
{ id: 7, name: 'Parent 2', hasChild: true, expanded: true },
{ id: 8, pid: 7, name: 'Child 1' },
{ id: 9, pid: 7, name: 'Child 2' },
{ id: 10, pid: 7, name: 'Child 3' },
];
public listfields: Object = {
dataSource: this.localData,
id: 'id',
parentID: 'pid',
text: 'name',
hasChildren: 'hasChild'
};
private parent?: any;
private child?: any;
private count: boolean = false;
private childCount: boolean = false;
public onNodeSelecting(args: NodeSelectEventArgs): void {
let id: any = args.nodeData['parentID'];
if (!this.count) {
this.parent = id;
this.count = true;
}
if (!this.childCount) {
this.child = args.nodeData['id'];
this.childCount = true;
}
if (id != null && id === this.parent) {
let element: HTMLElement = (this.tree as any)?.element.querySelector('[data-uid="' + id + '"]');
let liElements: any = element?.querySelectorAll('ul li');
for (let i: number = 0; i < (liElements?.length || 0); i++) {
let nodeData: any = (this.tree as any)?.getNode(liElements[i]);
if (nodeData.selected && args.action === "select" && this.child !== args.nodeData['id']) {
args.cancel = true;
} else if (args.action === "un-select" && this.child === args.nodeData['id']) {
this.childCount = false;
this.child = null;
this.parent = null;
this.count = false;
}
}
} else if (id !== this.parent && id !== null) {
if (args.action == "select") {
args.cancel = true;
}
} else if (id === null) {
this.childCount = false;
this.child = null;
this.parent = null;
this.count = false;
}
}
}Use Case: Restrict users to select only one child at a time under each parent while allowing different parents to have different selected children.
---
Remove Parent Checkbox
Hide Checkbox on Parent Nodes (Leaf Nodes Only)
To display checkboxes only for leaf nodes while hiding them on parent nodes for a cleaner interface:
import { Component } from '@angular/core';
import { DrawNodeEventArgs } from '@syncfusion/ej2-angular-navigations';
@Component({
selector: 'app-leaf-checkbox',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview
[fields]="field"
[showCheckBox]="true"
(drawNode)="onDrawNode($event)">
</ejs-treeview>
`,
styles: [`
:host ::ng-deep .custom .e-checkbox-wrapper {
display: none;
}
:host ::ng-deep .custom .e-list-item.e-leaf-node .e-checkbox-wrapper {
display: block;
}
`]
})
export class LeafCheckboxComponent {
public Countries: Object[] = [
{ id: 1, name: 'India', hasChild: true, expanded: true },
{ id: 2, pid: 1, name: 'Assam' },
{ id: 3, pid: 1, name: 'Bihar' },
{ id: 7, name: 'Brazil', hasChild: true },
{ id: 8, pid: 7, name: 'Paraná' },
];
public field: Object = {
dataSource: this.Countries,
id: 'id',
text: 'name',
parentID: 'pid',
hasChildren: 'hasChild'
};
onDrawNode(event: DrawNodeEventArgs): void {
if (event.nodeData?.hasChild) {
const checkbox = event.node?.querySelector('.e-checkbox-wrapper');
if (checkbox) {
checkbox.style.display = 'none';
}
}
}
}---
Checkbox Toggle on Node Text Click
Check/Uncheck Checkbox When Clicking Node Text
Enable checkbox toggling when users click the node text instead of just the checkbox itself:
import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@syncfusion/ej2-angular-navigations';
import { NodeClickEventArgs, NodeKeyPressEventArgs } from '@syncfusion/ej2-navigations';
@Component({
selector: 'app-click-to-check',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview
#treevalidate
[fields]="field"
[showCheckBox]="true"
(nodeClicked)="nodeCheck($event)"
(keyPress)="nodeCheck($event)">
</ejs-treeview>
`
})
export class ClickToCheckComponent {
@ViewChild('treevalidate') treevalidate?: TreeViewComponent;
public countries: Object[] = [
{ id: 1, name: 'Australia', hasChild: true, expanded: true },
{ id: 2, pid: 1, name: 'New South Wales' },
{ id: 3, pid: 1, name: 'Victoria' },
{ id: 7, name: 'Brazil', hasChild: true },
{ id: 8, pid: 7, name: 'Paraná' },
];
public field: Object = {
dataSource: this.countries,
id: 'id',
parentID: 'pid',
text: 'name',
hasChildren: 'hasChild'
};
public nodeCheck(args: NodeClickEventArgs | NodeKeyPressEventArgs | any): void {
let checkedNode: any = [args.node];
// Toggle checkbox on fullrow click or Enter key
if ((args.event.target as EventTarget | any)?.classList?.contains('e-fullrow') || args.event.key == "Enter") {
let getNodeDetails: any = (this.treevalidate as TreeViewComponent).getNode(args.node);
if (getNodeDetails.isChecked == 'true') {
(this.treevalidate as TreeViewComponent).uncheckAll(checkedNode);
} else {
(this.treevalidate as TreeViewComponent).checkAll(checkedNode);
}
}
}
}Features:
- Click on node text to toggle checkbox state
- Press Enter to toggle checkbox
- Provides larger click target for better UX
- Works alongside direct checkbox clicking
---
Disable Checkbox on Specific Nodes
Conditionally Disable Checkboxes
Disable checkboxes on specific nodes based on data attributes or conditions using the drawNode event:
import { Component } from '@angular/core';
import { DrawNodeEventArgs } from '@syncfusion/ej2-angular-navigations';
@Component({
selector: 'app-disable-checkbox',
standalone: true,
imports: [FormsModule, TreeViewModule],
template: `
<ejs-treeview
[fields]="field"
[showCheckBox]="true"
(drawNode)="onDrawNode($event)">
</ejs-treeview>
`
})
export class DisableCheckboxComponent {
public Items: Object[] = [
{ id: 1, name: 'Important', hasChild: true, canDisable: false, expanded: true },
{ id: 2, pid: 1, name: 'Read-Only Item', canDisable: true },
{ id: 3, pid: 1, name: 'Editable Item', canDisable: false },
{ id: 4, name: 'System', hasChild: true, canDisable: true },
{ id: 5, pid: 4, name: 'Protected', canDisable: true },
];
public field: Object = {
dataSource: this.Items,
id: 'id',
text: 'name',
parentID: 'pid',
hasChildren: 'hasChild'
};
onDrawNode(event: DrawNodeEventArgs): void {
if (event.nodeData?.canDisable) {
const checkbox = event.node?.querySelector('.e-checkbox');
if (checkbox) {
checkbox.classList.add('e-disabled');
checkbox.setAttribute('disabled', 'disabled');
}
}
}
}When to use:
- Protect critical nodes from modification
- Enforce permissions and access control
- Create read-only tree sections
---
Best Practices:
- Use checkboxes for batch operations on multiple items
- Use multi-selection for complex workflows
- Enable autoCheck for intuitive parent-child relationships
- Use selection events for validation or side effects
- Provide visual feedback for selection state changes