Drag and Drop

The TreeView allows the user to reorder its nodes or transfer them to another TreeView instance via Drag and Drop.

Setup

To activate the Drag and Drop functionality, apply the kendoTreeViewDragAndDrop directive on a TreeView component.

The directive allows the nodes to be dragged and the TreeView will emit the following events:

  • nodeDragStart—Fired when the dragging begins. If prevented, the node won't be dragged.
  • nodeDrag—Fired during dragging.
  • nodeDrop—Fired when a node is dropped on a valid target. If prevented or marked as invalid, the subsequent addItem and removeItem events will not be fired.
  • addItem—Fired on the targeted TreeView after the node is dropped.
  • removeItem—Fired on the source TreeView after the node is dropped.
  • nodeDragEnd—Fired when the dragging ends.

In the following example the event arguments are logged to the console as they occur.

import { Component } from '@angular/core';
import { TreeItemDropEvent, DropPosition, TreeItemLookup, DropAction } from '@progress/kendo-angular-treeview';

const isOfType = (fileName: string, ext: string) => new RegExp(`.${ext}\$`).test(fileName);
const isFile = (name: string) => name.split('.').length > 1;

@Component({
    selector: 'my-app',
    template: `
        <kendo-treeview
            [nodes]="data"
            [textField]="'text'"
            kendoTreeViewHierarchyBinding
            [childrenField]="'items'"
            kendoTreeViewExpandable
            [expandBy]="'id'"
            [expandedKeys]="[1]"
            kendoTreeViewDragAndDrop
            kendoTreeViewDragAndDropEditing
            (nodeDragStart)="log('nodeDragStart', $event)"
            (nodeDrag)="log('nodeDrag', $event)"
            (nodeDrop)="handleDrop($event)"
            (addItem)="log('addItem', $event)"
            (removeItem)="log('removeItem', $event)"
            (nodeDragEnd)="log('nodeDragEnd', $event)"
        >
            <ng-template kendoTreeViewNodeTemplate let-dataItem>
                <span [ngClass]="iconClass(dataItem)"></span>
                {{ dataItem.text }}
            </ng-template>
            <ng-template kendoTreeViewDragClueTemplate let-action="action" let-destinationItem="destinationItem" let-text="text">
                <span class="k-drag-status k-icon" [ngClass]="getDragStatus(action, destinationItem)"></span>
                <span>{{ text }}</span>
            </ng-template>
        </kendo-treeview>
    `
})
export class AppComponent {
    public data: any[] = [{
        id: 1, text: 'My Documents', items: [
            {
                id: 2, text: 'Kendo UI Project', items: [
                    { id: 3, text: 'about.html' },
                    { id: 4, text: 'index.html' },
                    { id: 5, text: 'logo.png' }
                ]
            },
            {
                id: 6, text: 'New Web Site', items: [
                    { id: 7, text: 'mockup.jpg' },
                    { id: 8, text: 'Research.pdf' }
                ]
            },
            {
                id: 9, text: 'Reports', items: [
                    { id: 10, text: 'February.pdf' },
                    { id: 11, text: 'March.pdf' },
                    { id: 12, text: 'April.pdf' }
                ]
            }
        ]
    }];

    public iconClass({ text }: any): any {
        return {
            'k-i-file-pdf': isOfType(text, 'pdf'),
            'k-i-folder': !isFile(text),
            'k-i-html': isOfType(text, 'html'),
            'k-i-image': isOfType(text, 'jpg|png'),
            'k-icon': true
        };
    }

    public getDragStatus(action: DropAction, destinationItem: TreeItemLookup): string {
        if (destinationItem && action === DropAction.Add && isFile(destinationItem.item.dataItem.text)) {
            return 'k-i-cancel';
        }

        switch (action) {
            case DropAction.Add: return 'k-i-plus';
            case DropAction.InsertTop: return 'k-i-insert-up';
            case DropAction.InsertBottom: return 'k-i-insert-down';
            case DropAction.InsertMiddle: return 'k-i-insert-middle';
            case DropAction.Invalid:
            default: return 'k-i-cancel';
        }
    }

    public log(event: string, args?: any): void {
        console.log(event, args);
    }

    public handleDrop(event: TreeItemDropEvent): void {
        this.log('nodeDrop', event);

        // prevent drop if attempting to add to file
        if (isFile(event.destinationItem.item.dataItem.text) && event.dropPosition === DropPosition.Over) {
            event.setValid(false);
        }
    }
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TreeViewModule } from '@progress/kendo-angular-treeview';

import { AppComponent } from './app.component';

@NgModule({
  bootstrap:    [ AppComponent ],
  declarations: [ AppComponent ],
  imports:      [ BrowserModule, BrowserAnimationsModule, TreeViewModule]
})
export class AppModule { }

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

Built-in Editing Directive

