0
0
Lập trình
Thaycacac
Thaycacac thaycacac

Tạo danh sách Masonry bằng Flexbox trong Next.js và React.js với SCSS Module

Đăng vào 1 tháng trước

• 10 phút đọc

Giới thiệu

Trong bài viết này, chúng ta sẽ cùng tìm hiểu cách xây dựng một danh sách Masonry sử dụng Flexbox thông qua Next.js và React.js, đồng thời áp dụng SCSS Module để quản lý kiểu dáng hiệu quả. Đây là một giải pháp thích hợp cho việc hiển thị một lượng lớn nội dung một cách linh hoạt và đẹp mắt.

Giải pháp xây dựng danh sách

  1. Chia mảng thành mảng đa chiều:
    Chúng ta sẽ chia mảng dữ liệu thành các mảng con, tương ứng với số cột mà bạn mong muốn.

  2. Sử dụng Flexbox:
    Chúng ta sử dụng Flexbox cho layout của danh sách, kết hợp với thuộc tính gap để tạo khoảng cách giữa các item. Kích thước width của mỗi item sẽ được tính toán theo công thức sau:

    scss Copy
    calc(100% / $column - $gap * (($column - 1) / $column))
  3. Demo code:
    Bạn có thể tham khảo đoạn mã mẫu tại đây.

Lưu ý khi sử dụng thư viện

Khi sử dụng thư viện để xây dựng danh sách Masonry, bạn có thể gặp phải hạn chế về chiều cao của các phần tử. Ví dụ, với số lượng lớn items như 9999, một số thư viện có thể gặp khó khăn trong việc load tất cả.

Sử dụng SCSS để tối ưu

Dưới đây là đoạn mã SCSS thiết lập kiểu dáng cho danh sách:

scss Copy
.isScrolling {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    overflow: auto;
}

Hướng dẫn sử dụng mã nguồn

Dưới đây là mã nguồn chính để tạo danh sách Masonry:

javascript Copy
'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 }) => {
    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 }) => {
    return (
        <div className={styles.row}> 
            <Suspense fallback={<p>Loading...</p>}>
                {useMemo(() => <RenderItem item={row} />, [row])}
            </Suspense>
        </div>
    );
});

const RenderItem = memo(({ item }) => {
    return (
        <div className={styles.card}>
            <picture>
                <img src={item.image} alt="" />
            </picture>
            <p>label: {item.label}</p>
        </div>
    );
});

SCSS cho giao diện

Dưới đây là mã SCSS cho giao diện của danh sách Masonry:

scss Copy
.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;
                            }
                        }
                    }
                }
            }
        }
    }
}

Kết luận

Với hướng dẫn này, bạn đã có thể tạo ra một danh sách Masonry đa cột, dễ dàng quản lý và đẹp mắt sử dụng Flexbox và SCSS. Hãy thử áp dụng vào dự án của bạn và theo dõi kết quả nhé!
source: viblo

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào