Search code examples
cssangularangular-materialdatasourceangular-material-table

Angular mat-table colspan does not skip columns leaving extra columns in the table


In my mat-table of 12 months I am attempting to colspan cells across multiple months. However, the months that are spanned over are not skipped in the datasource, therefore there are extra columns added on to the end of the table. Ex: I start a cell in March and colspan = 2, but now April data is being placed in the May column, May data is being placed in the June column and so forth.

Table with extra columns

----- html -----

<table mat-table [dataSource]="dataSource" multiTemplateDataRows class="mat-elevation-z8">
        @for (column of columnsToDisplay; track column) {
            <ng-container matColumnDef="{{column}}">
            <th mat-header-cell *matHeaderCellDef> {{ column }}</th>
            <td mat-cell *matCellDef="let element" [attr.colspan]="element[column].colspan" [style.background-color]="element[column].color">{{ element[column].name }}</td>
            </ng-container>
        }

        <ng-container matColumnDef="expand">
            <th mat-header-cell *matHeaderCellDef aria-label="row actions">&nbsp;</th>
            <td mat-cell *matCellDef="let element">
            <button mat-icon-button aria-label="expand row" (click)="(expandedElement = expandedElement === element ? null : element); $event.stopPropagation()">
                @if (expandedElement === element) {
                <mat-icon>keyboard_arrow_up</mat-icon>
                } @else {
                <mat-icon>keyboard_arrow_down</mat-icon>
                }
            </button>
            </td>
        </ng-container>

        <!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
        <ng-container matColumnDef="expandedDetail">
            <td mat-cell *matCellDef="let element" [attr.colspan]="columnsToDisplayWithExpand.length">
            <div class="example-element-detail" [@detailExpand]="element == expandedElement ? 'expanded' : 'collapsed'">
                <div class="example-element-description">
                {{element.description}}
                <span class="example-element-description-attribution"></span>
                </div>
            </div>
            </td>
        </ng-container>

        <tr mat-header-row *matHeaderRowDef="columnsToDisplayWithExpand"></tr>
        <tr mat-row *matRowDef="let element; columns: columnsToDisplayWithExpand;"
            class="example-element-row"
            [class.example-expanded-row]="expandedElement === element"
            (click)="expandedElement = expandedElement === element ? null : element">
        </tr>
        <tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="example-detail-row"></tr>
    </table>

----- TypeScript -----

import { Component } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { MatTableDataSource } from '@angular/material/table';

@Component({
  selector: 'app-fertilization',
  templateUrl: './fertilization.component.html',
  styleUrl: './fertilization.component.scss',
  animations: [
    trigger('detailExpand', [
      state('collapsed,void', style({ height: '0px', minHeight: '0' })),
      state('expanded', style({ height: '*' })),
      transition(
        'expanded <=> collapsed',
        animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')
      ),
    ]),
  ],
})
export class FertilizationComponent {
  public dataSource: any = new MatTableDataSource();
  public fertilizationSchedule: YearElement[] = [];
  columnsToDisplay = [
    'Jan',
    'Feb',
    'Mar',
    'Apr',
    'May',
    'June',
    'July',
    'Aug',
    'Sept',
    'Oct',
    'Nov',
    'Dec',
  ];
  columnsToDisplayWithExpand = [...this.columnsToDisplay, 'expand'];
  expandedElement: YearElement | undefined;

  constructor() {
    this.fertilizationSchedule = [
      { Jan: new Element(), Feb: new Element(), Mar: {name: "Step 1 - Early Spring", colspan: 2, color: "#ffff91"}, Apr: new Element(), May: new Element(), June: new Element(), July: new Element(), Aug: new Element(), Sept: new Element(), Oct: new Element(), Nov: new Element(), Dec: new Element(), description: "Fertilizer for lawn + pre-emergent weed control" }
    ];

    this.dataSource = this.fertilizationSchedule;
  }
}

interface YearElement {
  Jan?: Element;
  Feb?: Element;
  Mar?: Element;
  Apr?: Element;
  May?: Element;
  June?: Element;
  July?: Element;
  Aug?: Element;
  Sept?: Element;
  Oct?: Element;
  Nov?: Element;
  Dec?: Element;
  description: string; // Expanded Item
}

class Element {
  name: string = "";
  colspan?: number;
  color?: string;
}

----- css -----

table {
            width: 100%;
        }
        
        tr.example-detail-row {
            height: 0;
        }
        
        tr.example-element-row:not(.example-expanded-row):hover {
            background: whitesmoke;
        }
        
        tr.example-element-row:not(.example-expanded-row):active {
            background: #efefef;
        }
        
        .example-element-row td {
            border-bottom-width: 0;
        }
        
        .example-element-detail {
            overflow: hidden;
            display: flex;
        }
        
        .example-element-diagram {
            min-width: 80px;
            border: 2px solid black;
            padding: 8px;
            font-weight: lighter;
            margin: 8px 0;
            height: 104px;
        }
        
        .example-element-symbol {
            font-weight: bold;
            font-size: 40px;
            line-height: normal;
        }
        
        .example-element-description {
            padding: 16px;
        }
        
        .example-element-description-attribution {
            opacity: 0.5;
        }

My table consists of 12 columns, each being a month. I would like some cells to be able to colspan over several months. I have tried making columns optional expecting those months to be skipped since my colspan is in their column anyway. However they are just pushed over a column causing extra columns at the end of the row.


Solution

  • When you use colspan, you should "remove" so many columns to the right as mark the colspan. So, in any way, you should mark this columns to not show.

    For this, you need transform your array. The idea is, e.g. You have

    this.fertilizationSchedule = [
      { Jan: new Element(), 
        Feb: new Element(), 
        Mar: {name: "Step 1 - Early Spring", colspan: 2, color: "#ffff91"},
        Apr: new Element(), 
        May: new Element(), 
        June: {name: "Step 1 - Early Spring", colspan: 3, color: "#ffff91"}, 
        July:new Element(), 
        Aug:new Element(), 
        Sept:new Element(), 
        Oct:new Element(), 
        ...
      }
    

    Transform in the way

    this.fertilizationSchedule = [
      { Jan: new Element(), 
        Feb: new Element(), 
        Mar: {name: "Step 1 - Early Spring", colspan: 2, color: "#ffff91"},
        Apr: {colspan:-1}, //<--this colspan=-1
        May: new Element(), 
        June:new Element(), 
        June:{name: "Step 1 - Early Spring", colspan: 3, color: "#ffff91"}, 
        July:{colspan:-1},  //<--this colspan=-1 
        Aug: {colspan:-1},   //<--this colspan=-1
        Sept:new Element(), 
        Oct:new Element(), 
        ...
      }
    ]
    

    So you can write some like

    @for (column of columnsToDisplay; track column) {
       <ng-container matColumnDef="{{column}}">
         <th mat-header-cell *matHeaderCellDef> {{ column }}</th>
         @if (element[column].colspan!=-1) //if "colspan=-1" NOT create
         {
           <td mat-cell *matCellDef="let element" 
               [attr.colspan]="element[column].colspan" 
               [style.background-color]="element[column].color">
               {{ element[column].name }}
           </td>
         }
       </ng-container>
     }
    

    How do it? Create a function:

    fertilizationFormatted(fertilizationSchedule:any[])
    {
      const fer=fertilizationSchedule.map((element:any)=>{ //with each row
    
        this.columnsToDisplay.forEach((col:string,index:number)=>{ //with each column
              if (element[col]?.colspan>1)
              {
                 let i=1;
                 while (index+i<12 && i<element[col].colspan){
                    element[this.columnsToDisplay[index+i]]={colspan:-1}
                    i++;
                 }
              }
        })
        return element;
     })
     return fer;
    }
    

    and

    this.dataSource = this.fertilizationFormatted(this.fertilizationSchedule);