The kendoTreeViewDragAndDrop directive allows for items to be dragged and triggers the corresponding events.
It doesn't, however, make any changes to the nodes' order out-of-the-box, as the TreeView does not manage its own data.

Persisting and displaying the changes during Drag and Drop can be achieved either by manually handling the corresponding events and mutating the data, or by using the kendoTreeViewDragAndDropEditing directive. The latter uses an EditService which handles the addItem and removeItem events. You can supply your own EditService to the directive, or use it in combination with one of the two data binding directives.

Using with the Hierarchy Binding Directive

When the kendoTreeViewDragAndDropEditing directive is used in combination with the kendoTreeViewHierarchyBinding directive, the data binding directive sets the EditService of the editing directive. And the initially passed data gets updated on every Drag and Drop interaction with no further setup.

import { Component } from '@angular/core';

@Component({
    selector: 'my-app',
    template: `
        <kendo-treeview
            kendoTreeViewDragAndDrop
            kendoTreeViewDragAndDropEditing
            kendoTreeViewHierarchyBinding
            [childrenField]="'items'"
            [nodes]="data"
            [textField]="'text'"
            kendoTreeViewExpandable
            [expandBy]="'id'"
            [expandedKeys]="expandedKeys"
        >
        </kendo-treeview>
    `
})
export class AppComponent {
    public expandedKeys: number[] = [1];

    public data: any[] = [
        {
            id: 1, text: 'Furniture', items: [
                { id: 2, text: 'Tables & Chairs' },
                { id: 3, text: 'Sofas' },
                {
                    id: 3, text: 'Occasional Furniture', items: [{
                        id: 4, text: 'Decor', items: [
                            { id: 5, text: 'Bed Linen' },
                            { id: 6, text: 'Curtains & Blinds' }
                        ]
                    }]
                }
            ]
        },
        { id: 7, text: 'Carpets' },
        { id: 8, text: 'Outdoors' }
    ];
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TreeViewModule } from '@progress/kendo-angular-treeview';

import { AppComponent } from './app.component';

@NgModule({
  bootstrap:    [ AppComponent ],
  declarations: [ AppComponent ],
  imports:      [ BrowserModule, BrowserAnimationsModule, TreeViewModule]
})
export class AppModule { }

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

Using with the Flat Binding Directive

The kendoTreeViewDragAndDropEditing directive can also be used with the kendoTreeViewFlatDataBinding directive to get out-of-the-box data editing.

import { Component } from '@angular/core';

@Component({
    selector: 'my-app',
    template: `
        <kendo-treeview
            kendoTreeViewDragAndDrop
            kendoTreeViewDragAndDropEditing
            kendoTreeViewFlatDataBinding
            [idField]="'employeeId'"
            [parentIdField]="'reportsTo'"
            [nodes]="employees"
            [textField]="'name'"
            kendoTreeViewExpandable
            [expandBy]="'employeeId'"
            [expandedKeys]="expandedKeys"
        >
        </kendo-treeview>
    `
})
export class AppComponent {
    public expandedKeys: number[] = [2];

    public employees: any[] = [
        { employeeId: 2, name: 'Andrew Fuller', reportsTo: null },
        { employeeId: 1, name: 'Nancy Davolio', reportsTo: 2 },
        { employeeId: 3, name: 'Janet Leverling', reportsTo: 2 },
        { employeeId: 4, name: 'Margaret Peacock', reportsTo: 2 },
        { employeeId: 5, name: 'Steven Buchanan', reportsTo: 2 },
        { employeeId: 8, name: 'Laura Callahan', reportsTo: 2 },
        { employeeId: 6, name: 'Michael Suyama', reportsTo: 5 },
        { employeeId: 7, name: 'Robert King', reportsTo: 5 },
        { employeeId: 9, name: 'Anne Dodsworth', reportsTo: 5 }
    ];
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TreeViewModule } from '@progress/kendo-angular-treeview';

import { AppComponent } from './app.component';

@NgModule({
  bootstrap:    [ AppComponent ],
  declarations: [ AppComponent ],
  imports:      [ BrowserModule, BrowserAnimationsModule, TreeViewModule]
})
export class AppModule { }

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

Remote Binding

In order to display the changes from Drag and Drop interaction, a local data set with the fetched nodes has to be persisted and mutated.

Flat Homogeneous Data

The following example show-cases a custom directive that handles the remote data binding and editing during Drag and Drop.

import { Component } from '@angular/core';

@Component({
    selector: 'my-app',
    template: `
        <kendo-treeview
            employeesDataSource
            kendoTreeViewDragAndDrop
            kendoTreeViewExpandable
            [expandBy]="'employeeId'"
        >
        </kendo-treeview>
    `
})
export class AppComponent { }
import { Directive, OnInit } from '@angular/core';
import { Employee } from './employee';
import { EmployeesService } from './employees.service';
import { tap, filter, take } from 'rxjs/operators';
import { Observable, of } from 'rxjs';

