Using Angular to create a Mat table with the ability to select multiple rows

Trying to add shift click feature to a sorted MatDataTable using angular and typescript.

When a click event occurs on the table, the selected row is stored.

If a shift click is detected, it should select rows between the last selected and the current row (similar to Windows shift click selection).

Here's the event handling code:

clickHandler(event, row, index) {
    console.log('index clicked: ' + index);
    if (event.ctrlKey) {
        this.selectRows(row, index); // Records this.lastSelected
    } else if (event.shiftKey) {
        this.selectRowsFill(row, index); 
    } else {
        this.selectElement(row, index); // Also records this.call
    }
}

// Function to auto-select rows between this.lastSelected and currently clicked row
selectRowsFill(row, index) {
    const indexA = this.lastSelected;
    const indexB = index;
    if (indexA > indexB) {
        this.selectRowsBetween(indexB, indexA); // Descending order
    } else {
        this.selectRowsBetween(indexA, indexB); // Ascending order
    }
}

// Function for actual selection
private selectRowsBetween(start, end) {
    let currentIndex = 0;
    this.dataSource.data.forEach(row => {
        if (currentIndex >= start && currentIndex <= end) {
            this.selection.select(row);
        }
        currentIndex++;
    });
}

And in the HTML:

<mat-row *matRowDef="let row; let i = index; columns: cols;" (click)="clickHandler($event, row, i)" [ngClass]="{'inDatabase' : isAdded(row), 'highlight': isSelectedAndAdded(row) || isSelected(row) }">

This code works well if the table is not sorted. Sorting the MatTableDataSource messes up the selection as it is based on the original order of data. How can I make shift click work on sorted data instead?

Answer №1

My solution is available at: >STACKBLITZ<

SAVE CODE IN CASE STACKBLITZ IS UNAVAILABLE

  • HTML
<table [shiftClickSource]="dataSource"
       [shiftClickSelectModel]="selection"
       [shiftClickSourceId]="['position']"
       mat-table [dataSource]="dataSource" class="mat-elevation-z8" matSort>

....
  • TS (Directive)
    import { Directive, Input, HostListener, OnInit, OnDestroy, ElementRef } from '@angular/core';

    import {SelectionModel, SelectionChange} from '@angular/cdk/collections';

    import { Subject, BehaviorSubject, Observable, merge, pipe, } from 'rxjs';
    import { shareReplay, takeUntil, withLatestFrom, tap } from 'rxjs/operators';

    import {MatTable, MatTableDataSource} from '@angular/material/table';

    /**
     * Directive that adds shift-click selection to your mat-table.
     * It needs the datasource from the host, the selectionModel of the checkboxes 
     * and optionally an array of [ids] to find the correct rows in the
     * (possibly sorted/ filtered) source array.
     */

    @Directive({
      selector: '[shiftClickSource]'
    })
    ...

Answer №2

After figuring it out, the solution was to establish a connection to the data source and save an array containing the rendered rows from the table:

ngOnChanges(changes: SimpleChanges) {
    this.dataSource.sort = this.sort;
    this.dataSource.connect().subscribe(d => this.renderedData = d);
}

Subsequently, I could loop through that array:

// This function is responsible for selecting rows.
private selectRowsBetween(start, end) {
    let currentIndex = 0;
    this.renderedData.forEach(row => {
        if (currentIndex >= start && currentIndex <= end) {
            this.selection.select(row);
        }
        currentIndex++;
    });
}

Answer №3

Insert the following line into mat-checkbox

(click)="$event.stopPropagation(); ShiftKeyDown($event, i + (paginator.pageIndex * paginator.pageSize));"

<ng-container matColumnDef="Select" class="mr-2 mt-2">
  <th mat-header-cell *matHeaderCellDef [hidden]="model.StepCode != 'not_started'">
    <mat-checkbox (change)="$event ? masterToggle() : null; checked()"
      [checked]="selection.hasValue() && isAllSelected()"
      [indeterminate]="selection.hasValue() && !isAllSelected()" [aria-label]="checkboxLabel()">
    </mat-checkbox>
  </th>
  <td mat-cell *matCellDef="let row; let i = index" [hidden]="model.StepCode != 'not_started'">
    <mat-checkbox
      (click)="$event.stopPropagation(); ShiftKeyDown($event, i + (paginator.pageIndex * paginator.pageSize));"
      (change)="
        $event ? selection.toggle(row) : null; checked()" [checked]="selection.isSelected(row)"
      [aria-label]="checkboxLabel(row)">
    </mat-checkbox>
  </td>
</ng-container>

Implement this function in typescript

 lastSelectedSegmentRow = 1; // keeps track of the last selected row index
 ShiftKeyDown(event, lastRow) {
    if (event.shiftKey) {
      let obj: SegmentFile[] = Object.assign([], this.dataSource.data).filter((val, i) => {
        return i > this.lastSelectedSegmentRow && i < lastRow
      });
      
      obj.forEach(e => this.selection.select(e))
    }
    this.lastSelectedSegmentRow = lastRow;
  }

Answer №4

Copy and paste the following code snippet into your .html file:

<mat-checkbox (click)="$event.stopPropagation()" (keyup)="checkboxTest(row, $event.key)" (change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row) "color="primary" [aria-label]="checkboxLabel(row)"></mat-checkbox>

Insert the code below into your .ts file:

tableArray: Array<Custom> = [];
min: number;
max: number;

