Giải pháp:
- Chia mảng đang có thành mảng đa chiều tương ứng với số cột
- Sử dụng flexbox, dùng thuộc tính gap , và chia kích thước width theo item công thức như sau:
calc(100% / $column - $gap * (($column - 1) / $column))
- demo code: https://abi.sshop.live/masonry
Dùng thư viện bạn sẽ bị hạn chế về chiều cao của phần tử. ví dụ như masonry hình ảnh, .... Tui đã thử với số lượng lớn items : 9999 thì thư viện cũng không load nỗi
dùng SCSS để giải quyết: nhiêu code đây là đủ
.isScrolling { position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow: auto;
}
Cách dùng:
'use client';
import { filter, map, slice } from 'lodash';
import { CSSProperties, Fragment, Suspense, memo, useEffect, useMemo, useState,
} from 'react';
import styles from './styles.module.scss'; export default memo(function Test() { const [limit, setLimit] = useState(50); const column = 5; const list = map(Array.from({ length: 9999 }).fill(0), (_, idx) => { return { label: idx, image: 'https://source.unsplash.com/random?t=' + idx, }; }); const columnItems = Array.from({ length: column }, (_, colIndex) => filter(slice(list, 0, limit), (_, index) => index % column === colIndex) ); const handleScroll = () => { const boxList = document.getElementById('cover'); const listItem = document.getElementById('list'); if (boxList && listItem) { if ( boxList.scrollTop + boxList.clientHeight >= listItem.offsetHeight ) { setLimit((prev) => prev + 150); } } }; useEffect(() => { window.addEventListener('wheel', handleScroll); return () => { window.removeEventListener('wheel', handleScroll); }; }, [handleScroll]); return ( <div className={styles.Container}> <div className={styles.block}> <div className={styles.isScrolling} id="cover"> <ul id="list" className={styles.list} style={{ '--column': column } as CSSProperties} > {map(columnItems, (column, columnID) => { return ( <Fragment key={columnID}> <Suspense fallback={<p>Loading...</p>}> <RenderColumn column={column} /> </Suspense> </Fragment> ); })} </ul> </div> </div> </div> );
}); const RenderColumn = memo(({ column }: { column: any[] }) => { return ( <li className={styles.column}> {map(column, (row, rowID) => { return ( <Fragment key={rowID}> <Suspense fallback={<p>Loading...</p>}> <RenderRow row={row} /> </Suspense> </Fragment> ); })} </li> );
}); const RenderRow = memo(({ row }: any) => { return ( <div className={styles.row}> <Suspense fallback={<p>Loading...</p>}> {useMemo( () => ( <RenderItem item={row} /> ), [row] )} </Suspense> </div> );
}); const RenderItem = memo(({ item }: any) => { return ( <div className={styles.card}> <picture> <img src={item.image} alt="" /> </picture> <p>label: {item.label}</p> </div> );
});
SCSS cho nó:
.Container { padding: 1rem; display: flex; flex: 1; .block { position: relative; display: flex; flex: 1; margin: -1rem; $gap: 1rem; .list { $column: var(--column); padding: 1rem; width: 100%; list-style: none; display: flex; gap: $gap; .column { width: calc(100% / $column - $gap * (($column - 1) / $column)); display: flex; flex-direction: column; gap: $gap; .row { .card { padding: 1rem; box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.1); border-radius: 0.5rem; picture { img { width: 100%; border-radius: 0.5rem; object-fit: cover; object-position: center; } } } } } } }
}