import { TreeItemAddRemoveArgs, DropPosition, TreeViewComponent } from '@progress/kendo-angular-treeview';

@Directive({
    selector: '[employeesDataSource]'
})
export class EmployeesDataSourceDirective implements OnInit {

    public employees: Employee[] = [];

    constructor(
        private treeview: TreeViewComponent,
        private employeesService: EmployeesService
    ) { }

    public ngOnInit(): void {
        this.treeview.children = this.fetchChildren.bind(this);
        this.treeview.hasChildren = this.hasChildren.bind(this);
        this.treeview.textField = 'fullName';
        this.treeview.nodeDrop.subscribe(this.handleDrop.bind(this));

        this.employeesService.fetchRootLevelEmployees()
            .pipe(tap(employees => this.employees = employees))
            .subscribe(employees => this.treeview.nodes = employees.slice());
    }

    public hasChildren(employee: Employee): boolean {
        return employee.hasEmployees;
    }

    public fetchChildren = (employee: Employee): Observable<Employee[]> => {
        if (!(employee && employee.hasEmployees)) {
            return of([]);
        }

        const employees = this.employees.filter(e => e.reportsTo === employee.employeeId);
        if (employees.length > 0) {
            return of(employees);
        }

        return this.employeesService
            .fetchSubordinateEmployees(employee.employeeId)
            .pipe(tap(data => this.employees.push(...data)));
    }

    public handleDrop(args: TreeItemAddRemoveArgs): void {
        this.removeItem(args);
        this.addItem(args);

        // the TreeView data needs to be rebound to reflect changed parent ids
        this.rebindTreeView();
    }

    private addItem({ sourceItem, destinationItem, dropPosition, destinationTree }: TreeItemAddRemoveArgs): void {
        const sourceDataItem: Employee = sourceItem.item.dataItem;
        const destinationDataItem: Employee = destinationItem.item.dataItem;

        if (dropPosition === DropPosition.Over) {
            // assing the parent item id
            sourceDataItem.reportsTo = destinationDataItem.employeeId;

            // drop target has children but is collapsed (children not fetched yet)
            if (!this.hasLoadedEmployees(destinationDataItem) && destinationDataItem.hasEmployees) {
                destinationTree.childrenLoaded
                    .pipe(
                        filter(({ item }) => item.dataItem === destinationDataItem),
                        take(1)
                    )
                    .subscribe(
                        () => {
                            // push the moved item at the end of the collection after the new items are loaded
                            this.employees.push(sourceDataItem);
                            this.rebindTreeView();
                        }
                    );
            } else {
                destinationDataItem.hasEmployees = true;

                // push the moved item at the end of the collection
                this.employees.push(sourceDataItem);
            }

            // expand the node that was dropped into if it's collapsed
            if (!destinationTree.isExpanded(destinationDataItem, destinationItem.item.index)) {
                destinationTree.expandNode(destinationDataItem, destinationItem.item.index);
            }
        } else {
            // assign the same parent id as the item that it's moved to
            sourceDataItem.reportsTo = destinationDataItem.reportsTo;

            // possible positions left are DropPosition.After and DropPosition.Before
            const shiftIndex = dropPosition === DropPosition.After ? 1 : 0;
            const targetIndex = this.employees.indexOf(destinationDataItem) + shiftIndex;

            // insert the moved item
            this.employees.splice(targetIndex, 0, sourceDataItem);
        }
    }

    private removeItem({ sourceItem, sourceTree }: TreeItemAddRemoveArgs): void {
        // remove the item instance from its intial position
        const sourceItemIndex = this.employees.indexOf(sourceItem.item.dataItem);
        this.employees.splice(sourceItemIndex, 1);

        if (sourceItem.parent) {
            const parentEmployee: Employee = sourceItem.parent.item.dataItem;

            // update the sourceItem parent status
            parentEmployee.hasEmployees = this.hasLoadedEmployees(parentEmployee);

            // if the item parent no longer has child nodes - collapse it to remove its key from the expandedKeys collection
            if (!parentEmployee.hasEmployees) {
                sourceTree.collapseNode(sourceItem.parent.item.dataItem, sourceItem.parent.item.index);
            }
        }
    }

    private rebindTreeView(): void {
        this.treeview.nodes = this.employees.filter(employee => !employee.reportsTo);
    }

