Column spanning in Angular TreeGrid component

The column spanning feature in the Syncfusion® Angular TreeGrid allows merging adjacent cells horizontally, creating a visually appealing and informative layout. By defining the colSpan attribute in the queryCellInfo event, cells can be easily spanned and the appearance of the TreeGrid can be customized.

In the following example, Employee Davolio is scheduled for analysis from “9.00 AM” to “10.00 AM”, so those cells have been spanned.

import { NgModule, ViewChild } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { TreeGridModule } from '@syncfusion/ej2-angular-treegrid'
import { PageService, SortService, FilterService } from '@syncfusion/ej2-angular-treegrid'
import { Component, OnInit } from '@angular/core';
import { GridLine, Column, QueryCellInfoEventArgs, } from '@syncfusion/ej2-grids';
import { columnSpanData } from './datasource';

@Component({
    imports: [TreeGridModule,],
    providers: [PageService, SortService, FilterService],
    standalone: true,
    selector: 'app-container',
    template: `<ejs-treegrid [dataSource]='data' [height]='height' [width]='width' [gridLines]='gridLines' [treeColumnIndex]='1' childMapping="subtasks"
               [allowTextWrap]='textWrap' (queryCellInfo)='queryCellInfoEvent($event)'>
                <e-columns>
                   <e-column field='EmployeeID' headerText='Employee ID' width='150' textAlign="Right" isPrimaryKey=true></e-column>
                   <e-column field='EmployeeName' headerText='Employee Name' width='200'></e-column>
                   <e-column field='9:00' headerText='9:00 AM' width='120'></e-column>
                   <e-column field='9:30' headerText='9:30 AM' width='120'></e-column>
                   <e-column field='10:00' headerText='10:00 AM' width='120'></e-column>
                   <e-column field='10:30' headerText='10:30 AM' width='120'></e-column>
                   <e-column field='11:00' headerText='11:00 AM' width='120'></e-column>
                   <e-column field='11:30' headerText='11:30 AM' width='120'></e-column>
                   <e-column field='12:00' headerText='12:00 PM' width='120'></e-column>
                   <e-column field='12:30' headerText='12:30 PM' width='120'></e-column>
                   <e-column field='2:30' headerText='2:30 PM' width='120'></e-column>
                   <e-column field='3:00' headerText='3:00 PM' width='120'></e-column>
                   <e-column field='3:30' headerText='3:30 PM' width='120'></e-column>
                   <e-column field='4:00' headerText='4:00 PM' width='120'></e-column>
                   <e-column field='4:30' headerText='4:30 PM' width='120'></e-column>
                   <e-column field='5:00' headerText='5:00 PM' width='120'></e-column>
                </e-columns>
            </ejs-treegrid>`,
})
export class AppComponent implements OnInit {
    public data: object[] = [];
    public height: string | number = 0;
    public width: string | number = 0;
    public gridLines: GridLine = 'Both';
    public textWrap: boolean = false;

    public queryCellInfoEvent = (args: any) => {
        const datas: any = args.data as any;
        switch (datas.EmployeeID) {
            case 10001:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '2:30' ||
                    (args.column as Column).field === '4:30'
                ) {
                    (args.colSpan as number) = 2;
                } else if ((args.column as Column).field === '11:00') {
                    (args.colSpan as number) = 3;
                }
                break;
            case 10002:
                if (
                    (args.column as Column).field === '9:30' ||
                    (args.column as Column).field === '2:30' ||
                    (args.column as Column).field === '4:30'
                ) {
                    (args.colSpan as number) = 3;
                } else if ((args.column as Column).field === '11:00') {
                    (args.colSpan as number) = 4;
                }
                break;
            case 10003:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '11:30'
                ) {
                    (args.colSpan as number) = 3;
                } else if (
                    (args.column as Column).field === '10:30' ||
                    (args.column as Column).field === '3:30' ||
                    (args.column as Column).field === '4:30' ||
                    (args.column as Column).field === '2:30'
                ) {
                    (args.colSpan as number) = 2;
                }
                break;
            case 10004:
                if ((args.column as Column).field === '9:00') {
                    (args.colSpan as number) = 3;
                } else if ((args.column as Column).field === '11:00') {
                    (args.colSpan as number) = 4;
                } else if (
                    (args.column as Column).field === '4:00' ||
                    (args.column as Column).field === '2:30'
                ) {
                    (args.colSpan as number) = 2;
                }
                break;
            case 10005:
                if ((args.column as Column).field === '9:00') {
                    (args.colSpan as number) = 4;
                } else if ((args.column as Column).field === '11:30') {
                    (args.colSpan as number) = 3;
                } else if (
                    (args.column as Column).field === '3:30' ||
                    (args.column as Column).field === '4:30' ||
                    (args.column as Column).field === '2:30'
                ) {
                    (args.colSpan as number) = 2;
                }
                break;
            case 10006:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '4:30' ||
                    (args.column as Column).field === '2:30' ||
                    (args.column as Column).field === '3:30'
                ) {
                    (args.colSpan as number) = 2;
                } else if (
                    (args.column as Column).field === '10:00' ||
                    (args.column as Column).field === '11:30'
                ) {
                    (args.colSpan as number) = 3;
                }
                break;
            case 10007:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '3:00' ||
                    (args.column as Column).field === '10:30'
                ) {
                    (args.colSpan as number) = 2;
                } else if (
                    (args.column as Column).field === '11:30' ||
                    (args.column as Column).field === '4:00'
                ) {
                    (args.colSpan as number) = 3;
                }
                break;
            case 10008:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '10:30' ||
                    (args.column as Column).field === '2:30'
                ) {
                    (args.colSpan as number) = 3;
                } else if ((args.column as Column).field === '4:00') {
                    (args.colSpan as number) = 2;
                }
                break;
            case 10009:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '11:30'
                ) {
                    (args.colSpan as number) = 3;
                } else if (
                    (args.column as Column).field === '4:30' ||
                    (args.column as Column).field === '2:30'
                ) {
                    (args.colSpan as number) = 2;
                }
                break;
            case 100010:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '2:30' ||
                    (args.column as Column).field === '4:00' ||
                    (args.column as Column).field === '11:30'
                ) {
                    (args.colSpan as number) = 3;
                } else if ((args.column as Column).field === '10:30') {
                    (args.colSpan as number) = 2;
                }
                break;
        }
    };

    ngOnInit(): void {
        this.data = columnSpanData;
        this.gridLines = 'Both';
        this.height = 'auto';
        this.width = 'auto';
        this.textWrap = true;
    }
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));

Change the border color while column spanning

The border color for the spanned cells can be changed by using the queryCellInfo event. This event triggers before the cell element is appended to the TreeGrid element, allowing customization of the cell appearance including border styles.

import { NgModule, ViewChild } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { TreeGridModule } from '@syncfusion/ej2-angular-treegrid'
import { PageService, SortService, FilterService } from '@syncfusion/ej2-angular-treegrid'
import { Component, OnInit } from '@angular/core';
import { QueryCellInfoEventArgs, GridLine, Column, } from '@syncfusion/ej2-angular-grids';
import { columnSpanData } from './datasource';
import { EmitType } from '@syncfusion/ej2-base';

@Component({
    imports: [TreeGridModule,],
    providers: [PageService, SortService, FilterService],
    standalone: true,
    selector: 'app-container',
    template: `<ejs-treegrid [dataSource]='data' [height]='height' [width]='width' [gridLines]='gridLines' childMapping="subtasks" [treeColumnIndex]='1'
              [allowTextWrap]='textWrap' (queryCellInfo)='queryCellInfoEvent($event)'>
               <e-columns>
                  <e-column field='EmployeeID' headerText='Employee ID' width='150' textAlign="Right" isPrimaryKey=true></e-column>
                  <e-column field='EmployeeName' headerText='Employee Name' width='200'></e-column>
                  <e-column field='9:00' headerText='9:00 AM' width='120'></e-column>
                  <e-column field='9:30' headerText='9:30 AM' width='120'></e-column>
                  <e-column field='10:00' headerText='10:00 AM' width='120'></e-column>
                  <e-column field='10:30' headerText='10:30 AM' width='120'></e-column>
                  <e-column field='11:00' headerText='11:00 AM' width='120'></e-column>
                  <e-column field='11:30' headerText='11:30 AM' width='120'></e-column>
                  <e-column field='12:00' headerText='12:00 PM' width='120'></e-column>
                  <e-column field='12:30' headerText='12:30 PM' width='120'></e-column>
                  <e-column field='2:30' headerText='2:30 PM' width='120'></e-column>
                  <e-column field='3:00' headerText='3:00 PM' width='120'></e-column>
                  <e-column field='3:30' headerText='3:30 PM' width='120'></e-column>
                  <e-column field='4:00' headerText='4:00 PM' width='120'></e-column>
                  <e-column field='4:30' headerText='4:30 PM' width='120'></e-column>
                  <e-column field='5:00' headerText='5:00 PM' width='120'></e-column>
              </e-columns>
            </ejs-treegrid>`,
})
export class AppComponent implements OnInit {
    public data: object[] = columnSpanData;
    public height: string | number = 'auto';
    public width: string | number = 'auto';
    public gridLines: GridLine = 'Both';
    public textWrap: boolean = true;

    public queryCellInfoEvent: EmitType<QueryCellInfoEventArgs> = (
        args: QueryCellInfoEventArgs
    ) => {
        let data: any = args.data as any;
        switch (data.EmployeeID) {
            case 10001:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '2:30' ||
                    (args.column as Column).field === '4:30'
                ) {
                    args.colSpan = 2;
                } else if ((args.column as Column).field === '11:00') {
                    args.colSpan = 3;
                }
                break;
            case 10002:
                if (
                    (args.column as Column).field === '9:30' ||
                    (args.column as Column).field === '2:30' ||
                    (args.column as Column).field === '4:30'
                ) {
                    args.colSpan = 3;
                } else if ((args.column as Column).field === '11:00') {
                    args.colSpan = 4;
                }
                break;
            case 10003:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '11:30'
                ) {
                    args.colSpan = 3;
                } else if (
                    (args.column as Column).field === '10:30' ||
                    (args.column as Column).field === '3:30' ||
                    (args.column as Column).field === '4:30' ||
                    (args.column as Column).field === '2:30'
                ) {
                    args.colSpan = 2;
                }
                break;
            case 10004:
                if ((args.column as Column).field === '9:00') {
                    args.colSpan = 3;
                } else if ((args.column as Column).field === '11:00') {
                    args.colSpan = 4;
                } else if (
                    (args.column as Column).field === '4:00' ||
                    (args.column as Column).field === '2:30'
                ) {
                    args.colSpan = 2;
                }
                break;
            case 10005:
                if ((args.column as Column).field === '9:00') {
                    args.colSpan = 4;
                } else if ((args.column as Column).field === '11:30') {
                    args.colSpan = 3;
                } else if (
                    (args.column as Column).field === '3:30' ||
                    (args.column as Column).field === '4:30' ||
                    (args.column as Column).field === '2:30'
                ) {
                    args.colSpan = 2;
                }
                break;
            case 10006:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '4:30' ||
                    (args.column as Column).field === '2:30' ||
                    (args.column as Column).field === '3:30'
                ) {
                    args.colSpan = 2;
                } else if (
                    (args.column as Column).field === '10:00' ||
                    (args.column as Column).field === '11:30'
                ) {
                    args.colSpan = 3;
                }
                break;
            case 10007:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '3:00' ||
                    (args.column as Column).field === '10:30'
                ) {
                    args.colSpan = 2;
                } else if (
                    (args.column as Column).field === '11:30' ||
                    (args.column as Column).field === '4:00'
                ) {
                    args.colSpan = 3;
                }
                break;
            case 10008:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '10:30' ||
                    (args.column as Column).field === '2:30'
                ) {
                    args.colSpan = 3;
                } else if ((args.column as Column).field === '4:00') {
                    args.colSpan = 2;
                }
                break;
            default:
                this.extendQueryCellEvent(args, data.EmployeeID);
        }
        if ((args.colSpan as number) > 1) {
            (args.cell as HTMLElement).style.border = '1px solid red';
        }
    };
    public extendQueryCellEvent: EmitType<QueryCellInfoEventArgs> = (
        args: QueryCellInfoEventArgs,
        value: number
    ) => {
        switch (value) {
            case 10009:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '11:30'
                ) {
                    args.colSpan = 3;
                } else if (
                    (args.column as Column).field === '4:30' ||
                    (args.column as Column).field === '2:30'
                ) {
                    args.colSpan = 2;
                }
                break;
            case 100010:
                if (
                    (args.column as Column).field === '9:00' ||
                    (args.column as Column).field === '2:30' ||
                    (args.column as Column).field === '4:00' ||
                    (args.column as Column).field === '11:30'
                ) {
                    args.colSpan = 3;
                } else if ((args.column as Column).field === '10:30') {
                    args.colSpan = 2;
                }
                break;
        }
    };
    ngOnInit(): void { }
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));

Limitations

Column spanning in the TreeGrid has the following limitations:

  • The updateCell method does not support column spanning.
  • Column spanning is not compatible with the following features:
    1. Virtual scrolling
    2. Infinite scrolling

When using column spanning, ensure that the spanned cells do not interfere with TreeGrid operations such as sorting, filtering, or editing, as this may lead to unexpected behavior.

Column spanning implementation through API

The Syncfusion Angular TreeGrid provides an API-based approach to horizontally merge cells with identical values in the same row across adjacent columns.

