+2

Tạo Dropbox với Nextjs 14 Typescript

**Tôi định nghĩa dropbox là 1 cái hộp được thả ra với phương hướng bất kì là cơ sở cho dropdown, autocomplete. phương hướng bất kì là nó tự động dựa vạo vị trí mà nó đang ở để xác định thả box ra ngoài trong phạm vị xem được hợp lý ví dụ như: box nó ở gần cuối trang thì nó phải xuất hiển quay ngược lên trên , nó ở bên phải , thì nó phải lệch box sang trái **

image.png image.png

  1. Component
  • dropbox content thì tạo bằng createPortal
  • keyId thì dùng mặt định useId React 19
  • impact là lớp thao tác vs UI dev tự code, content cũng vậy
  • position là lớp css hiển thị box nó nằm trong logic

L.Master chỉ là cái tag div bình thường thui.

'use client';
import { $Master } from '@/libs';
import { L } from '@/libs/es';
import cx from 'classnames';
import { isEmpty } from 'lodash';
import React, { cloneElement, memo, useId } from 'react';
import { createPortal } from 'react-dom';
import useLogic from './logic';
import styles from './styles.module.scss';

type P = ReturnType<typeof useLogic>;
interface $Props {
    keyId?: string; // không truyền củng được || default dùng useId React 19
    impact?(args: Pick<P, 'show' | 'onToggle'>): JSX.Element;
    contents?(args: Pick<P, 'show' | 'onToggle' | 'dropBoxRect'>): JSX.Element; // dropBoxRect trả về Rect
    clsDropbox?: string; // class css
    clsContent?: string; // class css
    isOutlet?: boolean; // nhấn ra ngoài để tắt
    setting?: $Master; // css
}

function Dropdown(props: $Props) {
    const {
        clsDropbox,
        clsContent,
        impact,
        contents,
        keyId = useId(),
        isOutlet,
        setting,
    } = props;

    const args = useLogic({ keyId });

    const { onToggle, position, show } = args;

    return cloneElement(
        <L.Master
            id={keyId}
            className={cx(styles.Dropbox, clsDropbox)}
        ></L.Master>,
        { ...setting },
        <>
            {impact &&
                cloneElement(impact?.(args), {
                    id: `DropImpact_${keyId}`,
                })}

            {React.useMemo(
                () =>
                    show &&
                    !isEmpty(position) &&
                    createPortal(
                        <L.Master
                            id={`DropContent_${keyId}`}
                            onMouseLeave={isOutlet ? onToggle : undefined}
                            style={{
                                ...position,
                            }}
                            className={cx(styles.DropContent, clsContent)}
                        >
                            {contents?.(args)}
                        </L.Master>,
                        document.body
                    ),
                [show, position, keyId, isOutlet]
            )}
        </>
    );
}

export default memo(Dropdown);

  1. Logic Tạo cái use hook xử lý logic
  • tính nó gần bottom hông , nó gần trái hông
  • dùng transform
'use client';
import { $listenEvent } from '@/libs';
import { debounce } from 'lodash';
import React, { CSSProperties, useEffect, useState } from 'react';

type $Args = {
    keyId: string;
};