    private hasLoadedEmployees(employee: Employee): boolean {
        const subordinateEmployees = this.employees.filter(e => e.reportsTo === employee.employeeId);
        return subordinateEmployees.length > 0;
    }
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';
import { TreeViewModule } from '@progress/kendo-angular-treeview';

import { AppComponent } from './app.component';
import { EmployeesService } from './employees.service';
import { EmployeesDataSourceDirective } from './employees-data-source.directive';

@NgModule({
    bootstrap: [AppComponent],
    declarations: [
        AppComponent,
        EmployeesDataSourceDirective
    ],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        TreeViewModule,
        HttpClientModule,
        HttpClientJsonpModule
    ],
    providers: [EmployeesService]
})
export class AppModule { }

export class Employee {
    public employeeId: number;
    public fullName: string;
    public hasEmployees: boolean;
    public reportsTo?: number;

    constructor(initializer: { EmployeeId: number, FullName: string, HasEmployees: boolean, ReportsTo?: number }) {
        this.employeeId = initializer.EmployeeId;
        this.fullName = initializer.FullName;
        this.hasEmployees = initializer.HasEmployees;
        this.reportsTo = initializer.ReportsTo;
    }
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { Employee } from './employee';

@Injectable()
export class EmployeesService {

    private baseUrl = 'https://demos.telerik.com/kendo-ui/service/Employees';

    constructor(private http: HttpClient) { }

    public fetchRootLevelEmployees(): Observable<Employee[]> {
        return this.http.jsonp<any[]>(this.baseUrl, 'callback')
            .pipe(map(employees => employees.map(employee => new Employee(employee))));
    }

    public fetchSubordinateEmployees(employeeId: number): Observable<Employee[]> {
        return this.http.jsonp<any[]>(`${this.baseUrl}?employeeId=${employeeId}`, 'callback')
            .pipe(map(employees => employees.map(employee => new Employee(employee))));
    }
}
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

Hierarchical Heterogeneous Data

When using the Drag and Drop with heterogeneous data, some constraints need to be applied on which Drag and Drop operations are allowed.

The following example demonstrates remote binding to heterogeneous data, where items are allowed to be moved only in the same hierarchical level (categories can only be reordered, products can only be reordered or transferred to another category).

import { Component } from '@angular/core';
import { DropAction, TreeItemLookup } from '@progress/kendo-angular-treeview';

const isCategory = (item: any) => 'categoryId' in item;
const isProduct = (item: any) => 'productId' in item;

@Component({
    selector: 'my-app',
    template: `
        <kendo-treeview
            categoriesDataSource
            kendoTreeViewDragAndDrop
            kendoTreeViewExpandable
            [expandBy]="'categoryId'"
        >
            <ng-template
                kendoTreeViewDragClueTemplate
                let-action="action"
                let-sourceItem="sourceItem"
                let-destinationItem="destinationItem"
                let-text="text"
            >
                <span class="k-drag-status k-icon" [ngClass]="getDragStatus(action, sourceItem, destinationItem)"></span>
                <span>{{ text }}</span>
            </ng-template>
        </kendo-treeview>
    `
})
export class AppComponent {
    public getDragStatus(action: DropAction, sourceItem: TreeItemLookup, destinationItem: TreeItemLookup): string {
        if (!(sourceItem && destinationItem)) {
            return 'k-i-cancel';
        }

        const sourceDataItem = sourceItem.item.dataItem;
        const destinationDataItem = destinationItem.item.dataItem;

        // mark invalid drop action attempt (categories cannot be dragged
        // into other categories, products cannot be taken out as categories, etc.)
        const invalidCategoryDropTarget = action === DropAction.Add || isProduct(destinationDataItem);
        const invalidProductDropTarget = (action === DropAction.Add && isProduct(destinationDataItem)) ||
            (action !== DropAction.Add && isCategory(destinationDataItem));

        if ((isCategory(sourceDataItem) && invalidCategoryDropTarget) || (isProduct(sourceDataItem) && invalidProductDropTarget)) {
            return 'k-i-cancel';
        }

        switch (action) {
            case DropAction.Add: return 'k-i-plus';
            case DropAction.InsertTop: return 'k-i-insert-up';
            case DropAction.InsertBottom: return 'k-i-insert-down';
            case DropAction.InsertMiddle: return 'k-i-insert-middle';
            case DropAction.Invalid:
            default: return 'k-i-cancel';
        }
    }
}
import { Directive, OnInit } from '@angular/core';
import { tap, filter, take } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { TreeItemAddRemoveArgs, DropPosition, TreeItemLookup, TreeViewComponent, ItemLookup, TreeItemDropEvent } from '@progress/kendo-angular-treeview';
import { Category } from './category';
import { CategoriesService } from './categories.service';
import { Product } from './product';

const expandDropTarget = (dropTarget: TreeItemLookup, treeView: TreeViewComponent): void => {
    if (!treeView.isExpanded(dropTarget.item.dataItem, dropTarget.item.index)) {
        treeView.expandNode(dropTarget.item.dataItem, dropTarget.item.index);
    }
};

const collapseEmptyParent = (parent: ItemLookup, parentNodes: any[], treeview: TreeViewComponent): void => {
    if (parent && parentNodes.length === 0 && treeview.isExpanded(parent.item.dataItem, parent.item.index)) {
        treeview.collapseNode(parent.item.dataItem, parent.item.index);
    }
};

const isCategory = (item: any) => 'categoryId' in item;
const isProduct = (item: any) => 'productId' in item;

@Directive({
    selector: '[categoriesDataSource]'
})
export class CategoriesDataSourceDirective implements OnInit {

    public categories: Category[] = [];

    constructor(
        private categoriesService: CategoriesService,
        private treeview: TreeViewComponent
    ) { }

    public ngOnInit(): void {
        this.treeview.children = this.fetchChildren.bind(this);
        this.treeview.hasChildren = this.hasChildren.bind(this);
        this.treeview.textField = ['categoryName', 'productName'];
        this.treeview.nodeDrop.subscribe(this.handleDrop.bind(this));

        this.categoriesService.fetchCategories().subscribe(nodes => {
            this.categories = nodes;
            this.treeview.nodes = nodes;
        });
    }

    public hasChildren(item: any): boolean {
        return isCategory(item) && (!item.products || item.products.length);
    }

    public fetchChildren = (category: Category): Observable<Product[]> => {
        if (category.products && category.products.length) {
            return of(category.products);
        }

        return this.categoriesService
            .fetchProducts(category.categoryId)
            .pipe(tap(products => category.products = products));
    }

    public handleDrop(args: TreeItemDropEvent): void {
        const sourceDataItem = args.sourceItem.item.dataItem;
        const destinationDataItem = args.destinationItem.item.dataItem;

        // disallow invalid drag-and-drop actions (categories cannot be dragged
        // into other categories, products cannot be taken out as categories, etc.)
        const invalidCategoryDropTarget = args.dropPosition === DropPosition.Over || isProduct(destinationDataItem);
        const invalidProductDropTarget = (args.dropPosition === DropPosition.Over && isProduct(destinationDataItem)) ||
            (args.dropPosition !== DropPosition.Over && isCategory(destinationDataItem));

        if ((isCategory(sourceDataItem) && invalidCategoryDropTarget) || (isProduct(sourceDataItem) && invalidProductDropTarget)) {
            args.setValid(false);
            return;
        }

        this.removeItem(args);
        this.addItem(args);
    }

    private addItem({ sourceItem, destinationItem, dropPosition, destinationTree }: TreeItemAddRemoveArgs): void {
        const sourceDataItem = sourceItem.item.dataItem;
        const destinationDataItem = destinationItem.item.dataItem;

        if (dropPosition === DropPosition.Over) {
            if (!this.hasChildren(destinationDataItem)) {
                // drop target has no children => create a new array with the moved item
                destinationDataItem.products = [sourceDataItem];

            } else if (destinationTree.isExpanded(destinationDataItem, destinationItem.item.index)) {
                // drop target has children and is expanded => just push the moved item
                destinationDataItem.products.push(sourceDataItem);

            } else {
                // drop target has children but is collapsed (children not yet loaded)
                // push the moved item to the collection once it's loaded
                destinationTree.childrenLoaded
                    .pipe(
                        filter(({ item }) => item.dataItem === destinationDataItem),
                        take(1)
                    )
                    .subscribe(
                        () => destinationDataItem.products.push(sourceDataItem)
                    );
            }

            // expand the node that was dropped into if it's collapsed
            expandDropTarget(destinationItem, destinationTree);
        } else {
            const sourceParentNodes = this.getParentChildren(destinationItem);

            // possible positions left are DropPosition.After and DropPosition.Before
            const shiftIndex = dropPosition === DropPosition.After ? 1 : 0;
            const targetIndex = sourceParentNodes.indexOf(destinationDataItem) + shiftIndex;

            // insert the moved item
            sourceParentNodes.splice(targetIndex, 0, sourceDataItem);
        }
    }

    private removeItem({ sourceItem, sourceTree }: TreeItemAddRemoveArgs): void {
        const sourceParentNodes = this.getParentChildren(sourceItem);
        const sourceItemIndex = sourceParentNodes.indexOf(sourceItem.item.dataItem);

        // remove the item instance from its intial position
        sourceParentNodes.splice(sourceItemIndex, 1);

        // if the item parent no longer has child nodes - collapse it to remove its key from the expandedKeys collection
        if (sourceItem.parent) {
            collapseEmptyParent(sourceItem.parent, sourceParentNodes, sourceTree);
        }
    }

