Load More Button

The TreeView enables you to display only a limited number of nodes in each level, when the provided data set is too big.

Local Data

To reduce the number of rendered nodes and display a Load more button at the end of each level,
apply the kendoTreeViewLoadMore directive to the TreeView and specify the desired initial pageSize.

The following example demonstrates how to limit the number of displayed nodes when all the available data is loaded into the TreeView.

import { Component } from '@angular/core';
import { generateHierarchicalData } from './data-generator';

@Component({
    selector: 'my-app',
    template: `
        <kendo-treeview
            [nodes]="nodes"
            [textField]="'text'"
            kendoTreeViewExpandable
            kendoTreeViewHierarchyBinding
            [childrenField]="'items'"
            kendoTreeViewLoadMore
            [pageSize]="10"
        >
        </kendo-treeview>
    `
})
export class AppComponent {
    // 500 root nodes, each with 500 child nodes => 250 000 in total
    public nodes: any[] = generateHierarchicalData(500, 2);
}
const names = ['Daryl', 'Sweeney', 'Guy', 'Wooten', 'Buffy', 'Weber', 'Hyacinth', 'Hood', 'Akeem', 'Carr', 'Rinah', 'Simon', 'Gage', 'Daniels', 'Constance', 'Vazquez', 'Darrel', 'Solis', 'Brian', 'Yang', 'Lillian', 'Bradshaw', 'Christian', 'Palmer', 'Summer', 'Mosley', 'Barry', 'Ayers', 'Keiko', 'Espinoza', 'Candace', 'Pickett', 'Mia', 'Caldwell', 'Thomas', 'Terry', 'Ruth', 'Downs', 'Yasir', 'Wilder', 'Flavia', 'Short', 'Aaron', 'Roach', 'Eric', 'Russell', 'Cheyenne', 'Olson', 'Shaine', 'Avila', 'Chantale', 'Long', 'Dane', 'Cruz', 'Regan', 'Patterson', 'Drew', 'Mckay', 'Bevis', 'Miller', 'Bruce', 'Mccarty', 'Ocean', 'Blair', 'Guinevere', 'Osborn', 'Olga', 'Strong', 'Robert', 'Orr', 'Odette', 'Sears', 'Zelda', 'Medina', 'Priscilla', 'Frank', 'Ursula', 'Holmes', 'Melvin', 'Carrillo', 'Martha', 'Chavez', 'Oren', 'Fox', 'Amos', 'Barr'];
let id = 0;

const generateItem = (): any => {
    return {
        id: ++id,
        text: `${names[Math.floor(Math.random() * names.length)]} ${names[Math.floor(Math.random() * names.length)]}`
    };
};

export const generateHierarchicalData = (nodesPerLevel: number, levels: number): any[] => {
    if (levels === 0) {
        return null;
    }

    return new Array(nodesPerLevel)
        .fill(null)
        .map(() => {
            const node = generateItem();

            const childNodes = generateHierarchicalData(nodesPerLevel, levels - 1);
            if (childNodes != null) {
                node.items = childNodes;
            }

            return node;
        });
};
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 Data

The kendoTreeViewLoadMore directive can be configured to fetch the new nodes on demand from the server.
To achieve the aforementioned result, the following configuration data has to be provided:

  • kendoTreeViewLoadMore - A function that acquires new child nodes that will be called each time a Load more button is pressed. The function receives a LoadMoreRequestArgs object as a single parameter.
  • pageSize - The initial page size for the each level.
  • totalField - The field that holds the information how many children in total the node has.
  • totalRootNodes - As the root level nodes are not associated with any parent node, the total number of root nodes has to be provided exclusively.
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { LoadMoreRequestArgs } from '@progress/kendo-angular-treeview';

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

@Component({
    selector: 'my-app',
    template: `
        <kendo-treeview
            [nodes]="categories"
            [textField]="['categoryName', 'productName']"
            [children]="fetchProducts"
            [hasChildren]="hasProducts"
            kendoTreeViewExpandable
            [kendoTreeViewLoadMore]="loadMoreItems"
            [pageSize]="pageSize"
            [totalField]="'productsCount'"
            [totalRootNodes]="totalCategoriesCount"
        >
        </kendo-treeview>
    `
})
export class AppComponent implements OnInit {
    public categories: Category[] = [];

    public pageSize = 3;
    public totalCategoriesCount: number;

    constructor(private categoriesService: CategoriesService) { }

    public ngOnInit(): void {
        const skip = 0;
        this.categoriesService.getCatergories(skip, this.pageSize)
            .subscribe((data: { categories: Category[], total: number }) => {
                this.categories = data.categories;
                this.totalCategoriesCount = data.total;
            });
    }

    public hasProducts(item: any): boolean {
        return 'productsCount' in item;
    }

    public fetchProducts = (category: Category): Observable<Product[]> => {
        const skip = 0;
        const take = this.pageSize;
        return this.categoriesService.getProducts(category.id, skip, take);
    }