export default function useLogic({ keyId }: $Args) {
    const [show, setShow] = useState({
        [keyId]: false,
    });

    const [position, setPosition] = useState({
        [keyId]: {} as CSSProperties,
    });

    const [dropBoxRect, setDropBoxRect] = useState<DOMRect>();

    const onPosition = React.useCallback(
        debounce(() => {
            const DropImpact = document.getElementById(`DropImpact_${keyId}`);
            const DropContent = document.getElementById(`DropContent_${keyId}`);
            const DropImpactRect = DropImpact?.getBoundingClientRect();
            const DropContentRect = DropContent?.getBoundingClientRect();
            const { innerHeight, innerWidth } = window;

            if (DropImpactRect) {
                const isNearBottom = innerHeight / 2 - DropImpactRect.y < 0;
                const isNearLeft = innerWidth / 2 - DropImpactRect.x < -200;

                setPosition((prev) => ({
                    ...prev,
                    [keyId]: {
                        transform: `translate(${
                            isNearLeft
                                ? Number(DropImpactRect?.x) +
                                  Number(DropImpactRect?.width) -
                                  Number(DropContentRect?.width)
                                : Number(DropImpactRect.left)
                        }px, ${
                            isNearBottom
                                ? Number(DropImpactRect.y) -
                                  Number(DropContentRect?.height)
                                : Number(DropImpactRect.bottom)
                        }px)`,
                        top: -1,
                        left: -1,
                    },
                }));
            }
        }, 300),
        []
    );

    useEffect(() => {
        if (!show[keyId]) {
            onPosition();
            $listenEvent.add(onPosition);
        }

        return () => {
            onPosition();
            setPosition({});
            $listenEvent.remove(onPosition);
        };
    }, []);

    const onGetRect = React.useCallback(() => {
        if (!dropBoxRect) {
            const Dropbox = document.getElementById(`${keyId}`);
            const DropboxRect = Dropbox?.getBoundingClientRect();
            setDropBoxRect(DropboxRect);
        }
    }, [dropBoxRect]);

    useEffect(() => {
        if (!dropBoxRect) {
            onGetRect();
            $listenEvent.add(onGetRect);
        }
        return () => {
            onGetRect();
            $listenEvent.remove(onGetRect);
        };
    }, [dropBoxRect]);

    const onToggle = () => {
        setShow((prev) => ({ ...prev, [keyId]: !prev[keyId] }));
    };
    
    const onSetShow = (status: boolean) => {
        setShow((prev) => ({ ...prev, [keyId]: status }));
    };

    return {
        onSetShow, // set status cho key thay cho onOpen, onClose
        onToggle,
        position: position[keyId], // vị trí box được mở
        show: show[keyId], // trạng thái mở box
        dropBoxRect, // rect của dropbox
    };
}

Mình đặt nhân tử chung ra ngoài làm thừa số chung:

export const $listenEvent = {
    add(fnc: () => void) {
        document.addEventListener('wheel', fnc);
        document.addEventListener('resize', fnc);
        document.addEventListener('orientationchange', fnc);
        document.addEventListener('load', fnc);
        document.addEventListener('reload', fnc);
    },
    remove(fnc: () => void) {
        document.removeEventListener('wheel', fnc);
        document.removeEventListener('resize', fnc);
        document.removeEventListener('orientationchange', fnc);
        document.removeEventListener('load', fnc);
        document.removeEventListener('reload', fnc);
    },
};

  1. SCSS Đơn giản vầy thui, muốn thêm hiệu transform , transition, visiable , tự thêm vô
.Dropbox {
    display: flex;
}

.DropContent {
    position: fixed;
    z-index: 999;
    min-height: 250px;
    display: flex;
    content: '';
    padding: 0.5rem 0;
    flex: 1;
}

  1. Cách dùng :
<L.Dropbox
                    {...{
                        setting: {
                            width: '100%',
                        },
                        clsDropbox: styles.clsDropbox,
                        impact(args) {
                            return (
                                <L.Magic
                                    hover
                                    width={'fit-content'}
                                    shadow
                                    padding={'1rem'}
                                    onClick={args.onToggle}
                                    className={styles.impact}
                                >
                                    Toggle dropdown
                                </L.Magic>
                            );
                        },
                        contents({ dropBoxRect }) {
                            return (
                                <L.Section
                                    background={'white'}
                                    shadow
                                    minWidth={dropBoxRect?.width}
                                    minHeight={'100%'}
                                    padding={'1rem'}
                                >
                                    <L.Txt>
                                        contents contents contents contents
                                        contents contents
                                    </L.Txt>
                                </L.Section>
                            );
                        },
                    }}
                />

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí