angulartypescriptangular-material

Unable to get show and hide columns button to work


I am trying to add a show/hide button to show/hidden columns in an angular material table. I can't get the button to function properly. When clicking on the button it does show the columns in the table I want to show/hide. I'm also trying to get the button to be placed as the last column header of the table after the 'position' column header on the right or whatever column is the last column. Also want the column names for the button to show vertically instead of horizontally bunched together like they are now.

html

<button mat-button [matMenuTriggerFor]="menu" class="table-config-menu">
 <mat-icon aria-hidden="false" aria-label="Example home icon">settings</mat-icon>
 <mat-menu #menu="matMenu">
  <span class="table-config-menu-label">Edit Columns</span>
   <div class="table-config-menu-options">
    <mat-checkbox 
      *ngFor="let cd of columnDefinitions; let i = index"
      (click)="$event.stopPropagation()"
      [(ngModel)]="cd.visible">
      {{cd.label}}
    </mat-checkbox>
   </div>
 </mat-menu>
 </button>
  <table
   mat-table
     [dataSource]="dataSource"
     multiTemplateDataRows
     class="mat-elevation-z8"
   >
   <ng-container
    matColumnDef="{{column}}"
    *ngFor="let column of columnsToDisplay"
   >
   <ng-container *ngIf="column !== 'action'; else action">
    <th mat-header-cell *matHeaderCellDef>{{column}}</th>
    <td mat-cell *matCellDef="let element">{{element[column]}}</td>
   </ng-container>
   <ng-template #action>
    <th mat-header-cell *matHeaderCellDef>Actions</th>
    <td mat-cell *matCellDef="let element" class="checkbox-spacing">
     <mat-icon
       (click)="expandedElement = expandedElement === element ? null : element"
       >{{expandedElement === element ? 'expand_less' :
       'expand_more'}}</mat-icon
     >
  </td>     
</ng-template>
</ng-container>
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
  <mat-checkbox
    (change)="$event ? toggleAllRows() : null"
    [checked]="selection.hasValue() && isAllSelected()"
    [indeterminate]="selection.hasValue() && !isAllSelected()"
    [aria-label]="checkboxLabel()"
  >
  </mat-checkbox>
</th>
<td mat-cell *matCellDef="let row">
  <mat-checkbox
    (click)="$event.stopPropagation()"
    (change)="$event ? selection.toggle(row) : null"
    [checked]="selection.isSelected(row)"
    [aria-label]="checkboxLabel(row)"
  >
  </mat-checkbox>
 </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]="columnsToDisplay.length"
   >
   <div
     class="example-element-detail"
     [@detailExpand]="element == expandedElement ? 'expanded' : 'collapsed'"
  >
    <div class="example-element-diagram">
      <div class="example-element-position">{{element.position}}</div>
      <div class="example-element-symbol">{{element.symbol}}</div>
      <div class="example-element-name">{{element.name}}</div>
      <div class="example-element-weight">{{element.weight}}</div>
     </div>
     <div class="example-element-description">
      {{element.description}}
       <span class="example-element-description-attribution">
        -- Wikipedia
        </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"
   ></tr>
   <tr
    mat-row
    *matRowDef="let row; columns: ['expandedDetail']"
     class="example-detail-row"
  ></tr>
 <!-- <tr mat-header-row *matHeaderRowDef="getDisplayedColumns()">
      </tr>
      <tr mat-row *matRowDef="let row; columns: getDisplayedColumns()"></tr>  -->
    </table>

ts

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

/**
 * @title Table with expandable rows
*/
@Component({
  selector: 'table-expandable-rows-example',
  styleUrls: ['table-expandable-rows-example.css'],
  templateUrl: 'table-expandable-rows-example.html',
  imports: [MatCheckboxModule],
  animations: [
     trigger('detailExpand', [
     state('collapsed', 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 TableExpandableRowsExample {
   columnsToDisplay: string[] = [
   'action',
   'Name',
   'weight',
   'symbol',
   'position',
  ];
  columnsToDisplayWithExpand = ['select', ...this.columnsToDisplay];

  dataSource = new MatTableDataSource<PeriodicElement>(ELEMENT_DATA);
  expandedElement: PeriodicElement | null;
  selection = new SelectionModel<PeriodicElement>(true, []);

 /** Whether the number of selected elements matches the total number of rows. */
  isAllSelected() {
  const numSelected = this.selection.selected.length;
  const numRows = this.dataSource.data.length;
  return numSelected === numRows;
 }

/** Selects all rows if they are not all selected; otherwise clear selection. */
toggleAllRows() {
  if (this.isAllSelected()) {
   this.selection.clear();
   return;
  }

  this.selection.select(...this.dataSource.data);
}

  /** The label for the checkbox on the passed row */
  checkboxLabel(row?: PeriodicElement): string {
  if (!row) {
    return `${this.isAllSelected() ? 'deselect' : 'select'} all`;
 }
 return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${
   row.position + 1
 }`;
}
columnDefinitions = [
 { def: 'name', label: 'Name', visible: true },
 {
   def: 'weight',
   label: 'Weight',
   visible: true,
  },
 {
   def: 'symbol',
   label: 'Symbol',
   visible: true,
  },
  {
   def: 'position',
   label: 'Position',
   visible: true,
  },
];

 getDisplayedColumns(): string[] {
    return this.columnDefinitions
   .filter((cd) => cd.visible)
   .map((cd) => cd.def);
  }
 }

  export interface PeriodicElement {
    Name: string;
    position: number;
    weight: number;
    symbol: string;
    description: string;
 }

  const ELEMENT_DATA: PeriodicElement[] = [
  {
    position: 1,
    Name: 'Hydrogen',
    weight: 1.0079,
    symbol: 'H',
  description: `Hydrogen is a chemical element with symbol H and atomic number 1. With  a standard
    atomic weight of 1.008, hydrogen is the lightest element on the periodic table.`,
},
{
  position: 2,
  Name: 'Helium',
  weight: 4.0026,
  symbol: 'He',
  description: `Helium is a chemical element with symbol He and atomic number 2. It is a
    colorless, odorless, tasteless, non-toxic, inert, monatomic gas, the first in the noble gas
    group in the periodic table. Its boiling point is the lowest among all the elements.`,
  },
  {
   position: 3,
   Name: 'Lithium',
   weight: 6.941,
   symbol: 'Li',
   description: `Lithium is a chemical element with symbol Li and atomic number 3. It  is a soft,
    silvery-white alkali metal. Under standard conditions, it is the lightest metal and the
    lightest solid element.`,
    },

   ];
 /**
  * Control column ordering and which columns are displayed.
 */

Demo code is on StackBlitz. I have commented out the getDisplayedColumns in the html so the rest of the table is still functioning properly. When I uncomment them the columns on the table disappear


Solution

  • To avoid unnecessary complexity, simply add a separate displayedColumns array as main columns list, and when checkbox is changed, call method to mutate this array instead (add/remove item based on checked status, and to keep column order, use all columns array in combination with Set that tracks checked columns), and when used in <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> it will then handle itself when checkboxes/columns get toggled:

    Also, to have menu as last column, add a toggle column definition, and add this to displayed columns as well.

    And to display checkboxes vertically, just add some flex and padding:

    ts:

    toggleColDef = 'toggleCol';
    
      displayedColumns: any = [
        ...this.columnsToDisplayWithExpand,
        this.toggleColDef,
      ];
    
      // all columns for tracking order
      allCols = [...this.displayedColumns];
    
      // use Set to track checked columns
      checkedColumns = new Set(this.displayedColumns);
    
      toggleColumn(col: string, checked: boolean) {
    
        if (checked) {
          // add to checked columns if checked
          this.checkedColumns.add(col);
        } else {
          // remove
          this.checkedColumns.delete(col);
        }
    
        // recreate columns in the same order, add toggle manually
        this.displayedColumns = this.allCols.filter(el=>{
          if(el === this.toggleColDef) {
            return true;
          } else {
            return this.checkedColumns.has(el);    
          }
        });
        
      }
    
      isColumnVisible(col: string): boolean {
        return this.checkedColumns.has(col);
      }
    

    html

    toggle cols def

    
     <!-- toggle columns menu -->
      <ng-container matColumnDef="{{toggleColDef}}">
        <th mat-header-cell *matHeaderCellDef>
          <button mat-button [matMenuTriggerFor]="menu" class="table-config-menu">
            <mat-icon aria-hidden="false" aria-label="Example home icon"
              >settings</mat-icon
            >
            <mat-menu #menu="matMenu" >
              <span class="table-config-menu-label">Edit Columns</span>
              <div class="table-config-menu-options" style="display:flex;flex-direction:column;padding:8px;">
                <mat-checkbox
                  *ngFor="let cd of columnDefinitions; let i = index"
                  [checked]="isColumnVisible(cd.def)"
                          (change)="toggleColumn(cd.def, $event.checked)"
                >
                  {{cd.label}}
                </mat-checkbox>
              </div>
            </mat-menu>
          </button>
        </th>
        <td mat-cell *matCellDef="let element">
         </td> 
      </ng-container>
    
    

    use displayedColumns as main array

        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr
        mat-row
        *matRowDef="let element; columns: displayedColumns;"
        class="example-element-row"
        [class.example-expanded-row]="expandedElement === element"
      ></tr>
      <tr
        mat-row
        *matRowDef="let row; columns: ['expandedDetail']"
        class="example-detail-row"
      ></tr>
    

    demo