    private getParentChildren(treeItem: TreeItemLookup): any[] {
        // if the item doesn't have a parent, it's at root level, so return the whole nodes collection
        return treeItem.parent ?
            (treeItem.parent.item.dataItem as Category).products :
            this.categories;
    }
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';
import { TreeViewModule } from '@progress/kendo-angular-treeview';

import { AppComponent } from './app.component';
import { CategoriesService } from './categories.service';
import { CategoriesDataSourceDirective } from './categories-data-source.directive';

@NgModule({
    bootstrap: [AppComponent],
    declarations: [
        AppComponent,
        CategoriesDataSourceDirective
    ],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        TreeViewModule,
        HttpClientModule,
        HttpClientJsonpModule
    ],
    providers: [CategoriesService]
})
export class AppModule { }

import { Product } from './product';

export class Category {
    public categoryId: number;
    public categoryName: string;
    public products: Product[];

    constructor(initializer: { CategoryID: number, CategoryName: string }) {
        this.categoryId = initializer.CategoryID;
        this.categoryName = initializer.CategoryName;
    }
}
export class Product {
    public productId: number;
    public productName: string;

    constructor(initializer: { ProductID: number, ProductName: string }) {
        this.productId = initializer.ProductID;
        this.productName = initializer.ProductName;
    }
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { Category } from './category';
import { Product } from './product';

@Injectable()
export class CategoriesService {

    private baseUrl = 'https://odatasampleservices.azurewebsites.net/V4/Northwind/Northwind.svc';

    constructor(private http: HttpClient) { }

    public fetchCategories(): Observable<Category[]> {
        return this.fetch(`${this.baseUrl}/Categories`)
            .pipe(map(categories => categories.map(category => new Category(category))));
    }

    public fetchProducts(categoryID: number): Observable<Product[]> {
        return this.fetch(`${this.baseUrl}/Categories(${categoryID})/Products`)
            .pipe(map(products => products.map(product => new Product(product))));
    }

    private fetch(url: string): Observable<any[]> {
        return this.http.get(url)
            .pipe(map((response: any) => response.value));
    }
}
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

Multiple TreeViews

The Drag and Drop directive allows for items to be transferred from one TreeView to another. To mark a TreeView instance as a valid drop target, list it under the dropZoneTreeViews property of the other TreeView.

Note that the nodeDrop and addItem events are triggered on the target TreeView, while the removeItem event - on the source TreeView.

The following example also demonstrates the node copy functionality (allowCopy), and a custom emptyItemPlaceholder directive which adds an item placeholder to the TreeView once all its items are transferred away.

import { Component } from '@angular/core';
import { TreeItemDropEvent, DropPosition } from '@progress/kendo-angular-treeview';

@Component({
    selector: 'my-app',
    template: `
        <div class="example-config">
            <input type="checkbox" id="allow-copy" class="k-checkbox" name="allow-copy" [(ngModel)]="allowCopy">
            <label for="allow-copy" class="k-checkbox-label">
                <strong>Allow Copy</strong> (hold the CTRL key on drop to copy the dragged item)
            </label>
        </div>
        <div class="example-container">
            <div>
                <h5>Furnishing</h5>
                <kendo-treeview
                    #listA
                    [dropZoneTreeViews]="[listB]"
                    kendoTreeViewHierarchyBinding
                    [childrenField]="'items'"
                    kendoTreeViewDragAndDrop
                    [allowCopy]="allowCopy"
                    kendoTreeViewDragAndDropEditing
                    emptyItemPlaceholder
                    kendoTreeViewExpandable
                    [expandBy]="'id'"
                    [textField]="'text'"
                    [nodes]="furnishing"
                    [expandedKeys]="[1, 5]"
                    (nodeDrop)="handleDrop($event)"
                >
                </kendo-treeview>
            </div>

            <div>
                <h5>Maintenance</h5>
                <kendo-treeview
                    #listB
                    [dropZoneTreeViews]="[listA]"
                    kendoTreeViewHierarchyBinding
                    [childrenField]="'items'"
                    kendoTreeViewDragAndDrop
                    [allowCopy]="allowCopy"
                    kendoTreeViewDragAndDropEditing
                    emptyItemPlaceholder
                    kendoTreeViewExpandable
                    [expandBy]="'id'"
                    [textField]="'text'"
                    [nodes]="maintenance"
                    [expandedKeys]="[9, 13]"
                    (nodeDrop)="handleDrop($event)"
                >
                </kendo-treeview>
            </div>
        </div>
    `,
    styles: [`
        .example-container {
            display: flex;
        }
        .example-container > div {
            flex: 1;
        }
    `]
})
export class AppComponent {
    public allowCopy = true;

    public furnishing: any[] = [
        {
            id: 1, text: 'Furniture', items: [
                { id: 2, text: 'Tables & Chairs' },
                { id: 3, text: 'Sofas' },
                { id: 4, text: 'Occasional Furniture' }
            ]
        },
        {
            id: 5, text: 'Decor', items: [
                { id: 6, text: 'Bed Linen' },
                { id: 7, text: 'Curtains & Blinds' },
                { id: 8, text: 'Carpets' }
            ]
        }
    ];

