Angular - How to merged cells with DevExtreme DataGrid

文章目錄

DevExtreme DataGrid provides a grouping function to organize data by groups. However, we want to display traditional table cells with merged cells.

In this article, I will demonstrate how to merge cells using the HTML rowspan attribute. You can also adjust the code for colspan if needed. Also, I will show how to export to Excel with merged cells.

Set up merged Cell

1. Create Service

  • Decide which row to set HTML rowspan attribute?
  • Count how many rows to span?
export class UiService {
  /**
   * table should setup span
   */
  isNewGroup<T>(data: T[], index: number, property: keyof T): boolean {
    return index === 0 || data[index][property] !== data[index - 1][property];
  }

  /**
   * count the table rowspan
   */
  getRowSpan<T>(data: T[], index: number, property: keyof T): number {
    let span = 1;
    for (let i = index + 1; i < data.length; i++) {
      if (data[i][property] === data[index][property]) span++;
      else break;
    }
    return span;
  }

  /**
   * set up cellTemplate of devextreme datagrid column
   */
  groupTemplate<T>(dataSource: T[], container: HTMLTableCellElement, options: ColumnCellTemplateData) {
    const { data, rowIndex, column } = options;
    const property = column.dataField as keyof T;
    const isNewGroup = this.isNewGroup(dataSource, rowIndex, property);
    if (isNewGroup) {
      const rowspan = this.getRowSpan(dataSource, rowIndex, property);
      container.setAttribute("rowspan", `${rowspan}`);
      container.textContent = data[property];
    } else container.style.display = 'none';
  }
}

2. Consume Service

Add [cellTemplate] to execute groupTemplate().

<dxi-column dataField="station" caption="Station" [cellTemplate]="groupTemplate" [allowEditing]="false" [allowFiltering]="false"></dxi-column>
<dxi-column dataField="sysName" caption="System" [cellTemplate]="groupTemplate" [allowEditing]="false" [allowFiltering]="false"></dxi-column>
constructor(private _uiService: UiService) {
    this.groupTemplate = this.groupTemplate.bind(this);
}

groupTemplate(container: HTMLTableCellElement, options: ColumnCellTemplateData) {
    this._uiService.groupTemplate(this.execList, container, options);
}

Export Excel with merged cells

1. Add dependencies

{
  "dependencies": {
    "@types/file-saver": "^2.0.5",
    "exceljs": "^4.3.0",
    "file-saver": "^2.0.5"
  }
}

2. Create Service to export Excel

import { Inject, Injectable } from '@angular/core';
import { Workbook, Worksheet } from 'exceljs';
import saveAs from 'file-saver';
import { exportDataGrid } from 'devextreme/excel_exporter';
import { DxDataGridComponent } from 'devextreme-angular';

export class MaintPrintService {

   exportExcel(title: string, grid: DxDataGridComponent, groupTemplateName: string) {
    const fileName = prompt("Please enter a filename:", title);
  
    if (!fileName) {
      if (fileName === '') alert("Filename is required. Please try again!");
      return;
    }
  
    const workbook = new Workbook();
    const worksheet = workbook.addWorksheet(title);
  
    const mergeInfo: Record<string, { start: string | null, end: string | null; prevValue: any }> = {};
  
    return exportDataGrid({
      component: grid.instance,
      worksheet,
      autoFilterEnabled: true,
      customizeCell(options) {
        const { gridCell, excelCell } = options;
        if (gridCell?.rowType === 'data') {
          const dataField = gridCell.column?.dataField;
          const cellTemplate = gridCell.column?.cellTemplate;
          const isGroupTemplate = typeof cellTemplate === 'function' && cellTemplate.name === groupTemplateName;
          if (dataField && isGroupTemplate) {
            const dataField = gridCell.column!.dataField!;
            const cellInfo = mergeInfo[dataField] || { start: null, end: null, prevValue: null };
  
            if (cellInfo.prevValue === gridCell.value) {
              cellInfo.end = excelCell.address;
            } else {
              if (cellInfo.start && cellInfo.end)
                worksheet.mergeCells(cellInfo.start, cellInfo.end);
              cellInfo.start = excelCell.address;
              cellInfo.end = null;
            }
            cellInfo.prevValue = gridCell.value;
            mergeInfo[dataField] = cellInfo;
          }
        }
      }
    }).then(() => {
      this.mergeLastInfo(mergeInfo, worksheet);
      this.saveWorkbook(workbook, fileName);
      return fileName;
    })
  }

  mergeLastInfo(mergeInfo: Record<string, { start: string | null, end: string | null; prevValue: any }>, worksheet: Worksheet) {
    Object.values(mergeInfo).forEach(({ start, end }) => {
      if (start && end) worksheet.mergeCells(start, end);
    });
  }

  saveWorkbook(workbook: Workbook, fileName: string) {
    workbook.xlsx.writeBuffer().then(buffer => {
      saveAs(new Blob([buffer], { type: 'application/octet-stream' }), `${fileName}.xlsx`);
    });
  }
}