The column spanning feature in the Syncfusion Angular TreeGrid can be enabled using enableColumnSpan property to true in the TreeGrid configuration, which significantly enhances readability and delivers a cleaner layout by eliminating repetitive data in columns such as “Status”, “Permit Status”, “Inspection Status” and “Punch List Status”.

import { NgModule, ViewChild } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { TreeGridModule } from '@syncfusion/ej2-angular-treegrid'
import { Component, OnInit } from '@angular/core';
import { QueryCellInfoEventArgs, GridLine, Column } from '@syncfusion/ej2-angular-grids';
import { columnSpanData } from './datasource';
import { EmitType } from '@syncfusion/ej2-base';

@Component({
    imports: [TreeGridModule,],
    standalone: true,
    selector: 'app-container',
    template: `<ejs-treegrid #treegrid [dataSource]="data" childMapping="children" [treeColumnIndex]="0" [height]="300"
        [enableColumnSpan]="true" [rowHeight]="50" gridLines="Both" clipMode="EllipsisWithTooltip"
        [enableHover]="false" [allowSelection]="false" allowPaging="true" [pageSettings]="pageSettings">
        <e-columns>
            <e-column field="activityName" headerText="Phase Name" width="250" freeze="Left"></e-column>
            <e-column headerText="Schedule" textAlign="Center" [columns]="scheduleColumns"></e-column>
            <e-column headerText="Work Status" textAlign="Center" [columns]="statusColumns"></e-column>
            <e-column headerText="Compliance" textAlign="Center" [columns]="complianceColumns"></e-column>
            <e-column headerText="Personnel" textAlign="Center" [columns]="personnelColumns"></e-column>
            <e-column headerText="Materials" textAlign="Center" [columns]="materialsColumns"></e-column>
            <e-column headerText="Cost Summary" textAlign="Center" [columns]="budgetColumns"></e-column>
        </e-columns>
    </ejs-treegrid>`,
})
export class AppComponent implements OnInit {
    public data?: Object[];
    public pageSettings?: Object;
    public scheduleColumns?: Object[];
    public personnelColumns?: Object[];
    public statusColumns?: Object[];
    public complianceColumns?: Object[];
    public materialsColumns?: Object[];
    public budgetColumns?: Object[];

    ngOnInit(): void {
        this.data = columnSpanData;
        this.pageSettings= { pageSizeMode: 'All', pageSize: 18 };
        
        this.scheduleColumns = [
            { field: 'startDate', headerText: 'Start Date', type: 'date', format: 'MM/dd/yyyy', width: 140, textAlign: 'Center' },
            { field: 'endDate', headerText: 'End Date', type: 'date', format: 'MM/dd/yyyy', width: 140, textAlign: 'Center' }
        ];

        this.personnelColumns = [
            {
                field: 'supervisor',
                headerText: 'Supervisor',
                width: 180,
            },
            { field: 'team', headerText: 'Crew', width: 200 }
        ];

        this.statusColumns = [
            { field: 'status', headerText: 'Status', width: 180, textAlign: 'Center' }
        ];

        this.complianceColumns = [
            { field: 'permitStatus', headerText: 'Permit Status', width: 160, textAlign: 'Center' },
            { field: 'inspectionDate', headerText: 'Inspection Date', width: 180, type: 'date', format: 'MM/dd/yyyy', textAlign: 'Center' },
            { field: 'inspectionStatus', headerText: 'Inspection Status', width: 180, textAlign: 'Center' },
            { field: 'punchListStatus', headerText: 'Punch List Status', width: 180, textAlign: 'Center' }
        ];

        this.materialsColumns = [
            { field: 'materialUsed', headerText: 'Materials Used', width: 180, textAlign: 'Center' },
            { field: 'materialCost', headerText: 'Material Cost', width: 140, format: 'C2', textAlign: 'Right', enableColumnSpan: false }
        ];

        this.budgetColumns = [
            { field: 'totalBudget', headerText: 'Planned Budget', width: 140, format: 'C2', textAlign: 'Center', enableColumnSpan: false },
            { field: 'paidToDate', headerText: 'Actual Spend', width: 140, format: 'C2', textAlign: 'Center', enableColumnSpan: false }
        ];
    }
}
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import 'zone.js';
bootstrapApplication(AppComponent).catch((err) => console.error(err));

In the sample, column spanning is disabled at the column level for the price based columns such as “Planned Budget” and “Actual Spend” by setting each column’s enableColumnSpan property to false.

Limitations

  • Virtualization
  • Infinite Scrolling
  • Row Drag and Drop
  • Column Virtualization
  • Detail Template
  • Editing
  • Export