2

I'm looking for a working virtual scroll table with fixed headers, so I found the Cdk which is great but the documentation is really difficult to follow. At the moment I'm trying to combine the CdkTable with CdkVirtualScoll.

All working examples I could found are using Material table, but I don't.

So how can I get CdkVirtualScoll get to work? Here is what I have done so far (from the examples):

<cdk-virtual-scroll-viewport>
<cdk-table [dataSource]="dataSource">
    <ng-container cdkColumnDef="username">
        <cdk-header-cell *cdkHeaderCellDef> User name </cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.username}} </cdk-cell>
    </ng-container>

    <ng-container cdkColumnDef="title">
        <cdk-header-cell *cdkHeaderCellDef> Title </cdk-header-cell>
        <cdk-cell *cdkCellDef="let row"> {{row.title}} </cdk-cell>
    </ng-container>

    <!-- Header and Row Declarations -->
    <cdk-header-row *cdkHeaderRowDef="['username', 'age']"></cdk-header-row>
    <cdk-row *cdkRowDef="let row; columns: ['username', 'age']"></cdk-row>
</cdk-table>
</cdk-virtual-scroll-viewport>

As in the documentation is written, the table was wrapped into the scrolling viewport. But how and where can I set the *cdkVirtualFornow?

Thx for your help!

msanford
  • 10,127
  • 8
  • 56
  • 83
Lars
  • 545
  • 6
  • 19
  • Take a look on that discussion, maybe it would help [link](https://github.com/angular/material2/issues/10122) – Amir Arbabian Feb 23 '19 at 12:42
  • @AmirArbabian: I already did and spend hour of work to get it to work, but it doesn't. Isn't there really no re-usable example with simple code out there? – Lars Feb 23 '19 at 13:13
  • Try [this one](https://stackblitz.com/edit/nahgrin-virtual-scroll-table-cvxa7v), for example. – Amir Arbabian Feb 23 '19 at 13:25
  • @AmirArbabian: I'm sorry my answer was wrong: I got the code getting to run (like in the example) but the problem is still that the header is NOT fixed but moving. – Lars Feb 23 '19 at 13:31
  • Lars Hagen . Hi. there, How can I contact you? I need help with cdk-virtual-scroll. – mx_code Jun 08 '20 at 08:49
  • @mex I created a chat room https://chat.stackoverflow.com/rooms/215503/mex – Lars Jun 08 '20 at 08:55
  • Thank you Lars. It's working perfect. Thank you for your time. – mx_code Jun 08 '20 at 09:49
  • Hi. Lars. I need help with ngx-virtual scroll. How can I add a regular horizontal scrollbar for the table with ngx-virtual scroll if overflow occurs in the x-direction? – mx_code Jun 10 '20 at 17:32

2 Answers2

0

Because I couldn't find a real working solution I wrote my own "quick & dirty" code for a fixed header. Nevertheless I hope to find a much better way in future. Perhaps the next release of Cdk will offer a solution.

What I did now is to wrote a (more or less hack) directive that clones the table from within the cdk-virtual-scroll-viewport and places the cloned node before. In the next step the visibility of the table thead element is set to collapse.

Usage:

<cdk-virtual-scroll-viewport [itemSize]="30" cloneThead>
    <table class="table table-hover">
        <thead>
            ...
        </thead>
        <tbody>
            <tr *cdkVirtualFor="let item of list">
                <td>...</td>
                ...
            </tr>
        </tbody>
    </table>
</cdk-virtual-scroll-viewport>

The cloneThead directive is pretty simple:

import { Directive, AfterViewInit, ElementRef } from '@angular/core';

@Directive({
    selector: '[cloneThead]'
})

export class CloneDirective implements AfterViewInit{

    constructor(private el: ElementRef) {}

    ngAfterViewInit(){
        let cloned = this.el.nativeElement.cloneNode(true);

        let table = cloned.querySelector("table");
            table.style.position = 'sticky';
            table.style.top = '0';
            table.style.zIndex = '100';

        this.el.nativeElement.appendChild(table);
    }
}

This works pretty well but has still one big issue: the clone is created after ngAfterViewInit which effects that the table rows of cdkVirtualFor are not yet created to the DOM.

This is good for the clone itself, because it doesn't yet contain any tr elements of the tbody, BUT the computed CSS styles for the correct widths for the th elements are also not known.

So all th elements need to have a CSS width attribute. Otherwise th widths and td widths can differ - which looks ugly...

Maybe somebody else has a solution to make a "real" clone, after the cdk-virtual-scroll-viewport-table has been drawn.

Lars
  • 545
  • 6
  • 19
0

Here is an updated solution

a big problem of the previous code was that it wasn't able to dynamical calculate the widths of columns. So it was necessary to specify a with to every column.

This version fixes this issue.

@Directive({
  selector: '[cdkFixedHeader]'
})

export class FixedHeaderDirective implements AfterViewInit{

    constructor(private el: ElementRef, private renderer:Renderer2) {}

    ngAfterViewInit(){

        // get the viewport element
        let cdkViewport = this.el.nativeElement.closest("cdk-virtual-scroll-viewport");

        // check if table was already cloned
        let clonedHeader = cdkViewport.querySelectorAll('.cloned-header');

        // create a clone if not exists
        if (clonedHeader.length == 0)
        {
            let table = this.el.nativeElement.closest('table');
            let cloned = table.cloneNode(true);
                cloned.style.position = 'sticky';
                cloned.style.top = '0';
                cloned.style.zIndex = '100';

            // remove tbody with elements
            let tbody = cloned.querySelector('tbody');
            cloned.removeChild(tbody);

            // add a "helper" class
            this.renderer.addClass(cloned, "cloned-header");

            // append cloned object to viewport
            cdkViewport.appendChild(cloned);
        }       
        // 
        //  walk through all <tr> with their <td> and store the max value in an array
        //
        let width = [];
        let td = this.el.nativeElement.querySelectorAll("td");
        width = new Array(td.length).fill(0);

        td.forEach((item,index) => {
            const w = item.getBoundingClientRect().width;
            width[index] = Math.max(w, width[index]);
        })  
        // 
        //  get <th> elements and apply the max-width values
        //
        let th = cdkViewport.querySelectorAll('.cloned-header th');
        th.forEach((item,index) => {
            this.renderer.setStyle(item, "min-width", width[index] + 'px')
        })
    }
}

Usage:

The usage has been changed little bit because it was necessary for call the directive when *cdkVirtualFor is being processed.

<tr *cdkVirtualFor="let item of list" cdkFixedHeader>
    ...
</tr>

That's it! Not really nice but working...

Ashish Kamble
  • 1,536
  • 2
  • 17
  • 24
Lars
  • 545
  • 6
  • 19