Пишем стиль:
Пишем наш JS:
Смотрим наш результат:
SCSS:
$color-blue: #3a77ff;
$color-dark-blue: #28375a;
$row-height: 40px;
$table-gap: 1px;
html, body, .app {
width: 100%;
height: 100%;
font-family: 'Montserrat', sans-serif;
}
.app {
display: flex;
align-items: center;
justify-content: center;
background-color: rgba($color-blue, 0.2);
}
.table {
background-color: rgba($color-blue, 0.2);
border: 1px solid rgba($color-blue, 0.5);
max-height: 80%;
border-radius: 4px;
box-shadow: 0 0 5px rgba($color-blue, 0.4), 0 0 25px rgba($color-blue, 0.2), 0 0 200px 150px white;
display: flex;
flex-direction: column;
overflow: hidden;
.header {
display: grid;
grid-template-columns: 70px repeat(5, 90px);
grid-gap: $table-gap;
border-bottom: 1px solid rgba($color-blue, 0.2);
box-shadow: 0 0 2px 2px rgba($color-blue, 0.15), 0 0 15px 5px rgba($color-blue, 0.15);
z-index: 1;
font-weight: bold;
.table-cell {
background-color: mix($color-blue, white, 20%);
}
}
.table-inner {
display: grid;
grid-template-columns: 70px repeat(5, 90px);
grid-gap: $table-gap;
height: 100%;
overflow: auto;
}
.table-cell {
padding: 0 15px;
background-color: white;
display: flex;
align-items: center;
justify-content: center;
height: $row-height;
color: $color-dark-blue;
&.column-0 {
padding: 0 10px;
}
&.optimistic {
position: relative;
background-image: linear-gradient(rgba(58, 119, 255, 0.2) 1px, transparent 1px);
background-size: $row-height+$table-gap $row-height+$table-gap;
background-repeat: repeat;
margin-top: -1px; // This fixes the double gab below the visible rows that occurs as a result of the gradient meeting the grid gap.
&::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
// background-size: 80px 80px;
animation: blink 2s infinite;
background-repeat: repeat;
background-image: linear-gradient(to right, transparent 0, transparent 10%, rgba(58, 119, 255, 0.1) 10%, rgba(58, 119, 255, 0.1) 90%, transparent 90%);
}
}
}
}
@keyframes blink {
0%, 100% {
opacity: 0;
}
50% {
opacity: 1;
}
}
JavaScript:
const ROW_HEIGHT = 40;
const GAP_SIZE = 1;
const ROWS = 10000;
const COLS = 6;
const THRESHOLD = 300; // Represents the delay (in milliseconds) between data updates
const DATA_PADDING = 3; // Represents how much extra data should be rendered before
// and after the visibile rows to avoid showing empty rows
// when scrolling. This number is multiplied by the number
// of visible rows.
const OptimisticRow = ({rows}) => (
<React.Fragment>
{[...new Array(COLS)].map((_, i) => (
<div key={i} className='table-cell optimistic' style={{height: rows *(ROW_HEIGHT + GAP_SIZE)}}/>
))}
</React.Fragment>
);
const TableCell = ({children, index}) => (
<div
className={`table-cell column-${index}`}>
{children}
</div>
);
const TableRow = ({children}) => (
children
);
class Table extends React.PureComponent {
table = React.createRef();
state = {from: 0, to: 30};
previousScrolTop = 0;
getSnapshotBeforeUpdate() {
return this.table.current.scrollTop;
}
componentDidUpdate(prevProps, prevState, scrollTop) {
// When the visible rows are moved down by changing
// the height of the optimistic row above them, the browser automatically
// scrolls them back into view, which in turn creates another render
// becuase the onScroll is called, resulting in an infinite loop.
// To solve this, we get the snapshot before the DOM is updated
// and check for a mismatch between the scrollTop before and after.
// If such a mismatch exists, it means that the scroll
// was done by the browser, and not the user, and therefore
// we apply the scrollTop from the snapshot.
if (scrollTop !== this.table.current.scrollTop) {
this.table.current.scrollTop = scrollTop;
}
}
debounce = _.debounce((scrollTop, clientHeight) => {
const maxVisibleRows = Math.ceil(clientHeight / (ROW_HEIGHT + GAP_SIZE));
const from = Math.max(0, Math.floor(scrollTop / (ROW_HEIGHT + GAP_SIZE)) - maxVisibleRows * DATA_PADDING);
const to = Math.min(this.props.rows, from + maxVisibleRows * (DATA_PADDING * 2 + 1));
this.setState({from, to});
}, THRESHOLD);
handleOnScroll = e => {
const {scrollTop, clientHeight} = e.target;
this.debounce(scrollTop, clientHeight);
};
render() {
const {children, rows} = this.props;
const {from, to} = this.state;
return (
<div className='table'>
<div className='header'>
<TableRow>
<TableCell index={0}>Index</TableCell>
{[...new Array(COLS - 1)].map((_, i) => (
<TableCell index={i + 1} header>Header</TableCell>
))}
</TableRow>
</div>
<div className='table-inner' onScroll={this.handleOnScroll} ref={this.table}>
{from > 0 &&
<OptimisticRow rows={from}/>}
{children(from, to)}
{to < rows &&
<OptimisticRow rows={rows - to}/>}
</div>
</div>
);
}
}
class App extends React.PureComponent {
render() {
return (
<div className='app'>
<Table rows={ROWS}>
{(from, to) => (
[...new Array(to - from)].map((_, i) => (
<TableRow key={i} index={i}>
<TableCell index={0}>{i + from}</TableCell>
{[...new Array(COLS - 1)].map((_, i) => (
<TableCell index={i + 1} key={i}>data</TableCell>
))}
</TableRow>
))
)}
</Table>
</div>
);
}
}
ReactDOM.render(
<App/>,
document.body
);