Как объединить cdkVirtualScroll с cdkTable?

Я ищу рабочую виртуальную таблицу прокрутки с фиксированными заголовками, поэтому я нашел Cdk, который великолепен, но документация действительно сложна для подражания. На данный момент я пытаюсь объединить CdkTable с CdkVirtualScoll,

Все рабочие примеры, которые я смог найти, используют таблицу материалов, но я этого не делаю.

Так как я могу получить CdkVirtualScoll принимайтесь за работу? Вот что я сделал до сих пор (из примеров):

<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>

Как написано в документации, таблица была завёрнута в прокручиваемое окно просмотра. Но как и где я могу установить *cdkVirtualForсейчас?

Спасибо за вашу помощь!

2 ответа

Поскольку я не мог найти реально работающее решение, я написал свой "быстрый и грязный" код для фиксированного заголовка. Тем не менее я надеюсь найти гораздо лучший путь в будущем. Возможно, следующая версия Cdk предложит решение.

Теперь я написал (более или менее хакерскую) директиву, которая клонирует таблицу изнутри cdk-virtual-scroll-viewport и размещает клонированный узел раньше. На следующем этапе visibility из table thead элемент установлен в collapse,

Использование:

<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>

cloneThead Директива довольно проста:

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);
    }
}

Это работает довольно хорошо, но есть еще одна большая проблема: клон создается после ngAfterViewInit что влияет на то, что строки таблицы cdkVirtualFor еще не созданы для DOM.

Это хорошо для самого клона, потому что он еще не содержит tr элементы tbody, НО вычисленные стили CSS для правильной ширины для th элементы также не известны.

Итак все th элементы должны иметь атрибут CSS width. Иначе th ширина и td ширина может отличаться - что выглядит некрасиво...

Может быть, у кого-то еще есть решение сделать "настоящий" клон после cdk-virtual-scroll-viewportТаблица была нарисована.

Вот обновленное решение

большая проблема предыдущего кода заключалась в том, что он не мог динамически вычислять ширину столбцов. Поэтому необходимо было указать с каждым столбцом.

Эта версия исправляет эту проблему.

@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')
        })
    }
}

Использование:

Использование было немного изменено, потому что это было необходимо для вызова директивы, когда *cdkVirtualFor в процессе.

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

Это оно! Не очень хорошо, но работает...

Другие вопросы по тегам