, name: string): Example {
21 | return {
22 | title,
23 | name,
24 | component,
25 | ts: require(`!!../examples/${name}/${name}.component.ts?raw`),
26 | html: require(`!!../examples/${name}/${name}.component.html?raw`),
27 | css: require(`!!../examples/${name}/${name}.component.css?raw`),
28 | };
29 | }
30 |
31 | export const examples: Example[] = [
32 | getExample('Base Example', BaseExampleComponent, 'base-example'),
33 | getExample('Cdk Example', CdkExampleComponent, 'cdk-example'),
34 | getExample('Table with footer', FooterExampleComponent, 'footer-example'),
35 | getExample('Table with filter, sort and selection', FilterSortSelectExampleComponent, 'filter-sort-select-example'),
36 | getExample('Table with sticky header', StickyExampleComponent, 'sticky-example'),
37 | getExample('Table with sticky column', StickyColumnExampleComponent, 'sticky-column-example'),
38 | ];
39 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/examples/sticky-column-example/sticky-column-example.component.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | height: 300px;
3 | width: 100%;
4 | overflow: auto;
5 | }
6 |
7 | table {
8 | width: 1000px;
9 | table-layout: fixed;
10 | }
11 |
12 | th,
13 | td {
14 | width: 100px;
15 | }
16 |
17 | .col-sm {
18 | width: 70px;
19 | }
20 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/examples/sticky-column-example/sticky-column-example.component.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | No.
13 | {{element.id}}
16 |
17 |
18 |
19 | Name
20 | {{element.name}}
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/examples/sticky-column-example/sticky-column-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { TableVirtualScrollDataSource } from 'ng-table-virtual-scroll';
3 |
4 | const DATA = Array.from({length: 1000}, (v, i) => ({
5 | id: i + 1,
6 | name: `Element #${i + 1}`
7 | }));
8 |
9 | @Component({
10 | selector: 'app-sticky-column-example',
11 | templateUrl: './sticky-column-example.component.html',
12 | styleUrls: ['./sticky-column-example.component.css']
13 | })
14 | export class StickyColumnExampleComponent {
15 | displayedColumns = ['id', 'name', 'name', 'name', 'name', 'name', 'name', 'name', 'name', 'name'];
16 |
17 | dataSource = new TableVirtualScrollDataSource(DATA);
18 | }
19 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/examples/sticky-example/sticky-example.component.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | height: 300px;
3 | }
4 |
5 | table {
6 | width: 100%;
7 | table-layout: fixed;
8 | }
9 |
10 | .col-sm {
11 | width: 70px;
12 | }
13 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/examples/sticky-example/sticky-example.component.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | No.
13 | {{element.id}}
16 |
17 |
18 |
19 | Name
20 | {{element.name}}
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/examples/sticky-example/sticky-example.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { TableVirtualScrollDataSource } from 'ng-table-virtual-scroll';
3 |
4 | const DATA = Array.from({length: 1000}, (v, i) => ({
5 | id: i + 1,
6 | name: `Element #${i + 1}`
7 | }));
8 |
9 | @Component({
10 | selector: 'app-sticky-example',
11 | templateUrl: './sticky-example.component.html',
12 | styleUrls: ['./sticky-example.component.css']
13 | })
14 | export class StickyExampleComponent {
15 |
16 | displayedColumns = ['id', 'name'];
17 |
18 | dataSource = new TableVirtualScrollDataSource(DATA);
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/navbar/navbar.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 | Virtual Scroll for Angular Material Table
8 |
9 |
10 |
12 |
15 | GitHub
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/navbar/navbar.component.scss:
--------------------------------------------------------------------------------
1 | @use '@angular/material' as mat;
2 | @import 'helpers';
3 | :host {
4 | @include mat.elevation(2);
5 | }
6 |
7 | .header-logo {
8 | height: 26px;
9 | margin: 0 4px 3px 0;
10 | vertical-align: middle;
11 | }
12 |
13 | mat-toolbar-row {
14 | span {
15 | text-overflow: ellipsis;
16 | overflow: hidden;
17 | }
18 | }
19 |
20 | @media (max-width: 598px) {
21 | [mat-button] span {
22 | display: none;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/navbar/navbar.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-navbar',
5 | templateUrl: './navbar.component.html',
6 | styleUrls: ['./navbar.component.scss']
7 | })
8 | export class NavbarComponent {
9 |
10 |
11 | constructor() {}
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/overview/overview.component.html:
--------------------------------------------------------------------------------
1 |
2 | The ng-table-virtual-scroll
package provides an opportunity to use Angular
4 | Material
5 | cdk-virtual-scroll-viewport
with
6 | mat-table
7 |
8 |
9 | Installation
10 |
11 |
12 | To install package by npm
:
13 |
14 | $ npm install -save ng-table-virtual-scroll
15 |
16 |
17 | Version compatibility
18 |
19 |
22 |
23 |
24 |
25 |
26 | Angular Version
29 | {{element[0]}}
32 |
33 |
34 |
35 | Library Version
38 | {{element[1]}}
41 |
42 |
43 |
44 |
45 |
46 |
47 | To make it work in your project:
48 |
49 | 1) Import module:
50 |
51 |
52 |
53 |
54 | Note : you need to install and configure
55 | cdk-virtual-scroll-viewport
56 | (ScrollingModule) and
57 | mat-table
58 | (MatTableModule) before.
59 | TableVirtualScroll
only make them work together properly
60 |
61 |
62 |
63 | 2) Use tvsItemSize
directive on <cdk-virtual-scroll-viewport>
64 |
65 |
66 | 3) Use new TableVirtualScrollDataSource()
as [dataSource]
of a <table
67 | mat-table>
, or
68 | new CdkTableVirtualScrollDataSource()
as [dataSource]
of a <table
69 | cdk-table>
70 |
71 |
72 | tvsItemSize directive
73 |
74 |
75 | When all rows of the table are the same fixed size, you can wrap your table[mat-table]
with cdk-virtual-scroll-viewport
76 | container and add the tvsItemSize
directive on it.
77 |
78 |
79 |
80 | To add come configuration, you can add additional properties:
81 |
82 | tvsItemSize
- the row height in px (default: 48)
83 |
84 | headerHeight
- the header row height in px (default: 56)
85 |
86 | footerHeight
- the footer row height in px (default: 48)
87 |
88 |
89 | Note : tvsItemSize
/headerHeight
/footerHeight
properties only required
90 | for calculations - you need to style your table separately by css
91 |
92 |
93 |
94 | headerEnabled
- is the header row in the table (default: true)
95 |
96 | footerEnabled
- is the footer row in the table (default: false)
97 |
98 | bufferMultiplier
- the size of rendered buffer (default: 0.7). The 'bufferMultiplier *
99 | visibleRowsCount
'
100 | number of rows will be rendered before and after visible part of the table
101 |
102 |
103 |
104 | Don't forget to set height on the cdk-virtual-scroll-viewport
105 |
106 |
107 | VirtualTableDataSource
108 |
109 |
110 | The tvsItemSize
requires the VirtualTableDataSource
to be provide as mat-table[dataSource]
111 | (CdkTableVirtualScrollDataSource
for cdk-table[dataSource]
) .
112 | It is the same as the MatTableDataSource
, but with few adjustments which was made so the virtual scroll
113 | could work.
114 |
115 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/overview/overview.component.scss:
--------------------------------------------------------------------------------
1 | @import 'helpers';
2 |
3 | code {
4 | @include code();
5 | }
6 |
7 | pre {
8 | padding: 0 1em;
9 | }
10 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/overview/overview.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | const snippets = {
4 | importModule: `import { TableVirtualScrollModule } from 'ng-table-virtual-scroll';
5 |
6 | @NgModule({
7 | imports: [
8 | // ...
9 | TableVirtualScrollModule
10 | ]
11 | })
12 | export class AppModule { }
13 | `
14 | };
15 |
16 |
17 | @Component({
18 | selector: 'app-overview',
19 | templateUrl: './overview.component.html',
20 | styleUrls: ['./overview.component.scss']
21 | })
22 | export class OverviewComponent {
23 | snippets = snippets;
24 |
25 | versionCompatibilityColumns = ['ng', 'lib'];
26 | versionCompatibility = Object.entries({
27 | '\>= 15': 'latest',
28 | '13 - 14': '1.5.*',
29 | '<= 12': '1.3.*'
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './stackblitz.service';
2 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/services/stackblitz.service.ts:
--------------------------------------------------------------------------------
1 | import { APP_BASE_HREF } from '@angular/common';
2 | import { HttpClient } from '@angular/common/http';
3 | import { Inject, Injectable } from '@angular/core';
4 | import stackBlitzSDK from '@stackblitz/sdk';
5 | import { Example } from '../examples';
6 | import { Utils } from '../utils';
7 |
8 | function trimEndSlash(url: string): string {
9 | if (url[url.length - 1] === '/') {
10 | url = url.substring(0, url.length - 1);
11 | }
12 | return url;
13 | }
14 |
15 | const templatePath = '/assets/stackblitz/';
16 | const templateFiles = [
17 | 'angular.json',
18 | 'package.json',
19 | 'src/index.html',
20 | 'src/styles.scss',
21 | 'src/polyfills.ts',
22 | 'src/main.ts',
23 | ];
24 | const replaceFilesPath = [
25 | 'src/main.ts',
26 | 'src/index.html',
27 | ];
28 |
29 | function getFilePath(example: Example, ext: 'ts' | 'css' | 'html') {
30 | return `src/app/${example.name}.component.${ext}`;
31 | }
32 |
33 |
34 | @Injectable({
35 | providedIn: 'root'
36 | })
37 | export class StackblitzService {
38 | private files: { [path: string]: string } = {};
39 |
40 | constructor(
41 | private http: HttpClient,
42 | @Inject(APP_BASE_HREF) private baseHref: string,
43 | ) {
44 | this.setFiles();
45 | }
46 |
47 | open(example: Example): void {
48 | stackBlitzSDK.openProject(
49 | {
50 | files: this.getFiles(example),
51 | title: 'ng-table-virtual-scroll | ' + example.title,
52 | description: example.title,
53 | template: 'angular-cli',
54 | dependencies: {
55 | '@angular/cdk': '*',
56 | '@angular/material': '*',
57 | 'ng-table-virtual-scroll': '*'
58 | }
59 | },
60 | {
61 | openFile: [getFilePath(example, 'ts'), getFilePath(example, 'html')]
62 | }
63 | );
64 | }
65 |
66 | private setFiles(): void {
67 | templateFiles
68 | .forEach(fileUrl => {
69 | this.http.get(trimEndSlash(this.baseHref) + templatePath + fileUrl, { responseType: 'text' })
70 | .subscribe(content => {
71 | this.files[fileUrl] = content;
72 | });
73 | });
74 | }
75 |
76 | private getFiles(example: Example): { [path: string]: string } {
77 | const exampleFiles = (['ts', 'css', 'html'] as const).reduce((files, ext) => {
78 | files[getFilePath(example, ext)] = example[ext];
79 | return files;
80 | }, {});
81 | const replacedFiles = replaceFilesPath.reduce((files, path) => {
82 | files[path] = Utils.replace(this.files[path], {
83 | exampleComponentName: Utils.capitalize(Utils.toCamelCase(example.name)) + 'Component',
84 | exampleName: example.name,
85 | title: example.title
86 | });
87 | return files;
88 | }, {});
89 |
90 | return {
91 | ...this.files,
92 | ...exampleFiles,
93 | ...replacedFiles,
94 | };
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/projects/demo-page/src/app/utils.ts:
--------------------------------------------------------------------------------
1 | export class Utils {
2 | static replace(str: string, obj: object): string {
3 | return str.replace(/<%([\w\W][^%]+)%>/g, (val, name) => obj[name] ?? val);
4 | }
5 | static toCamelCase(str: string): string {
6 | return str
7 | .split(/[,| -]+/)
8 | .filter(v => !!v && !!v.trim())
9 | .map((part, index) => index ? this.capitalize(part, true) : part.toLowerCase())
10 | .join('');
11 | }
12 | static capitalize(str: string, toLower = false): string {
13 | return str[0].toUpperCase() + (toLower ? str.slice(1).toLowerCase() : str.slice(1));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/projects/demo-page/src/assets/images/angular-white-transparent.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/projects/demo-page/src/assets/images/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diprokon/ng-table-virtual-scroll/0328ecd01336177d347b4ec75559d06ed4e47b91/projects/demo-page/src/assets/images/cover.png
--------------------------------------------------------------------------------
/projects/demo-page/src/assets/images/github-circle-white-transparent.svg:
--------------------------------------------------------------------------------
1 | github-circle-white-transparent
--------------------------------------------------------------------------------
/projects/demo-page/src/assets/stackblitz/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "ng-table-virtual-scroll-example": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "schematics": {
11 | "@schematics/angular:component": {
12 | "style": "scss"
13 | }
14 | },
15 | "prefix": "app",
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:browser",
19 | "options": {
20 | "aot": true,
21 | "outputPath": "dist",
22 | "index": "src/index.html",
23 | "main": "src/main.ts",
24 | "polyfills": "src/polyfills.ts",
25 | "tsConfig": "tsconfig.app.json",
26 | "assets": [],
27 | "styles": [
28 | "src/styles.scss"
29 | ],
30 | "scripts": []
31 | },
32 | "configurations": {
33 | "production": {
34 | "fileReplacements": [],
35 | "optimization": true,
36 | "outputHashing": "all",
37 | "sourceMap": false,
38 | "namedChunks": false,
39 | "aot": true,
40 | "extractLicenses": true,
41 | "vendorChunk": false,
42 | "buildOptimizer": true,
43 | "budgets": [
44 | {
45 | "type": "initial",
46 | "maximumWarning": "2mb",
47 | "maximumError": "5mb"
48 | }
49 | ]
50 | }
51 | }
52 | },
53 | "serve": {
54 | "builder": "@angular-devkit/build-angular:dev-server",
55 | "options": {
56 | "browserTarget": "ng-table-virtual-scroll-example:build"
57 | },
58 | "configurations": {
59 | "production": {
60 | "browserTarget": "ng-table-virtual-scroll-example:build:production"
61 | }
62 | }
63 | },
64 | "extract-i18n": {
65 | "builder": "@angular-devkit/build-angular:extract-i18n",
66 | "options": {
67 | "browserTarget": "ng-table-virtual-scroll-example:build"
68 | }
69 | },
70 | "lint": {
71 | "builder": "@angular-devkit/build-angular:tslint",
72 | "options": {
73 | "tsConfig": [
74 | "tsconfig.app.json"
75 | ],
76 | "exclude": [
77 | "**/node_modules/**"
78 | ]
79 | }
80 | }
81 | }
82 | }
83 | },
84 | "defaultProject": "ng-table-virtual-scroll-example"
85 | }
86 |
--------------------------------------------------------------------------------
/projects/demo-page/src/assets/stackblitz/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ng-table-virtual-scroll-example",
3 | "version": "0.0.0",
4 | "scripts": {
5 | },
6 | "private": true,
7 | "dependencies": {
8 | "@angular/animations": "^15.1.4",
9 | "@angular/cdk": "^15.1.4",
10 | "@angular/common": "^15.1.4",
11 | "@angular/compiler": "^15.1.4",
12 | "@angular/core": "^15.1.4",
13 | "@angular/forms": "^15.1.4",
14 | "@angular/material": "^15.1.4",
15 | "@angular/platform-browser": "^15.1.4",
16 | "@angular/platform-browser-dynamic": "^15.1.4",
17 | "@angular/router": "^15.1.4",
18 | "ng-table-virtual-scroll": "*",
19 | "rxjs": "^7.5.5",
20 | "tslib": "^2.4.0",
21 | "zone.js": "~0.11.5"
22 | },
23 | "devDependencies": {
24 | "@angular-devkit/build-angular": "^15.1.5",
25 | "@angular/cli": "^15.1.5",
26 | "@angular/compiler-cli": "^15.1.4",
27 | "@angular/language-service": "^15.1.4",
28 | "ts-node": "~10.9.1",
29 | "typescript": "~4.8.*"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/projects/demo-page/src/assets/stackblitz/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%title%>
8 |
9 |
10 | >Loading >
11 |
12 |
13 |
--------------------------------------------------------------------------------
/projects/demo-page/src/assets/stackblitz/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
5 | import { MatFormFieldModule } from '@angular/material/form-field';
6 | import { MatInputModule } from '@angular/material/input';
7 | import { MatSortModule } from '@angular/material/sort';
8 | import { MatCheckboxModule } from '@angular/material/checkbox';
9 |
10 |
11 | import { MatTableModule } from '@angular/material/table';
12 | import { ScrollingModule } from '@angular/cdk/scrolling';
13 | import { TableVirtualScrollModule } from 'ng-table-virtual-scroll';
14 |
15 |
16 | import { <%exampleComponentName%> } from './app/<%exampleName%>.component';
17 |
18 |
19 | @NgModule({
20 | imports: [
21 | BrowserModule,
22 | BrowserAnimationsModule,
23 | MatTableModule,
24 | ScrollingModule,
25 | TableVirtualScrollModule,
26 | MatFormFieldModule,
27 | MatInputModule,
28 | MatSortModule,
29 | MatCheckboxModule,
30 | ],
31 | entryComponents: [<%exampleComponentName%>],
32 | declarations: [<%exampleComponentName%>],
33 | bootstrap: [<%exampleComponentName%>],
34 | })
35 | export class AppModule {}
36 |
37 | platformBrowserDynamic().bootstrapModule(AppModule)
38 | .catch(err => console.error(err));
39 |
--------------------------------------------------------------------------------
/projects/demo-page/src/assets/stackblitz/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js';
2 |
--------------------------------------------------------------------------------
/projects/demo-page/src/assets/stackblitz/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import '@angular/material/prebuilt-themes/indigo-pink.css';
2 |
3 | body {
4 | font-family: Roboto, Arial, sans-serif;
5 | margin: 0;
6 | padding: 30px;
7 | }
8 |
--------------------------------------------------------------------------------
/projects/demo-page/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/projects/demo-page/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false
7 | };
8 |
9 | /*
10 | * For easier debugging in development mode, you can import the following file
11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
12 | *
13 | * This import should be commented out in production mode because it will have a negative impact
14 | * on performance if an error is thrown.
15 | */
16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
17 |
--------------------------------------------------------------------------------
/projects/demo-page/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diprokon/ng-table-virtual-scroll/0328ecd01336177d347b4ec75559d06ed4e47b91/projects/demo-page/src/favicon.ico
--------------------------------------------------------------------------------
/projects/demo-page/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Virtual Scroll for Angular Material Table
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/projects/demo-page/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/projects/demo-page/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags.ts';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js'; // Included with Angular CLI.
49 |
50 |
51 | /***************************************************************************************************
52 | * APPLICATION IMPORTS
53 | */
54 |
--------------------------------------------------------------------------------
/projects/demo-page/src/styles.scss:
--------------------------------------------------------------------------------
1 | @use '@angular/material' as mat;
2 | @use 'helpers';
3 |
4 | @include mat.core();
5 |
6 | @include mat.all-component-themes(helpers.$mat-app-theme);
7 |
8 | @import 'highlight';
9 |
10 | html,
11 | body {
12 | height: 100%;
13 | }
14 |
15 | body {
16 | margin: 0;
17 | padding: 0;
18 | background: helpers.mat-theme-color(background, background);
19 | font-family: Roboto, "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
20 | }
21 |
22 | * {
23 | box-sizing: border-box;
24 | }
25 |
26 | code {
27 | font-size: 90%;
28 | font-family: 'Roboto Mono', monospace;
29 | }
30 |
31 | h1, h2 {
32 | font-weight: 400;
33 | }
34 |
35 | .spacer {
36 | flex: 1 1 auto;
37 | }
38 |
39 | .docs {
40 | p {
41 | font-size: 16px;
42 | line-height: 28px;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/projects/demo-page/src/styles/helpers.scss:
--------------------------------------------------------------------------------
1 | @use '@angular/material' as mat;
2 |
3 | $mat-app-primary: mat.define-palette(mat.$indigo-palette);
4 | $mat-app-accent: mat.define-palette(mat.$amber-palette);
5 |
6 | $mat-app-theme: mat.define-light-theme((
7 | color: (
8 | primary: $mat-app-primary,
9 | accent: $mat-app-accent,
10 | ),
11 | typography: mat.define-typography-config(),
12 | density: 0,
13 | ));
14 |
15 | @function mat-theme-color($type: primary, $hue: default) {
16 | $color: map-get($mat-app-theme, $type);
17 | @return mat.get-color-from-palette($color, $hue);
18 | }
19 |
20 | @mixin code() {
21 | background: rgba(0, 0, 0, 0.03);
22 | padding: 3px;
23 | font-family: 'Roboto Mono',monospace;
24 | }
25 |
--------------------------------------------------------------------------------
/projects/demo-page/src/styles/highlight.scss:
--------------------------------------------------------------------------------
1 | .hljs {
2 | display: block;
3 | overflow-x: auto;
4 | padding: 1em;
5 | background: #fafafa;
6 | color: #37474f;
7 | -webkit-font-smoothing: antialiased;
8 | -webkit-text-size-adjust: 100%;
9 | -moz-text-size-adjust: 100%;
10 | -ms-text-size-adjust: 100%;
11 | text-size-adjust: 100%;
12 | font: 300 100%/1 Roboto Mono, monospace;
13 | font-size: 14px;
14 | line-height: normal;
15 | }
16 |
17 | .hljs-section, .hljs > ::-moz-selection {
18 | background-color: #d6edea
19 | }
20 |
21 | .hljs-section, .hljs > ::selection {
22 | background-color: #d6edea
23 | }
24 |
25 | .hljs-comment {
26 | color: #b0bec5;
27 | font-style: italic
28 | }
29 |
30 | .hljs-meta, .hljs-regexp, .hljs-selector-tag, .hljs-tag {
31 | color: #9c27b0
32 | }
33 |
34 | .hljs-string, .hljs-subst {
35 | color: #0d904f
36 | }
37 |
38 | .hljs-number, .hljs-template-variable, .hljs-variable {
39 | color: #80cbc4
40 | }
41 |
42 | .hljs-attribute, .hljs-keyword, .hljs-name, .hljs-type {
43 | color: #3b78e7
44 | }
45 |
46 | .hljs-built_in, .hljs-builtin-name, .hljs-bullet, .hljs-function > .hljs-title, .hljs-link, .hljs-symbol, .hljs-title {
47 | color: #6182b8
48 | }
49 |
50 | .hljs-params {
51 | color: #d81b60
52 | }
53 |
54 | .hljs-addition {
55 | color: #3b78e7;
56 | display: inline-block;
57 | width: 100%
58 | }
59 |
60 | .hljs-deletion {
61 | color: #e53935;
62 | display: inline-block;
63 | width: 100%
64 | }
65 |
66 | .hljs-selector-class, .hljs-selector-id {
67 | color: #8796b0
68 | }
69 |
70 | .hljs-emphasis {
71 | font-style: italic
72 | }
73 |
74 | .hljs-strong {
75 | font-weight: 700
76 | }
77 |
78 | .hljs-link {
79 | text-decoration: underline
80 | }
81 |
--------------------------------------------------------------------------------
/projects/demo-page/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/app"
5 | },
6 | "files": [
7 | "src/main.ts",
8 | "src/polyfills.ts"
9 | ],
10 | "include": [
11 | "src/**/*.d.ts"
12 | ],
13 | "angularCompilerOptions": {
14 | "strictInjectionParameters": true,
15 | "strictInputAccessModifiers": true,
16 | "strictTemplates": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../.eslintrc.json",
3 | "ignorePatterns": [
4 | "!**/*"
5 | ],
6 | "overrides": [
7 | {
8 | "files": [
9 | "*.ts"
10 | ],
11 | "parserOptions": {
12 | "project": [
13 | "projects/ng-table-virtual-scroll/tsconfig.lib.json",
14 | "projects/ng-table-virtual-scroll/tsconfig.spec.json"
15 | ],
16 | "createDefaultProgram": true
17 | },
18 | "rules": {
19 | "@angular-eslint/directive-selector": [
20 | "error",
21 | {
22 | "type": "attribute",
23 | "prefix": "tvs",
24 | "style": "camelCase"
25 | }
26 | ],
27 | "@angular-eslint/component-selector": [
28 | "error",
29 | {
30 | "type": "element",
31 | "prefix": "tvs",
32 | "style": "kebab-case"
33 | }
34 | ]
35 | }
36 | },
37 | {
38 | "files": [
39 | "*.html"
40 | ],
41 | "rules": {}
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "../../dist/ng-table-virtual-scroll",
4 | "lib": {
5 | "entryFile": "src/public-api.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ng-table-virtual-scroll",
3 | "version": "1.6.1",
4 | "description": "Virtual scroll for for Angular Material Table",
5 | "homepage": "https://github.com/diprokon/ng-table-virtual-scroll",
6 | "author": {
7 | "name": "Dmytro Prokhorov",
8 | "url": "https://github.com/diprokon"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git://github.com/diprokon/ng-table-virtual-scroll.git"
13 | },
14 | "bugs": {
15 | "url": "https://github.com/diprokon/ng-table-virtual-scroll/issues"
16 | },
17 | "keywords": [
18 | "angular",
19 | "material",
20 | "scroll",
21 | "virtual scroll",
22 | "table"
23 | ],
24 | "license": "MIT",
25 | "dependencies": {
26 | "tslib": "^2.0.0"
27 | },
28 | "peerDependencies": {
29 | "@angular/common": ">=15.0.0",
30 | "@angular/core": ">=15.0.0",
31 | "@angular/cdk": ">=15.0.0",
32 | "@angular/material": ">=15.0.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/src/lib/fixed-size-table-virtual-scroll-strategy.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { distinctUntilChanged } from 'rxjs/operators';
3 | import { BehaviorSubject, Subject } from 'rxjs';
4 | import { CdkVirtualScrollViewport, VirtualScrollStrategy } from '@angular/cdk/scrolling';
5 | import { ListRange } from '@angular/cdk/collections';
6 |
7 | export interface TSVStrategyConfigs {
8 | rowHeight: number;
9 | headerHeight: number;
10 | footerHeight: number;
11 | bufferMultiplier: number;
12 | }
13 |
14 | @Injectable()
15 | export class FixedSizeTableVirtualScrollStrategy implements VirtualScrollStrategy {
16 | private rowHeight!: number;
17 | private headerHeight!: number;
18 | private footerHeight!: number;
19 | private bufferMultiplier!: number;
20 | private indexChange = new Subject();
21 | public stickyChange = new Subject();
22 |
23 | public viewport: CdkVirtualScrollViewport;
24 |
25 | public renderedRangeStream = new BehaviorSubject({start: 0, end: 0});
26 |
27 | public scrolledIndexChange = this.indexChange.pipe(distinctUntilChanged());
28 |
29 | get dataLength(): number {
30 | return this._dataLength;
31 | }
32 |
33 | set dataLength(value: number) {
34 | if (value !== this._dataLength) {
35 | this._dataLength = value;
36 | this.onDataLengthChanged();
37 | }
38 | }
39 |
40 | private _dataLength = 0;
41 |
42 | public attach(viewport: CdkVirtualScrollViewport): void {
43 | this.viewport = viewport;
44 | this.viewport.renderedRangeStream.subscribe(this.renderedRangeStream);
45 | this.stickyChange.next(0);
46 | this.onDataLengthChanged();
47 | }
48 |
49 | public detach(): void {
50 | this.indexChange.complete();
51 | this.stickyChange.complete();
52 | this.renderedRangeStream.complete();
53 | }
54 |
55 | public onContentScrolled(): void {
56 | this.updateContent();
57 | }
58 |
59 | public onDataLengthChanged(): void {
60 | if (this.viewport) {
61 | const contentSize = this.dataLength * this.rowHeight + this.headerHeight + this.footerHeight;
62 | this.viewport.setTotalContentSize(contentSize);
63 | const viewportSize = this.viewport.getViewportSize();
64 | if (this.viewport.measureScrollOffset() + viewportSize >= contentSize) {
65 | this.viewport.scrollToOffset(contentSize - viewportSize);
66 | }
67 | }
68 | this.updateContent();
69 | }
70 |
71 | public onContentRendered(): void {
72 | }
73 |
74 | public onRenderedOffsetChanged(): void {
75 | // no-op
76 | }
77 |
78 | public scrollToIndex(index: number, behavior?: ScrollBehavior): void {
79 | if (!this.viewport || !this.rowHeight) {
80 | return;
81 | }
82 | this.viewport.scrollToOffset((index - 1 ) * this.rowHeight + this.headerHeight, behavior);
83 | }
84 |
85 | public setConfig(configs: TSVStrategyConfigs) {
86 | const {rowHeight, headerHeight, footerHeight, bufferMultiplier} = configs;
87 | if (
88 | this.rowHeight === rowHeight
89 | && this.headerHeight === headerHeight
90 | && this.footerHeight === footerHeight
91 | && this.bufferMultiplier === bufferMultiplier
92 | ) {
93 | return;
94 | }
95 | this.rowHeight = rowHeight;
96 | this.headerHeight = headerHeight;
97 | this.footerHeight = footerHeight;
98 | this.bufferMultiplier = bufferMultiplier;
99 | this.onDataLengthChanged();
100 | }
101 |
102 | private updateContent() {
103 | if (!this.viewport || !this.rowHeight) {
104 | return;
105 | }
106 |
107 | const renderedOffset = this.viewport.getOffsetToRenderedContentStart();
108 | const start = renderedOffset / this.rowHeight;
109 | const itemsDisplayed = Math.ceil(this.viewport.getViewportSize() / this.rowHeight);
110 | const bufferItems = Math.ceil(itemsDisplayed * this.bufferMultiplier);
111 | const end = start + itemsDisplayed + 2 * bufferItems;
112 |
113 |
114 | const bufferOffset = renderedOffset + bufferItems * this.rowHeight;
115 | const scrollOffset = this.viewport.measureScrollOffset();
116 |
117 | // How far the scroll offset is from the lower buffer, which is usually where items start being displayed
118 | const relativeScrollOffset = scrollOffset - bufferOffset;
119 | const rowsScrolled = relativeScrollOffset / this.rowHeight;
120 |
121 | const displayed = scrollOffset / this.rowHeight;
122 | this.indexChange.next(displayed);
123 |
124 | // Only bother updating the displayed information if we've scrolled more than a row
125 | const rowSensitivity = 1.0;
126 | if (Math.abs(rowsScrolled) < rowSensitivity) {
127 | this.viewport.setRenderedContentOffset(renderedOffset);
128 | this.viewport.setRenderedRange({start, end});
129 | return;
130 | }
131 |
132 | // Special case for the start of the table.
133 | // At the top of the table, the first few rows are first rendered because they're visible, and then still rendered
134 | // Because they move into the buffer. So we only need to change what's rendered once the user scrolls far enough down.
135 | if (renderedOffset === 0 && rowsScrolled < 0) {
136 | this.viewport.setRenderedContentOffset(renderedOffset);
137 | this.viewport.setRenderedRange({start, end});
138 | return;
139 | }
140 |
141 | const rowsToMove = Math.sign(rowsScrolled) * Math.floor(Math.abs(rowsScrolled));
142 | const adjustedRenderedOffset = Math.max(0, renderedOffset + rowsToMove * this.rowHeight);
143 | this.viewport.setRenderedContentOffset(adjustedRenderedOffset);
144 |
145 | const adjustedStart = Math.max(0, start + rowsToMove);
146 | const adjustedEnd = adjustedStart + itemsDisplayed + 2 * bufferItems;
147 | this.viewport.setRenderedRange({start: adjustedStart, end: adjustedEnd});
148 |
149 | this.stickyChange.next(adjustedRenderedOffset);
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/src/lib/table-data-source.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { CdkTableVirtualScrollDataSource, TableVirtualScrollDataSource, TVSDataSource } from './table-data-source';
3 | import { Subject } from 'rxjs';
4 | import { ListRange } from '@angular/cdk/collections';
5 | import { map, switchMap } from 'rxjs/operators';
6 | import { DataSource } from '@angular/cdk/table';
7 | import { Type } from '@angular/core';
8 | import { MatTableDataSource } from '@angular/material/table';
9 |
10 | interface TestData {
11 | index: number;
12 | }
13 |
14 | function getTestData(n = 10): TestData[] {
15 | return Array.from({ length: n }).map((e, i) => ({ index: i }));
16 | }
17 |
18 | describe('TableVirtualScrollDataSource', () => {
19 | beforeEach(() => TestBed.configureTestingModule({}));
20 |
21 | runDataSourceTests(TableVirtualScrollDataSource);
22 |
23 | it('should extend MatTableDataSource', () => {
24 | const dataSource: TVSDataSource = new TableVirtualScrollDataSource();
25 | expect(dataSource instanceof MatTableDataSource).toBeTruthy();
26 | });
27 | });
28 |
29 | describe('CdkTableVirtualScrollDataSource', () => {
30 | beforeEach(() => TestBed.configureTestingModule({}));
31 |
32 | runDataSourceTests(CdkTableVirtualScrollDataSource);
33 |
34 | it('should extend DataSource', () => {
35 | const dataSource: TVSDataSource = new CdkTableVirtualScrollDataSource();
36 | expect(dataSource instanceof DataSource).toBeTruthy();
37 | });
38 | });
39 |
40 | function runDataSourceTests(
41 | // tslint:disable-next-line:variable-name
42 | DataSourceClass: Type>
43 | ) {
44 |
45 | it('should be created', () => {
46 | const dataSource: TVSDataSource = new DataSourceClass();
47 | expect(dataSource).toBeTruthy();
48 |
49 | const dataSource2: TVSDataSource = new DataSourceClass([{ index: 0 }]);
50 | expect(dataSource2).toBeTruthy();
51 | });
52 |
53 |
54 | it('should have reaction on dataOfRange$ changes', () => {
55 | const testData: TestData[] = getTestData();
56 | const dataSource: TVSDataSource = new DataSourceClass(testData);
57 | const stream = new Subject();
58 |
59 | stream.subscribe(dataSource.dataOfRange$);
60 |
61 | const renderData: Subject = dataSource['_renderData'];
62 |
63 | let count = -1; // renderData is BehaviorSubject with base value '[]'
64 | renderData.subscribe(() => {
65 | count++;
66 | });
67 |
68 | stream.next(testData.slice(0, 1));
69 | stream.next(testData);
70 |
71 | expect(count).toBe(2);
72 | });
73 |
74 | it('should provide correct data', () => {
75 | const testData: TestData[] = getTestData(10);
76 | const dataSource: TVSDataSource = new DataSourceClass(testData);
77 | const stream = new Subject();
78 |
79 | dataSource.dataToRender$
80 | .pipe(
81 | switchMap(data => stream
82 | .pipe(
83 | map(({ start, end }) => data.slice(start, end))
84 | )
85 | )
86 | )
87 | .subscribe(dataSource.dataOfRange$);
88 |
89 | const renderData: Subject = dataSource['_renderData'];
90 |
91 | const results: TestData[][] = [];
92 |
93 | renderData.subscribe((data) => {
94 | results.push(data);
95 | });
96 |
97 | stream.next({ start: 0, end: 2 });
98 | stream.next({ start: 8, end: testData.length });
99 |
100 | expect(results).toEqual([
101 | [],
102 | [{ index: 0 }, { index: 1 }],
103 | [{ index: 8 }, { index: 9 }]
104 | ]);
105 | });
106 | }
107 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/src/lib/table-data-source.ts:
--------------------------------------------------------------------------------
1 | import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject, Subject, Subscription } from 'rxjs';
2 | import { map } from 'rxjs/operators';
3 | import { MatTableDataSource } from '@angular/material/table';
4 | import { MatSort, Sort } from '@angular/material/sort';
5 | import { MatPaginator, PageEvent } from '@angular/material/paginator';
6 | import { DataSource } from '@angular/cdk/collections';
7 |
8 | export interface TVSDataSource {
9 | dataToRender$: Subject;
10 | dataOfRange$: Subject;
11 | }
12 |
13 | export function isTVSDataSource(dataSource: unknown): dataSource is TVSDataSource {
14 | return dataSource instanceof CdkTableVirtualScrollDataSource || dataSource instanceof TableVirtualScrollDataSource;
15 | }
16 |
17 | export class CdkTableVirtualScrollDataSource extends DataSource implements TVSDataSource {
18 | /** Stream that emits when a new data array is set on the data source. */
19 | private readonly _data: BehaviorSubject;
20 |
21 | /** Stream emitting render data to the table (depends on ordered data changes). */
22 | private readonly _renderData = new BehaviorSubject([]);
23 |
24 | /**
25 | * Subscription to the changes that should trigger an update to the table's rendered rows, such
26 | * as filtering, sorting, pagination, or base data changes.
27 | */
28 | _renderChangesSubscription: Subscription | null = null;
29 |
30 | /** Array of data that should be rendered by the table, where each object represents one row. */
31 | get data() {
32 | return this._data.value;
33 | }
34 |
35 | set data(data: T[]) {
36 | data = Array.isArray(data) ? data : [];
37 | this._data.next(data);
38 | }
39 |
40 | public dataToRender$: Subject;
41 | public dataOfRange$: Subject;
42 | private streamsReady: boolean;
43 |
44 |
45 | constructor(initialData: T[] = []) {
46 | super();
47 | this._data = new BehaviorSubject(initialData);
48 | this._updateChangeSubscription();
49 | }
50 |
51 | _updateChangeSubscription() {
52 | this.initStreams();
53 |
54 | this._renderChangesSubscription?.unsubscribe();
55 | this._renderChangesSubscription = new Subscription();
56 | this._renderChangesSubscription.add(
57 | this._data.subscribe(data => this.dataToRender$.next(data))
58 | );
59 | this._renderChangesSubscription.add(
60 | this.dataOfRange$.subscribe(data => this._renderData.next(data))
61 | );
62 | }
63 |
64 | connect() {
65 | if (!this._renderChangesSubscription) {
66 | this._updateChangeSubscription();
67 | }
68 |
69 | return this._renderData;
70 | }
71 |
72 |
73 | disconnect() {
74 | this._renderChangesSubscription?.unsubscribe();
75 | this._renderChangesSubscription = null;
76 | }
77 |
78 | private initStreams() {
79 | if (!this.streamsReady) {
80 | this.dataToRender$ = new ReplaySubject(1);
81 | this.dataOfRange$ = new ReplaySubject(1);
82 | this.streamsReady = true;
83 | }
84 | }
85 | }
86 |
87 | export class TableVirtualScrollDataSource extends MatTableDataSource implements TVSDataSource {
88 | public dataToRender$: Subject;
89 | public dataOfRange$: Subject;
90 | private streamsReady: boolean;
91 |
92 | _updateChangeSubscription() {
93 | this.initStreams();
94 | const _sort: MatSort | null = this['_sort'];
95 | const _paginator: MatPaginator | null = this['_paginator'];
96 | const _internalPageChanges: Subject = this['_internalPageChanges'];
97 | const _filter: BehaviorSubject = this['_filter'];
98 | const _renderData: BehaviorSubject = this['_renderData'];
99 |
100 | const sortChange: Observable = _sort ?
101 | merge(_sort.sortChange, _sort.initialized) as Observable :
102 | of(null);
103 | const pageChange: Observable = _paginator ?
104 | merge(
105 | _paginator.page,
106 | _internalPageChanges,
107 | _paginator.initialized
108 | ) as Observable :
109 | of(null);
110 | const dataStream: Observable = this['_data'];
111 | const filteredData = combineLatest([dataStream, _filter])
112 | .pipe(map(([data]) => this._filterData(data)));
113 | const orderedData = combineLatest([filteredData, sortChange])
114 | .pipe(map(([data]) => this._orderData(data)));
115 | const paginatedData = combineLatest([orderedData, pageChange])
116 | .pipe(map(([data]) => this._pageData(data)));
117 |
118 | this._renderChangesSubscription?.unsubscribe();
119 | this._renderChangesSubscription = new Subscription();
120 | this._renderChangesSubscription.add(
121 | paginatedData.subscribe(data => this.dataToRender$.next(data))
122 | );
123 | this._renderChangesSubscription.add(
124 | this.dataOfRange$.subscribe(data => _renderData.next(data))
125 | );
126 | }
127 |
128 | private initStreams() {
129 | if (!this.streamsReady) {
130 | this.dataToRender$ = new ReplaySubject(1);
131 | this.dataOfRange$ = new ReplaySubject(1);
132 | this.streamsReady = true;
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/src/lib/table-item-size.directive.cy.ts:
--------------------------------------------------------------------------------
1 | import { DataSource } from '@angular/cdk/collections';
2 | import { CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling';
3 | import { CdkTableModule } from '@angular/cdk/table';
4 | import { Component, Type, ViewChild, ViewEncapsulation } from '@angular/core';
5 | import { MatLegacyTableModule } from '@angular/material/legacy-table';
6 | import { MatTableModule } from '@angular/material/table';
7 | import { mount } from 'cypress/angular';
8 | import { CdkTableVirtualScrollDataSource, TableVirtualScrollDataSource } from './table-data-source';
9 | import { TableItemSizeDirective } from './table-item-size.directive';
10 |
11 | interface Data {
12 | id: number;
13 | }
14 |
15 | const ITEMS_COUNT = 5000;
16 | const ITEMS_COUNT2 = 500;
17 |
18 | const VIEWPORT_HEIGHT = 200;
19 | const ROW_HEIGHT = 20;
20 | const HEADER_HEIGHT = 30;
21 | const FOOTER_HEIGHT = 15;
22 | const BUFFER_MULTIPLIER = 0.5;
23 |
24 | const VISIBLE_ITEMS_COUNT = Math.ceil(VIEWPORT_HEIGHT / ROW_HEIGHT);
25 |
26 | abstract class TestComponent {
27 | headerEnabled = true;
28 | footerEnabled = false;
29 | stickyHeader = false;
30 | stickyFooter = false;
31 |
32 | @ViewChild(CdkVirtualScrollViewport, { static: true })
33 | viewport: CdkVirtualScrollViewport;
34 |
35 | @ViewChild(TableItemSizeDirective, { static: true })
36 | directive: TableItemSizeDirective;
37 |
38 | displayedColumns = ['id'];
39 |
40 | data = Array(ITEMS_COUNT).fill(0).map((_, i) => ({ id: i }));
41 | data2 = Array(ITEMS_COUNT2).fill(0).map((_, i) => ({ id: i + ITEMS_COUNT }));
42 |
43 | dataSource = new this.dataSourceClass(this.data);
44 |
45 | protected constructor(protected dataSourceClass: Type>) {
46 | }
47 |
48 | changeDataSource() {
49 | this.dataSource = new this.dataSourceClass(this.data2);
50 | }
51 | }
52 |
53 | @Component({
54 | template: `
55 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | No.
75 | el - {{element.id}}
76 |
77 |
78 |
79 |
80 |
81 |
82 | changeDataSource
83 | `,
84 | styles: [`
85 | .wrapper {
86 | height: ${VIEWPORT_HEIGHT}px;
87 | }
88 |
89 | table {
90 | border-collapse: collapse;
91 | }
92 |
93 | tr {
94 | height: auto !important;
95 | }
96 |
97 | th {
98 | height: ${HEADER_HEIGHT}px !important;
99 | }
100 |
101 | td {
102 | height: ${ROW_HEIGHT}px !important;
103 | }
104 |
105 | th, td {
106 | padding: 0 !important;
107 | margin: 0 !important;
108 | border-width: 0 !important;
109 | border-style: none !important;
110 | font-size: 8px;
111 | }
112 |
113 | .footer-cell {
114 | height: ${FOOTER_HEIGHT}px !important;
115 | }
116 | `],
117 | encapsulation: ViewEncapsulation.None
118 | })
119 | class MatTableTestComponent extends TestComponent {
120 | constructor() {
121 | super(TableVirtualScrollDataSource);
122 | }
123 | }
124 |
125 |
126 | @Component({
127 | template: `
128 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | No.
148 | el - {{element.id}}
149 |
150 |
151 |
152 |
153 |
154 |
155 | changeDataSource
156 | `,
157 | styles: [`
158 | .wrapper {
159 | height: ${VIEWPORT_HEIGHT}px;
160 | }
161 |
162 | table {
163 | border-collapse: collapse;
164 | }
165 |
166 | tr {
167 | height: auto !important;
168 | }
169 |
170 | th {
171 | height: ${HEADER_HEIGHT}px !important;
172 | }
173 |
174 | td {
175 | height: ${ROW_HEIGHT}px !important;
176 | }
177 |
178 | th, td {
179 | padding: 0 !important;
180 | margin: 0 !important;
181 | border-width: 0 !important;
182 | border-style: none !important;
183 | font-size: 8px;
184 | }
185 |
186 | .footer-cell {
187 | height: ${FOOTER_HEIGHT}px !important;
188 | }
189 | `],
190 | encapsulation: ViewEncapsulation.None
191 | })
192 | class CdkTableTestComponent extends TestComponent {
193 | constructor() {
194 | super(CdkTableVirtualScrollDataSource);
195 | }
196 | }
197 |
198 | describe('TableItemSizeDirective', () => {
199 | describe('CdkTable', () => {
200 | runTableTests(CdkTableModule, CdkTableTestComponent);
201 | });
202 | describe('MatTable', () => {
203 | runTableTests(MatTableModule, MatTableTestComponent);
204 | });
205 | describe('MatLegacyTableModule', () => {
206 | runTableTests(MatLegacyTableModule, MatTableTestComponent);
207 | });
208 | });
209 |
210 | function runTableTests(
211 | tableModule: typeof CdkTableModule,
212 | tableComponent: Type
213 | ) {
214 | describe('common actions', () => {
215 | let testComponent: TestComponent;
216 | let viewport: CdkVirtualScrollViewport;
217 |
218 | beforeEach(() => {
219 | mount(tableComponent, {
220 | imports: [ScrollingModule, tableModule],
221 | declarations: [TableItemSizeDirective]
222 | })
223 | .then(mountResponse => {
224 | testComponent = mountResponse.component;
225 | viewport = testComponent.viewport;
226 | });
227 | });
228 |
229 | it('should init correct state', () => {
230 | // should render buffer before, visible rows and buffer after
231 | const renderedRowsCount = VISIBLE_ITEMS_COUNT * (BUFFER_MULTIPLIER + 1 + BUFFER_MULTIPLIER);
232 | cy.get('tbody tr').should('have.length', renderedRowsCount);
233 | expect(viewport.getRenderedRange()).to.deep.equal({ start: 0, end: renderedRowsCount });
234 | cy.get('.wrapper')
235 | .then(el => {
236 | expect(el.get(0).scrollHeight).to.equal(HEADER_HEIGHT + ROW_HEIGHT * ITEMS_COUNT);
237 | });
238 | });
239 |
240 | it('should set the correct rendered range on scroll', () => {
241 | const rowsToScroll = 100;
242 | const bufferSize = BUFFER_MULTIPLIER * VISIBLE_ITEMS_COUNT;
243 | cy.get('.wrapper')
244 | .scrollTo(0, rowsToScroll * ROW_HEIGHT)
245 | .wait(0)
246 | .then(() => {
247 | expect(viewport.getRenderedRange())
248 | .to.deep.equal({ start: rowsToScroll - bufferSize, end: rowsToScroll + VISIBLE_ITEMS_COUNT + bufferSize });
249 | });
250 | });
251 |
252 | it('should subscribe and rerender after dataSource is changed', () => {
253 | cy.get('tbody')
254 | .children().first()
255 | .should('contain', 'el - 0');
256 |
257 | cy.get('[data-cy="changeDataSource"]')
258 | .click();
259 |
260 | cy.get('tbody')
261 | .children().first()
262 | .should('contain', `el - ${ITEMS_COUNT}`);
263 |
264 | });
265 |
266 | it('should check scroll position after dataSource is changed', () => {
267 | cy.get('tbody')
268 | .children().first()
269 | .should('contain', 'el - 0');
270 |
271 | cy.get('.wrapper')
272 | .scrollTo('bottom')
273 | .wait(0);
274 |
275 | cy.get('tbody')
276 | .children().last()
277 | .should('contain', `el - ${ITEMS_COUNT - 1}`);
278 |
279 | cy.get('[data-cy="changeDataSource"]')
280 | .click();
281 |
282 | cy.get('tbody')
283 | .children().last()
284 | .should('contain', `el - ${ITEMS_COUNT + ITEMS_COUNT2 - 1}`);
285 |
286 | });
287 | });
288 |
289 | describe('initialization variants', () => {
290 | it('should have correct height with footer', () => {
291 | mount(tableComponent, {
292 | imports: [ScrollingModule, tableModule],
293 | declarations: [TableItemSizeDirective],
294 | componentProperties: {
295 | footerEnabled: true
296 | }
297 | });
298 |
299 | cy.get('.wrapper')
300 | .wait(0)
301 | .then(el => {
302 | expect(el.get(0).scrollHeight).to.equal(HEADER_HEIGHT + ROW_HEIGHT * ITEMS_COUNT + FOOTER_HEIGHT);
303 | });
304 | });
305 |
306 | it('should have correct height without header', () => {
307 | mount(tableComponent, {
308 | imports: [ScrollingModule, tableModule],
309 | declarations: [TableItemSizeDirective],
310 | componentProperties: {
311 | headerEnabled: false,
312 | footerEnabled: false
313 | }
314 | });
315 |
316 | cy.get('.wrapper')
317 | .wait(0)
318 | .then(el => {
319 | expect(el.get(0).scrollHeight).to.equal(ROW_HEIGHT * ITEMS_COUNT);
320 | });
321 | });
322 |
323 | const tests: {
324 | title: string;
325 | props: {
326 | headerEnabled?: boolean;
327 | footerEnabled?: boolean;
328 | stickyHeader?: boolean;
329 | stickyFooter?: boolean;
330 | },
331 | checks: {
332 | header?: boolean;
333 | footer?: boolean;
334 | }
335 | }[] =
336 | [
337 | {
338 | title: 'sticky header',
339 | props: {
340 | headerEnabled: true,
341 | stickyHeader: true
342 | },
343 | checks: {
344 | header: true
345 | }
346 | },
347 | {
348 | title: 'sticky footer',
349 | props: {
350 | headerEnabled: false,
351 | footerEnabled: true,
352 | stickyFooter: true
353 | },
354 | checks: {
355 | footer: true
356 | }
357 | },
358 | {
359 | title: 'sticky header with footer enabled',
360 | props: {
361 | headerEnabled: true,
362 | footerEnabled: true,
363 | stickyHeader: true
364 | },
365 | checks: {
366 | header: true
367 | }
368 | },
369 | {
370 | title: 'sticky footer with header enabled',
371 | props: {
372 | headerEnabled: true,
373 | footerEnabled: true,
374 | stickyFooter: true
375 | },
376 | checks: {
377 | footer: true
378 | }
379 | },
380 | {
381 | title: 'sticky header and footer',
382 | props: {
383 | headerEnabled: true,
384 | footerEnabled: true,
385 | stickyHeader: true,
386 | stickyFooter: true
387 | },
388 | checks: {
389 | header: true,
390 | footer: true
391 | }
392 | },
393 | ];
394 |
395 | tests.forEach(test => {
396 | it('should have visible ' + test.title, () => {
397 | mount(tableComponent, {
398 | imports: [ScrollingModule, tableModule],
399 | declarations: [TableItemSizeDirective],
400 | componentProperties: test.props
401 | });
402 |
403 | const rowsToScroll = 100;
404 | cy.get('.wrapper')
405 | .scrollTo(0, rowsToScroll * ROW_HEIGHT)
406 | .wait(0);
407 |
408 | if (test.props.headerEnabled) {
409 | cy.get('th')
410 | .should((test.checks.header ? '' : 'not.') + `be.inViewport`, '.wrapper');
411 | }
412 | if (test.props.footerEnabled) {
413 | cy.get('.footer-cell')
414 | .should((test.checks.footer ? '' : 'not.') + `be.inViewport`, '.wrapper');
415 | }
416 | });
417 | });
418 | });
419 | }
420 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/src/lib/table-item-size.directive.ts:
--------------------------------------------------------------------------------
1 | import { VIRTUAL_SCROLL_STRATEGY } from '@angular/cdk/scrolling';
2 | import { CanStick, CdkTable } from '@angular/cdk/table';
3 | import {
4 | AfterContentInit,
5 | ContentChild,
6 | Directive,
7 | forwardRef,
8 | Input,
9 | NgZone,
10 | OnChanges,
11 | OnDestroy
12 | } from '@angular/core';
13 | import { MatTable } from '@angular/material/table';
14 | import { combineLatest, from, Subject } from 'rxjs';
15 | import { delayWhen, distinctUntilChanged, map, startWith, switchMap, take, takeUntil, tap } from 'rxjs/operators';
16 | import { FixedSizeTableVirtualScrollStrategy } from './fixed-size-table-virtual-scroll-strategy';
17 | import { CdkTableVirtualScrollDataSource, isTVSDataSource, TableVirtualScrollDataSource } from './table-data-source';
18 |
19 | export function _tableVirtualScrollDirectiveStrategyFactory(tableDir: TableItemSizeDirective) {
20 | return tableDir.scrollStrategy;
21 | }
22 |
23 | function combineSelectors(...pairs: string[][]): string {
24 | return pairs.map((selectors) => `${selectors.join(' ')}, ${selectors.join('')}`).join(', ');
25 | }
26 |
27 | const stickyHeaderSelector = combineSelectors(
28 | ['.mat-mdc-header-row', '.mat-mdc-table-sticky'],
29 | ['.mat-header-row', '.mat-table-sticky'],
30 | ['.cdk-header-row', '.cdk-table-sticky']
31 | );
32 |
33 | const stickyFooterSelector = combineSelectors(
34 | ['.mat-mdc-footer-row', '.mat-mdc-table-sticky'],
35 | ['.mat-footer-row', '.mat-table-sticky'],
36 | ['.cdk-footer-row', '.cdk-table-sticky']
37 | );
38 |
39 | function isMatTable(table: unknown): table is MatTable {
40 | return table instanceof CdkTable && table['stickyCssClass'].includes('mat');
41 | }
42 |
43 | function isCdkTable(table: unknown): table is CdkTable {
44 | return table instanceof CdkTable && table['stickyCssClass'].includes('cdk');
45 | }
46 |
47 | const defaults = {
48 | rowHeight: 48,
49 | headerHeight: 56,
50 | headerEnabled: true,
51 | footerHeight: 48,
52 | footerEnabled: false,
53 | bufferMultiplier: 0.7
54 | };
55 |
56 | @Directive({
57 | selector: 'cdk-virtual-scroll-viewport[tvsItemSize]',
58 | providers: [{
59 | provide: VIRTUAL_SCROLL_STRATEGY,
60 | useFactory: _tableVirtualScrollDirectiveStrategyFactory,
61 | deps: [forwardRef(() => TableItemSizeDirective)]
62 | }]
63 | })
64 | export class TableItemSizeDirective implements OnChanges, AfterContentInit, OnDestroy {
65 | private destroyed$ = new Subject();
66 |
67 | // eslint-disable-next-line @angular-eslint/no-input-rename
68 | @Input('tvsItemSize')
69 | rowHeight: string | number = defaults.rowHeight;
70 |
71 | @Input()
72 | headerEnabled: boolean = defaults.headerEnabled;
73 |
74 | @Input()
75 | headerHeight: string | number = defaults.headerHeight;
76 |
77 | @Input()
78 | footerEnabled: boolean = defaults.footerEnabled;
79 |
80 | @Input()
81 | footerHeight: string | number = defaults.footerHeight;
82 |
83 | @Input()
84 | bufferMultiplier: string | number = defaults.bufferMultiplier;
85 |
86 | @ContentChild(CdkTable, { static: false })
87 | table: CdkTable;
88 |
89 | scrollStrategy = new FixedSizeTableVirtualScrollStrategy();
90 |
91 | dataSourceChanges = new Subject();
92 |
93 | private stickyPositions: Map;
94 | private resetStickyPositions = new Subject();
95 | private stickyEnabled = {
96 | header: false,
97 | footer: false
98 | };
99 |
100 | constructor(private zone: NgZone) {
101 | }
102 |
103 | ngOnDestroy() {
104 | this.destroyed$.next();
105 | this.destroyed$.complete();
106 | this.dataSourceChanges.complete();
107 | }
108 |
109 | ngAfterContentInit() {
110 | const switchDataSourceOrigin = this.table['_switchDataSource'];
111 | this.table['_switchDataSource'] = (dataSource: any) => {
112 | switchDataSourceOrigin.call(this.table, dataSource);
113 | this.connectDataSource(dataSource);
114 | };
115 |
116 | const updateStickyColumnStylesOrigin = this.table.updateStickyColumnStyles;
117 | this.table.updateStickyColumnStyles = () => {
118 | const stickyColumnStylesNeedReset = this.table['_stickyColumnStylesNeedReset'];
119 | updateStickyColumnStylesOrigin.call(this.table);
120 | if (stickyColumnStylesNeedReset) {
121 | this.resetStickyPositions.next();
122 | }
123 | };
124 |
125 | this.connectDataSource(this.table.dataSource);
126 |
127 | combineLatest([
128 | this.scrollStrategy.stickyChange,
129 | this.resetStickyPositions.pipe(
130 | startWith(void 0),
131 | delayWhen(() => this.getScheduleObservable()),
132 | tap(() => {
133 | this.stickyPositions = null;
134 | })
135 | )
136 | ])
137 | .pipe(
138 | takeUntil(this.destroyed$)
139 | )
140 | .subscribe(([stickyOffset]) => {
141 | if (!this.stickyPositions) {
142 | this.initStickyPositions();
143 | }
144 | if (this.stickyEnabled.header) {
145 | this.setStickyHeader(stickyOffset);
146 | }
147 | if (this.stickyEnabled.footer) {
148 | this.setStickyFooter(stickyOffset);
149 | }
150 | });
151 | }
152 |
153 | connectDataSource(dataSource: unknown) {
154 | this.dataSourceChanges.next();
155 | if (!isTVSDataSource(dataSource)) {
156 | throw new Error('[tvsItemSize] requires TableVirtualScrollDataSource or CdkTableVirtualScrollDataSource be set as [dataSource] of the table');
157 | }
158 | if (isMatTable(this.table) && !(dataSource instanceof TableVirtualScrollDataSource)) {
159 | throw new Error('[tvsItemSize] requires TableVirtualScrollDataSource be set as [dataSource] of [mat-table]');
160 | }
161 | if (isCdkTable(this.table) && !(dataSource instanceof CdkTableVirtualScrollDataSource)) {
162 | throw new Error('[tvsItemSize] requires CdkTableVirtualScrollDataSource be set as [dataSource] of [cdk-table]');
163 | }
164 |
165 | dataSource
166 | .dataToRender$
167 | .pipe(
168 | distinctUntilChanged(),
169 | takeUntil(this.dataSourceChanges),
170 | takeUntil(this.destroyed$),
171 | tap(data => this.scrollStrategy.dataLength = data.length),
172 | switchMap(data =>
173 | this.scrollStrategy
174 | .renderedRangeStream
175 | .pipe(
176 | map(({
177 | start,
178 | end
179 | }) => typeof start !== 'number' || typeof end !== 'number' ? data : data.slice(start, end))
180 | )
181 | )
182 | )
183 | .subscribe(data => {
184 | this.zone.run(() => {
185 | dataSource.dataOfRange$.next(data);
186 | });
187 | });
188 | }
189 |
190 | ngOnChanges() {
191 | const config = {
192 | rowHeight: +this.rowHeight || defaults.rowHeight,
193 | headerHeight: this.headerEnabled ? +this.headerHeight || defaults.headerHeight : 0,
194 | footerHeight: this.footerEnabled ? +this.footerHeight || defaults.footerHeight : 0,
195 | bufferMultiplier: +this.bufferMultiplier || defaults.bufferMultiplier
196 | };
197 | this.scrollStrategy.setConfig(config);
198 | }
199 |
200 | private setStickyEnabled(): boolean {
201 | if (!this.scrollStrategy.viewport) {
202 | this.stickyEnabled = {
203 | header: false,
204 | footer: false
205 | };
206 | return;
207 | }
208 |
209 | const isEnabled = (rowDefs: CanStick[]) => rowDefs
210 | .map(def => def.sticky)
211 | .reduce((prevState, state) => prevState && state, true);
212 |
213 | this.stickyEnabled = {
214 | header: isEnabled(this.table['_headerRowDefs']),
215 | footer: isEnabled(this.table['_footerRowDefs']),
216 | };
217 | }
218 |
219 | private setStickyHeader(offset: number) {
220 | this.scrollStrategy.viewport.elementRef.nativeElement.querySelectorAll(stickyHeaderSelector)
221 | .forEach((el: HTMLElement) => {
222 | const parent = el.parentElement;
223 | let baseOffset = 0;
224 | if (this.stickyPositions.has(parent)) {
225 | baseOffset = this.stickyPositions.get(parent);
226 | }
227 | el.style.top = `${baseOffset - offset}px`;
228 | });
229 | }
230 |
231 | private setStickyFooter(offset: number) {
232 | this.scrollStrategy.viewport.elementRef.nativeElement.querySelectorAll(stickyFooterSelector)
233 | .forEach((el: HTMLElement) => {
234 | const parent = el.parentElement;
235 | let baseOffset = 0;
236 | if (this.stickyPositions.has(parent)) {
237 | baseOffset = this.stickyPositions.get(parent);
238 | }
239 | el.style.bottom = `${-baseOffset + offset}px`;
240 | });
241 | }
242 |
243 | private initStickyPositions() {
244 | this.stickyPositions = new Map();
245 |
246 | this.setStickyEnabled();
247 |
248 | if (this.stickyEnabled.header) {
249 | this.scrollStrategy.viewport.elementRef.nativeElement.querySelectorAll(stickyHeaderSelector)
250 | .forEach(el => {
251 | const parent = el.parentElement;
252 | if (!this.stickyPositions.has(parent)) {
253 | this.stickyPositions.set(parent, parent.offsetTop);
254 | }
255 | });
256 | }
257 |
258 | if (this.stickyEnabled.footer) {
259 | this.scrollStrategy.viewport.elementRef.nativeElement.querySelectorAll(stickyFooterSelector)
260 | .forEach(el => {
261 | const parent = el.parentElement;
262 | if (!this.stickyPositions.has(parent)) {
263 | this.stickyPositions.set(parent, -parent.offsetTop);
264 | }
265 | });
266 | }
267 | }
268 |
269 |
270 | private getScheduleObservable() {
271 | // Use onStable when in the context of an ongoing change detection cycle so that we
272 | // do not accidentally trigger additional cycles.
273 | return this.zone.isStable
274 | ? from(Promise.resolve(undefined))
275 | : this.zone.onStable.pipe(take(1));
276 | }
277 | }
278 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/src/lib/table-virtual-scroll.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { TableItemSizeDirective } from './table-item-size.directive';
3 |
4 |
5 | @NgModule({
6 | declarations: [TableItemSizeDirective],
7 | imports: [],
8 | exports: [TableItemSizeDirective]
9 | })
10 | export class TableVirtualScrollModule {
11 | }
12 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/src/public-api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of ng-fixed-size-table-virtual-scroll
3 | */
4 |
5 | export * from './lib/table-virtual-scroll.module';
6 | export * from './lib/table-item-size.directive';
7 | export * from './lib/fixed-size-table-virtual-scroll-strategy';
8 | export * from './lib/table-data-source';
9 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/lib",
5 | "declarationMap": true,
6 | "declaration": true,
7 | "inlineSources": true,
8 | "types": [],
9 | "lib": [
10 | "dom",
11 | "es2018"
12 | ]
13 | },
14 | "angularCompilerOptions": {
15 | "skipTemplateCodegen": true,
16 | "strictMetadataEmit": true,
17 | "fullTemplateTypeCheck": true,
18 | "strictInjectionParameters": true,
19 | "enableResourceInlining": true
20 | },
21 | "exclude": [
22 | "**/*.spec.ts",
23 | "**/*.cy.ts"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "declarationMap": false
5 | },
6 | "angularCompilerOptions": {
7 | "compilationMode": "partial"
8 | }
9 | }
--------------------------------------------------------------------------------
/projects/ng-table-virtual-scroll/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../out-tsc/spec",
5 | "esModuleInterop": true,
6 | "types": [
7 | "jest"
8 | ]
9 | },
10 | "include": [
11 | "**/*.spec.ts",
12 | "**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "experimentalDecorators": true,
10 | "module": "es2020",
11 | "moduleResolution": "node",
12 | "importHelpers": true,
13 | "target": "ES2022",
14 | "typeRoots": [
15 | "node_modules/@types"
16 | ],
17 | "lib": [
18 | "es2018",
19 | "dom"
20 | ],
21 | "paths": {
22 | "ng-table-virtual-scroll": [
23 | "projects/ng-table-virtual-scroll/src/public-api.ts"
24 | ]
25 | },
26 | "useDefineForClassFields": false
27 | },
28 | "angularCompilerOptions": {
29 | "fullTemplateTypeCheck": true,
30 | "strictInjectionParameters": true
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/webpack.config.ts:
--------------------------------------------------------------------------------
1 | import { CustomWebpackBrowserSchema, TargetOptions } from '@angular-builders/custom-webpack';
2 | import * as webpack from 'webpack';
3 |
4 | export default (
5 | config: webpack.Configuration,
6 | options: CustomWebpackBrowserSchema,
7 | targetOptions: TargetOptions
8 | ) => {
9 | config.module.rules.push({
10 | resourceQuery: /raw/,
11 | type: 'asset/source',
12 | });
13 |
14 | return config;
15 | };
16 |
--------------------------------------------------------------------------------