    // the loadMoreItems callback will be executed every time a load more button is pressed
    // the `LoadMoreRequestArgs` object holds data about the group parent where a Load more button has been pressed
    public loadMoreItems = (args: LoadMoreRequestArgs): Observable<any[]> => {
        // if there's no parent item, root level nodes are requested
        if (!args.dataItem) {
            return this.categoriesService.getCatergories(args.skip, args.take)
                .pipe(map(data => data.categories));

        } else {
            return this.categoriesService.getProducts(args.dataItem.id, args.skip, args.take);
        }
    }
}
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, tap } from 'rxjs/operators';

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

@Injectable()
export class CategoriesService {

    private productsCache: Map<number, Product[]> = new Map();

    private baseUrl = 'https://services.odata.org/V4/Northwind/Northwind.svc';

    constructor(private http: HttpClient) { }

    public getCatergories(skip: number, take: number): Observable<{ categories: Category[], total: number }> {
        const params = new HttpParams({
            fromObject: {
                $skip: skip.toString(),
                $top: take.toString(),
                $count: 'true',
                $expand: 'Products'
            }
        });

        return this.http.get<{ value: any[], '@odata.count': number }>(`${this.baseUrl}/Categories`, { params })
            .pipe(
                map(response => ({
                    categories: response.value.map(category => new Category(category)),
                    total: response['@odata.count']
                }))
            );
    }

    public getProducts(categoryId: number, skip: number, take: number): Observable<Product[]> {
        if (skip === 0 && this.productsCache.has(categoryId)) {
            return of(this.productsCache.get(categoryId));
        }

        const params = new HttpParams({
            fromObject: {
                $skip: skip.toString(),
                $top: take.toString(),
                $filter: `CategoryID eq ${categoryId}`
            }
        });

        return this.http.get<{ value: any[] }>(`${this.baseUrl}/Products`, { params })
            .pipe(
                map(response => response.value.map(product => new Product(product))),
                tap(products => {
                    const cachedData = this.productsCache.get(categoryId) || [];
                    this.productsCache.set(categoryId, cachedData.concat(products));
                })
            );
    }
}
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';

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

export class Category {
    public id: number;
    public categoryName: string;
    public productsCount: number;

    constructor(initializer: { CategoryID: number, CategoryName: string, Products: any[] }) {
        this.id = initializer.CategoryID;
        this.categoryName = initializer.CategoryName;
        this.productsCount = initializer.Products && initializer.Products.length;
    }
}
export class Product {
    public id: number;
    public productName: string;

    constructor(initializer: { ProductID: number, ProductName: string }) {
        this.id = initializer.ProductID;
        this.productName = initializer.ProductName;
    }
}
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

enableProdMode();

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

Managing Page Sizes

The page size for each data item children group is initially set through the the pageSize
property of the kendoTreeViewLoadMore directive. It can, however, be read and manually
changed for any node through the TreeView getNodePageSize and setNodePageSize methods.

import { Component, ViewChild } from '@angular/core';
import { TreeViewComponent } from '@progress/kendo-angular-treeview';
import { generateHierarchicalData } from './data-generator';

@Component({
    selector: 'my-app',
    template: `
        <div class="example-config">
            <strong>Root nodes collection page size: {{ rootCollectionPageSize }}</strong> |
            <button class="k-button" (click)="updateRootPageSize(rootCollectionPageSize + 1)">Increment</button> |
            <button class="k-button" (click)="updateRootPageSize(rootCollectionPageSize - 1)">Decrement</button>
            <br />
            <strong>First node children page size: {{ firstNodePageSize }}</strong> |
            <button class="k-button" (click)="updateFirstNodePageSize(firstNodePageSize + 1)">Increment</button> |
            <button class="k-button" (click)="updateFirstNodePageSize(firstNodePageSize - 1)">Decrement</button>
        </div>
        <kendo-treeview
            #treeview
            [nodes]="nodes"
            [textField]="'text'"
            kendoTreeViewExpandable
            [expandedKeys]="expandedKeys"
            kendoTreeViewHierarchyBinding
            [childrenField]="'items'"
            kendoTreeViewLoadMore
            [pageSize]="10"
        >
        </kendo-treeview>
    `
})
export class AppComponent {
    @ViewChild('treeview', { static: true })
    public treeview: TreeViewComponent;

    public nodes: any[] = generateHierarchicalData(500, 2);
    public expandedKeys: string[] = ['0'];

    public get rootCollectionPageSize(): number {
        // as the root nodes are not associated with any parent item, pass `null` for parent data item
        const rootNodesParent = null;
        return this.treeview.getNodePageSize(rootNodesParent);
    }

    public get firstNodePageSize(): number {
        const firstNode = this.nodes[0];
        return this.treeview.getNodePageSize(firstNode);
    }

    public updateFirstNodePageSize(pageSize: number): void {
        const firstNode = this.nodes[0];
        this.treeview.setNodePageSize(firstNode, pageSize);
    }