checkboxTest(row, event) {
    if (event == "Shift") {
      this.selection.selected.forEach((row?: PeriodicElement) => {
        let custom = new Custom();
        custom.position = row.position;
        this.tableArray.push(custom);
      });

      this.tableArray.sort((a, b) => 0 - (a.position > b.position ? -1 : 1));

      for (var i = 0; i < this.tableArray.length; i++) {
        this.min = this.tableArray[0].position;
        this.max = this.tableArray[1].position;
      }

      this.dataSource.data.forEach((row?: PeriodicElement) => {
        if(this.min < row.position && row.position < this.max)
        {
          this.selection.select(row);
        } 
      });

      this.tableArray.pop();
      this.tableArray.pop();
    }
  }

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Error: Cannot locate 'import-resolver-typescript/lib' in jsconfig.json file

Issue: An error occurred stating that the file '/Users/nish7/Documents/Code/WebDev/HOS/frontend/node_modules/eslint-import-resolver-typescript/lib' could not be found. This error is present in the program because of the specified root file for c ...

Strange Node.js: I always avoid utilizing `require()`, yet encountered an unexpected error

For more information on this particular issue, please refer to this link It's quite puzzling as I haven't used the require() function in my code, yet I'm receiving an error telling me not to use it. How odd! The problematic code snippet i ...

Utilize dynamic components to load varying data sets multiple times

Is there a way to dynamically load a component multiple times and pass data based on certain values so that it will display with real-time information? I came across an example at the following link: In this example, there is a messageComponent with a "m ...

What could be causing my if statement to fail even though the condition is met?

I'm attempting to generate dynamic fields based on my chosen attributes. I have two array objects called addAttributes and fakeAttributes. The fakeAttributes contain the details of the selected attributes. I have a dropdown select component that displ ...

The icons within the <i> tag are failing to appear on my Ionic webpage

Trying to incorporate the tag icon in the HTML, but encountering issues with displaying the icon. Here is the code snippet I am using to display the icon, but unfortunately it's not appearing: <i class="fas fa-plus"></i> Intere ...

Is there a way to cancel or undo a transaction in the middle of using the PayPal JavaScript SDK?

As a newcomer to Angular, I am working on integrating PayPal as a payment gateway. However, I am unsure of the correct procedure to follow. paypal .Buttons({ createOrder: (data, actions) => { return actions.order.create({ purchase_ ...

The issue arises when attempting to apply a class binding to the mat-card element, as the mat-card class

When a single class binding is applied to mat-card, the mat-card class does not bind. <mat-card cdkDropList [className]="getClassName(item)"><!-- some content--></mat-card> In this scenario, the class name appears as something ...

Shared validation between two input fields in Angular 2+

I have a unique task at hand. I am working on creating an input field with shared validation. The goal is to ensure that both fields are technically required, but if a user fills in their email address, then both fields become valid. Similarly, if they ent ...

Creating a React component with a column allowing multiple checkbox selections in NextUI table

Setting up multiple "checkbox" columns in a table using the NextUI table has been my current challenge. Each row should have selectable checkboxes, and I want these selections to be remembered when navigating between pages, running searches, or removing co ...

What is the best way to combine two responses and then convert them into a promise?

When making two calls, the firstCallData prints data fine. However, when I use + to merge the responses, it returns me the following Response. What is a better approach to achieve this task? main.ts let data = await this.processResponse(__data.Detail ...

Generate a vector tile in Azure Maps containing a massive one million data points

Is it possible to use Azure maps to display a large amount of data points, such as one million? I am interested in creating an Azure map with optimized performance and loading it into my Angular application using a map URL. Can you provide guidance on how ...

Is there a way to retrieve a data type from a class in TypeScript?

Within my code, there exists a class: class Person { name: string; age: number; gender: string; constructor(params: any){ this.name = params.name; this.age = params.age; this.gender = params.gender; } } My question is how ca ...

What is the best way to assign a value to a class variable within a method by referencing the 'this' keyword?

Is there a way to set the state of this Ionic react app when displaying the outcome of a reset service? I am facing challenges with using this.setState({resetSuccess}) within a method due to scope issues. (Details provided in comments) Here is the relevan ...

Maintain a continuous live server for your Angular 2/4 application

Is there a way to run an Angular application permanently? When I run the npm start command on a local server, it provides a URL with a port such as localhost:4200. However, when I close the terminal, the project stops running. The same issue occurs when ...

Transforming this JavaScript function using Template Strings into TypeScript

Is there anyone out there who can assist with the following query? I have a functional .js file that I need to integrate into a TypeScript program. import React from "react"; import styled, { css } from "styled-components"; const style = ({ theme, ...res ...

Implementing ngFor to Iterate Through a JSON Object in Angular 6

Iterate through the object list retrieved from a JSON object Here is the JSON object that I have fetched: { "0": { "0": null, "1": "Consolidated Statements of Changes in Stockholders\u2019 Deficit", "2": null, "3": "", "4": "" ...

What is the best way to make an Angular Material checkbox respond to a programmatic change in value within a reactive form?

I have implemented a dynamic angular form that allows users to add rows using a button. I am currently working on adding functionality to select all rows. Each row includes a checkbox that toggles the value in the object between T and F. If all checkboxes ...

Update a value in the sessionStorage using Angular

I am working on a function that handles checkbox options based on event.target.name. The goal is to add the checkbox option to session storage if it's not already there, and update the value if it exists. However, I'm facing some issues with my c ...

Currently honing my skills in Angular 2, but encountering an error stating "Bindings cannot contain assignments."

<app-employeecount [all]= "gettotalemployeescount()" <app-employeecount [all]= "gettotalemployeescount()" [male]= "gettotalmaleemployeescount()" [female]="gettotalfemaleemployeescount()" (on ...

Is there a way to effectively refresh in Angular while using JBoss 6.4?

In my configuration of the standalone.xml file, I have set up the following rules inside subsystem tags: <rewrite name="rule-2" pattern="^((?!.*(rest)).*)\/([\w\-]+)\/([\w\-]+)$" substitution="/$1/index.html" flags="L"/> ...