├── LICENSE
├── README.md
├── src
├── app
│ ├── app.html
│ ├── app.js
│ └── getData.js
├── child
│ ├── child.css
│ ├── child.html
│ └── child.js
└── virtualScroll
│ ├── virtualScroll.html
│ └── virtualScroll.js
└── virtual-scroll.gif
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 skysan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Virtual Scroll in Lightning Web Component
2 |
3 | ## Image
4 | 
5 |
6 | ## Reference
7 | https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib
--------------------------------------------------------------------------------
/src/app/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/app/app.js:
--------------------------------------------------------------------------------
1 | import { LightningElement, track } from 'lwc';
2 | import getData from './getData';
3 |
4 | export default class App extends LightningElement {
5 |
6 |
7 | @track alldata = [];
8 | @track viewHight = 100;
9 | @track rowheight = 20;
10 |
11 | async connectedCallback() {
12 | this.alldata = await getData({ amountOfRecords: 1000 });
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/getData.js:
--------------------------------------------------------------------------------
1 | export default function getData({ amountOfRecords }) {
2 | const response = [];
3 | for(let index = 1; index < amountOfRecords; index++) {
4 | response.push({
5 | id: index,
6 | name: `dummy${index}`,
7 | email: `dummy${index}@xxx.yyy`
8 | });
9 | }
10 | return response;
11 | }
12 |
--------------------------------------------------------------------------------
/src/child/child.css:
--------------------------------------------------------------------------------
1 | .child-row {
2 | width: 100%;
3 | display: flex;
4 | flex-direction: row;
5 | align-items: center;
6 | justify-content: center;
7 | background: #f3f2f2;
8 | }
9 |
10 | .child-cell {
11 | display: flex;
12 | text-align: left;
13 | text-overflow: ellipsis;
14 | white-space: nowrap;
15 | overflow: hidden;
16 | padding-left: 3px;
17 | }
--------------------------------------------------------------------------------
/src/child/child.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {row.index}
8 |
9 |
10 | {row.name}
11 |
12 |
13 | {row.email}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/child/child.js:
--------------------------------------------------------------------------------
1 | import { LightningElement, api } from 'lwc';
2 |
3 | export default class App extends LightningElement {
4 |
5 | @api row;
6 | @api rowheight = 0;
7 |
8 | handleChange(e) {
9 | // create CustomEvent to dispatch event from Shadow DOM
10 | e.preventDefault();
11 | const selectedEvent = new CustomEvent('select', { detail: this.row.id });
12 | this.dispatchEvent(selectedEvent);
13 | }
14 |
15 | get rowStyle() {
16 | return `
17 | height: ${this.rowheight}px;
18 | `;
19 | }
20 | }
--------------------------------------------------------------------------------
/src/virtualScroll/virtualScroll.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/virtualScroll/virtualScroll.js:
--------------------------------------------------------------------------------
1 | import { LightningElement, track, api } from 'lwc';
2 |
3 | export default class VirtualScroll extends LightningElement {
4 |
5 | @api viewheight = 0;
6 | @api rowheight = 0;
7 |
8 | @track visibledata = [];
9 | @track offsetY = 0;
10 |
11 | totalHeight=0;
12 | _alldata = [];
13 | alldataLength = 0;
14 |
15 | visibleRowCount = 0;
16 | startRowIndex = 0;
17 |
18 | bufferRowCount = 5;
19 |
20 | connectedCallback() {
21 | // Since getData() is an async method,
22 | // it is difficult to get data at this time.
23 | }
24 |
25 | @api
26 | get data() {
27 | return this._alldata;
28 | }
29 |
30 | set data(value) {
31 | this.init(value);
32 | }
33 |
34 | init(data) {
35 | this._alldata = data.map((row, index) => {
36 | let refObj = Object.assign({}, row);
37 | refObj.index = index;
38 | refObj.checked = false;
39 | return refObj;
40 | });
41 |
42 | this.alldataLength = this._alldata.length;
43 | this.totalHeight = this.alldataLength * this.rowheight;
44 |
45 | this.calcVisibleData(true);
46 | }
47 |
48 | selectHandler(e) {
49 | const dataId = e.detail;
50 | const item = this._alldata.find(data => data.id == dataId);
51 | item.checked = !item.checked;
52 | }
53 |
54 | scrollHandler() {
55 | this.calcVisibleData(false);
56 | }
57 |
58 | calcVisibleData(firstTime) {
59 | if(firstTime === false) {
60 | const scrollTop = this.getScrollTop();
61 | const lastIndex = this.startRowIndex;
62 | this.startRowIndex = Math.floor(scrollTop / this.rowheight) - this.bufferRowCount;
63 | this.startRowIndex = Math.max(0, this.startRowIndex);
64 |
65 | if(lastIndex === this.startRowIndex) return;
66 | }
67 |
68 | this.visibleRowCount = Math.ceil(this.viewheight / this.rowheight) + 2 * this.bufferRowCount;
69 | this.visibleRowCount = Math.min(this.alldataLength - this.startRowIndex, this.visibleRowCount);
70 |
71 | this.offsetY = this.startRowIndex * this.rowheight;
72 | this.visibledata = this.getVisibleData();
73 | }
74 |
75 | getVisibleData() {
76 | let endIndex = this.startRowIndex + this.visibleRowCount;
77 | return this._alldata.slice(this.startRowIndex, endIndex);
78 | }
79 |
80 | getScrollTop() {
81 | const element = this.template.querySelector('.viewport');
82 | return element.scrollTop;
83 | }
84 |
85 | get transformStyle() {
86 | return `
87 | transform: translateY(${this.offsetY}px);
88 | `;
89 | }
90 |
91 | get contentStyle() {
92 | return `
93 | height: ${this.totalHeight}px;
94 | `;
95 | }
96 |
97 | get viewportStyle() {
98 | return `
99 | max-height: ${this.viewheight}px;
100 | overflow: auto;
101 | `;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/virtual-scroll.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skysan87/lwc-virtual-scroll-sample/2c0fe1324e287f46f8619489db29cd985a810a9d/virtual-scroll.gif
--------------------------------------------------------------------------------