    public maintenance: any[] = [
        {
            id: 9, text: 'Storage', items: [
                { id: 10, text: 'Wall Shelving' },
                { id: 11, text: 'Floor Shelving' },
                { id: 12, text: 'Kids Storage' }
            ]
        },
        {
            id: 13, text: 'Lights', items: [
                { id: 14, text: 'Ceiling' },
                { id: 15, text: 'Table' },
                { id: 16, text: 'Floor' }
            ]
        }
    ];

    public handleDrop(event: TreeItemDropEvent): void {
        // prevent the default to prevent the drag-and-drop directive from emitting `addItem` and inserting items with duplicate IDs
        if (this.allowCopy && event.originalEvent.ctrlKey) {
            event.preventDefault();

            // clone the item and its children and assign them new IDs
            const itemWithNewId = this.assignNewIds(event.sourceItem.item.dataItem, 'id', 'items');

            // if the target is an empty placeholder, remove it and push the new item to the destination tree nodes
            if (event.destinationItem.item.dataItem.placeholder) {
                const placeholderItemIndex = event.destinationTree.nodes.indexOf(event.destinationItem.item.dataItem);
                event.destinationTree.nodes.splice(placeholderItemIndex, 1, itemWithNewId);
                return;
            }

            // manually insert the new item and its children at the targeted position
            if (event.dropPosition === DropPosition.Over) {
                event.destinationItem.item.dataItem.items = event.destinationItem.item.dataItem.items || [];
                event.destinationItem.item.dataItem.items.push(itemWithNewId);

                if (!event.destinationTree.isExpanded(event.destinationItem.item.dataItem, event.destinationItem.item.index)) {
                    event.destinationTree.expandNode(event.destinationItem.item.dataItem, event.destinationItem.item.index);
                }
            } else {
                const parentChildren: any[] = event.destinationItem.parent ?
                    event.destinationItem.parent.item.dataItem.items :
                    event.destinationTree.nodes;

                const shiftIndex = event.dropPosition === DropPosition.After ? 1 : 0;
                const targetIndex = parentChildren.indexOf(event.destinationItem.item.dataItem) + shiftIndex;

                parentChildren.splice(targetIndex, 0, itemWithNewId);
            }
        }
    }

    // recursively clones and assigns new ids to the root level item and all its children
    private assignNewIds(item: any, idField: string, childrenField: string): any {
        const result = Object.assign({}, item, { [idField]: Math.random() });

        if (result[childrenField] && result[childrenField].length) {
            result[childrenField] = result[childrenField].map(childItem => this.assignNewIds(childItem, idField, childrenField));
        }

        return result;
    }
}
import { Directive, AfterViewInit } from '@angular/core';
import { TreeViewComponent, TreeItemDropEvent, TreeItemDragStartEvent, DropPosition } from '@progress/kendo-angular-treeview';

const EMPTY_ITEM = { text: '[Empty]', placeholder: true };

@Directive({
    selector: '[emptyItemPlaceholder]'
})
export class EmptyItemPlaceholderDirective implements AfterViewInit {

    constructor(private treeview: TreeViewComponent) { }

    public ngAfterViewInit(): void {
        this.treeview.addItem.subscribe(this.handleAdd.bind(this));
        this.treeview.removeItem.subscribe(this.handleRemoveItem.bind(this));
        this.treeview.nodeDragStart.subscribe(this.handleDragStart.bind(this));
    }

    private handleAdd(event: TreeItemDropEvent): void {
        const placeholderItem = this.treeview.nodes.find(item => item.placeholder);
        if (placeholderItem) {
            // remove the empty item placeholder
            const index = this.treeview.nodes.indexOf(placeholderItem);
            this.treeview.nodes.splice(index, 1);

            // if the item was dropped into the empty item, we've just spliced it as well, so restore it back
            if (event.dropPosition === DropPosition.Over) {
                this.treeview.nodes.push(event.sourceItem.item.dataItem);
            }
        }
    }

    private handleRemoveItem(): void {
        if (this.treeview.nodes.length > 0) {
            return;
        }

        this.treeview.nodes.push(Object.assign({}, EMPTY_ITEM));
    }

    private handleDragStart(event: TreeItemDragStartEvent): void {
        if (event.sourceItem.item.dataItem.placeholder) {
            event.preventDefault();
        }
    }
}
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TreeViewModule } from '@progress/kendo-angular-treeview';

import { AppComponent } from './app.component';
import { EmptyItemPlaceholderDirective } from './empty-item-placeholder.directive';

@NgModule({
    bootstrap: [AppComponent],
    declarations: [
        AppComponent,
        EmptyItemPlaceholderDirective
    ],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        TreeViewModule,
        FormsModule
    ]
})
export class AppModule { }

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

