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 **
- 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);
- 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);
},
};
- 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;
}
- 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