    public updateRootPageSize(pageSize: number): void {
        // as the root nodes are not associated with any parent item, pass `null` for parent data item
        const rootNodesParent = null;
        this.treeview.setNodePageSize(rootNodesParent, pageSize);
    }
}
const names = ['Daryl', 'Sweeney', 'Guy', 'Wooten', 'Buffy', 'Weber', 'Hyacinth', 'Hood', 'Akeem', 'Carr', 'Rinah', 'Simon', 'Gage', 'Daniels', 'Constance', 'Vazquez', 'Darrel', 'Solis', 'Brian', 'Yang', 'Lillian', 'Bradshaw', 'Christian', 'Palmer', 'Summer', 'Mosley', 'Barry', 'Ayers', 'Keiko', 'Espinoza', 'Candace', 'Pickett', 'Mia', 'Caldwell', 'Thomas', 'Terry', 'Ruth', 'Downs', 'Yasir', 'Wilder', 'Flavia', 'Short', 'Aaron', 'Roach', 'Eric', 'Russell', 'Cheyenne', 'Olson', 'Shaine', 'Avila', 'Chantale', 'Long', 'Dane', 'Cruz', 'Regan', 'Patterson', 'Drew', 'Mckay', 'Bevis', 'Miller', 'Bruce', 'Mccarty', 'Ocean', 'Blair', 'Guinevere', 'Osborn', 'Olga', 'Strong', 'Robert', 'Orr', 'Odette', 'Sears', 'Zelda', 'Medina', 'Priscilla', 'Frank', 'Ursula', 'Holmes', 'Melvin', 'Carrillo', 'Martha', 'Chavez', 'Oren', 'Fox', 'Amos', 'Barr'];
let id = 0;

const generateItem = (): any => {
    return {
        id: ++id,
        text: `${names[Math.floor(Math.random() * names.length)]} ${names[Math.floor(Math.random() * names.length)]}`
    };
};

export const generateHierarchicalData = (nodesPerLevel: number, levels: number): any[] => {
    if (levels === 0) {
        return null;
    }

    return new Array(nodesPerLevel)
        .fill(null)
        .map(() => {
            const node = generateItem();

            const childNodes = generateHierarchicalData(nodesPerLevel, levels - 1);
            if (childNodes != null) {
                node.items = childNodes;
            }

            return node;
        });
};
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);

Button Template

The Load more button can be fully customized according the the application needs by using the
kendoTreeViewLoadMoreButtonTemplate directive.

import { Component } from '@angular/core';
import { generateHierarchicalData } from './data-generator';

@Component({
    selector: 'my-app',
    template: `
        <kendo-treeview
            [nodes]="nodes"
            [textField]="'text'"
            kendoTreeViewExpandable
            kendoTreeViewHierarchyBinding
            [childrenField]="'items'"
            kendoTreeViewLoadMore
            [pageSize]="10"
        >
            <ng-template kendoTreeViewNodeTemplate let-dataItem>
                <span class="k-icon k-i-file"></span>
                <span>{{ dataItem.text }}</span>
            </ng-template>
            <ng-template kendoTreeViewLoadMoreButtonTemplate>
                <span class="k-icon k-i-folder-more"></span>
                <span>Load more...</span>
            </ng-template>
        </kendo-treeview>
    `
})
export class AppComponent {
    public nodes: any[] = generateHierarchicalData(500, 2);
}
const names = ['Daryl', 'Sweeney', 'Guy', 'Wooten', 'Buffy', 'Weber', 'Hyacinth', 'Hood', 'Akeem', 'Carr', 'Rinah', 'Simon', 'Gage', 'Daniels', 'Constance', 'Vazquez', 'Darrel', 'Solis', 'Brian', 'Yang', 'Lillian', 'Bradshaw', 'Christian', 'Palmer', 'Summer', 'Mosley', 'Barry', 'Ayers', 'Keiko', 'Espinoza', 'Candace', 'Pickett', 'Mia', 'Caldwell', 'Thomas', 'Terry', 'Ruth', 'Downs', 'Yasir', 'Wilder', 'Flavia', 'Short', 'Aaron', 'Roach', 'Eric', 'Russell', 'Cheyenne', 'Olson', 'Shaine', 'Avila', 'Chantale', 'Long', 'Dane', 'Cruz', 'Regan', 'Patterson', 'Drew', 'Mckay', 'Bevis', 'Miller', 'Bruce', 'Mccarty', 'Ocean', 'Blair', 'Guinevere', 'Osborn', 'Olga', 'Strong', 'Robert', 'Orr', 'Odette', 'Sears', 'Zelda', 'Medina', 'Priscilla', 'Frank', 'Ursula', 'Holmes', 'Melvin', 'Carrillo', 'Martha', 'Chavez', 'Oren', 'Fox', 'Amos', 'Barr'];
let id = 0;

const generateItem = (): any => {
    return {
        id: ++id,
        text: `${names[Math.floor(Math.random() * names.length)]} ${names[Math.floor(Math.random() * names.length)]}`
    };
};

export const generateHierarchicalData = (nodesPerLevel: number, levels: number): any[] => {
    if (levels === 0) {
        return null;
    }

    return new Array(nodesPerLevel)
        .fill(null)
        .map(() => {
            const node = generateItem();

            const childNodes = generateHierarchicalData(nodesPerLevel, levels - 1);
            if (childNodes != null) {
                node.items = childNodes;
            }

            return node;
        });
};
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