Expanded, Selected, Checked State

During Drag and Drop the indices of the reordered nodes are updated according to their new position, so in order to persist the expanded, selected or checked state, it's best not to rely on indices at all.

The following example show-cases how the state of each node is tracked by an object property value.

import { Component } from '@angular/core';

@Component({
    selector: 'my-app',
    template: `
        <kendo-treeview
            kendoTreeViewDragAndDrop
            kendoTreeViewDragAndDropEditing
            kendoTreeViewFlatDataBinding
            [idField]="'employeeId'"
            [parentIdField]="'reportsTo'"
            [nodes]="employees"
            [textField]="'name'"
            kendoTreeViewExpandable
            [expandBy]="'employeeId'"
            [expandedKeys]="expandedKeys"
            kendoTreeViewSelectable
            [selectBy]="'employeeId'"
            kendoTreeViewCheckable
            [checkBy]="'employeeId'"
        >
        </kendo-treeview>
    `
})
export class AppComponent {
    public expandedKeys: number[] = [2];

    public employees: any[] = [
        { employeeId: 2, name: 'Andrew Fuller', reportsTo: null },
        { employeeId: 1, name: 'Nancy Davolio', reportsTo: 2 },
        { employeeId: 3, name: 'Janet Leverling', reportsTo: 2 },
        { employeeId: 4, name: 'Margaret Peacock', reportsTo: 2 },
        { employeeId: 5, name: 'Steven Buchanan', reportsTo: 2 },
        { employeeId: 8, name: 'Laura Callahan', reportsTo: 2 },
        { employeeId: 6, name: 'Michael Suyama', reportsTo: 5 },
        { employeeId: 7, name: 'Robert King', reportsTo: 5 },
        { employeeId: 9, name: 'Anne Dodsworth', reportsTo: 5 }
    ];
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TreeViewModule } from '@progress/kendo-angular-treeview';

import { AppComponent } from './app.component';

@NgModule({
  bootstrap:    [ AppComponent ],
  declarations: [ AppComponent ],
  imports:      [ BrowserModule, BrowserAnimationsModule, TreeViewModule]
})
export class AppModule { }

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

Templates

The drag clue and drop hint can easily be customized according the the design needs.

import { Component } from '@angular/core';
import { DropAction } from '@progress/kendo-angular-treeview';

@Component({
    selector: 'my-app',
    template: `
        <kendo-treeview
            kendoTreeViewDragAndDrop
            kendoTreeViewDragAndDropEditing
            kendoTreeViewFlatDataBinding
            [idField]="'employeeId'"
            [parentIdField]="'reportsTo'"
            [nodes]="employees"
            [textField]="'name'"
            kendoTreeViewExpandable
            [expandBy]="'employeeId'"
            [expandedKeys]="expandedKeys"
        >
            <ng-template kendoTreeViewDragClueTemplate let-action="action" let-text="text">
                [{{ getActionText(action) }}]: {{ text }}
            </ng-template>
            <ng-template kendoTreeViewDropHintTemplate>
                <div class="drop-hint"></div>
            </ng-template>
        </kendo-treeview>
    `,
    styles: [`
        .drop-hint {
            width: 160px;
            height: 1px;
            background: #333;
        }
    `]
})
export class AppComponent {
    public expandedKeys: number[] = [2];

    public employees: any[] = [
        { employeeId: 2, name: 'Andrew Fuller', reportsTo: null },
        { employeeId: 1, name: 'Nancy Davolio', reportsTo: 2 },
        { employeeId: 3, name: 'Janet Leverling', reportsTo: 2 },
        { employeeId: 4, name: 'Margaret Peacock', reportsTo: 2 },
        { employeeId: 5, name: 'Steven Buchanan', reportsTo: 2 },
        { employeeId: 8, name: 'Laura Callahan', reportsTo: 2 },
        { employeeId: 6, name: 'Michael Suyama', reportsTo: 5 },
        { employeeId: 7, name: 'Robert King', reportsTo: 5 },
        { employeeId: 9, name: 'Anne Dodsworth', reportsTo: 5 }
    ];

    public getActionText(action: DropAction): string {
        switch (action) {
            case DropAction.Add: return 'Add';
            case DropAction.InsertTop: return 'InsertTop';
            case DropAction.InsertMiddle: return 'InsertMiddle';
            case DropAction.InsertBottom: return 'InsertBottom';
            case DropAction.Invalid:
            default: return 'Invalid';
        }
    }
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TreeViewModule } from '@progress/kendo-angular-treeview';

import { AppComponent } from './app.component';

@NgModule({
  bootstrap:    [ AppComponent ],
  declarations: [ AppComponent ],
  imports:      [ BrowserModule, BrowserAnimationsModule, TreeViewModule]
})
export class AppModule { }

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

In this article