├── 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 | ![Virtual Scroll in Lightning Web Component](virtual-scroll.gif) 5 | 6 | ## Reference 7 | https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib -------------------------------------------------------------------------------- /src/app/app.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 --------------------------------------------------------------------------------