همانطور که کنترلهای React UI پیچیدهتر میشوند، منطق پیچیده میتواند با UI در هم آمیخته شود. این امر استدلال در مورد رفتار کامپوننت را دشوار می کند، آزمایش آن را دشوار می کند و ساخت کامپوننت های مشابهی را که نیاز به ظاهر متفاوتی دارند ضروری می کند. یک کامپوننت Headless ، تمام منطق غیر UI و مدیریت state را استخراج می کند و مغز یا منطق یک کامپوننت را از ظاهر آن جدا می کند.
React طرز تفکر ما در مورد کامپوننت های UI و مدیریت state در UI را متحول کرده است. اما با هر درخواست یا بهبود ویژگی جدید، یک کامپوننت به ظاهر ساده میتواند به سرعت به ادغام پیچیدهای از state درهم تنیده و منطق UI تبدیل شود.
تصور کنید که یک لیست Dropdown ساده بسازید. در ابتدا، ساده به نظر می رسد - شما state باز/بسته را مدیریت می کنید و ظاهر آن را طراحی می کنید. اما، همانطور که برنامه شما رشد می کند و تکامل می یابد، شرایط لازم برای این لیست Dropdown نیز انجام می شود:
پشتیبانی دسترسپذیری: اطمینان از اینکه لیست Dropdown شما برای همه قابل استفاده است، از جمله کسانی که از صفحهخوانها یا سایر فناوریهای کمکی استفاده میکنند، لایه دیگری از پیچیدگی را اضافه میکند. شما باید state های تمرکز، ویژگی های aria
را مدیریت کنید و مطمئن شوید که Dropdown شما از نظر معنایی درست است.
پیمایش کیبورد: کاربران نباید به تعاملات ماوس محدود شوند. آنها ممکن است بخواهند با استفاده از کلیدهای جهتدار گزینهها را پیمایش کنند، با استفاده از Enter انتخاب کنند، یا با استفاده از Escape، منوی Dropdown را ببندند. این امر مستلزم event listeners اضافی و مدیریت state است.
ملاحظات دادههای Async: همانطور که برنامه شما مقیاس میشود، شاید گزینههای Dropdown دیگر کدگذاری نشده باشند. ممکن است از یک API فتچ شوند. این نیاز به مدیریت بارگیری، خطا و state های خالی در Dropdown را معرفی می کند.
تغییرات رابط کاربری و تم: بخشهای مختلف برنامه شما ممکن است به استایل ها یا تمهای متفاوتی برای Dropdown نیاز داشته باشند. مدیریت این تغییرات در کامپوننت می تواند منجر به انفجاری از نیاز ها و تنظیمات شود.
گسترش ویژگی ها: با گذشت زمان، ممکن است به ویژگی های اضافی مانند انتخاب چندگانه، گزینه های فیلتر یا ادغام با سایر کنترل های فرم نیاز داشته باشید. افزودن اینها به یک کامپوننت از قبل پیچیده می تواند دلهره آور باشد.
هر یک از این ملاحظات لایه هایی از پیچیدگی را به کامپوننت Dropdown ما اضافه می کند. ترکیب state، منطق و ارائه رابط کاربری باعث میشود که قابلیت نگهداری کمتری داشته باشد و قابلیت استفاده مجدد آن را محدود کند. هر چه آنها بیشتر در هم تنیده شوند، ایجاد تغییرات بدون مشکل ناخواسته دشوارتر می شود.
معرفی الگوی کامپوننت Headless
در مواجهه با این چالشها، الگوی Headless Component راهی برای خروج ارائه میکند. این بر جداسازی محاسبات از نمایش UI تاکید می کند و به توسعه دهندگان این قدرت را می دهد که کامپوننت های همه کاره، قابل نگهداری و قابل استفاده مجدد را بسازند.
یک کامپوننت Headless یک الگوی طراحی در React است که در آن یک کامپوننت، که معمولاً به عنوان هوک React وارد میشود، تنها مسئولیت مدیریت منطق و state را بدون نیاز به هیچ UI (رابط کاربری) خاصی بر عهده دارد. "مغز" عملیات را فراهم می کند اما "نگاه looks" را به توسعه دهنده ای که آن را اجرا می کند واگذار می کند. در اصل، بدون تحمیل یک UI خاص، عملکردی را ارائه می دهد.
هنگام تجسم، کامپوننت Headless به عنوان یک لایه باریک ظاهر می شود که از یک طرف با نماهای JSX ارتباط برقرار می کند و در صورت لزوم با مدل های داده زیرین ارتباط برقرار می کند. این الگو به ویژه برای افرادی که صرفاً به دنبال جنبههای رفتاری یا مدیریت state رابط کاربری هستند، مفید است، زیرا به راحتی این موارد را از UI جدا میکند.
به عنوان مثال، یک کامپوننت دراپ داون Headless در نظر بگیرید. مدیریت state برای stateهای باز/بسته، انتخاب آیتم، پیمایش صفحهکلید، و غیره را مدیریت میکند. وقتی زمان رندر فرا میرسد، به جای رندر کردن رابط کاربری Dropdown رمزگذاریشده خود، این state و منطق را برای یک تابع یا کامپوننت فرزند فراهم میکند و به توسعهدهنده اجازه میدهد. تصمیم بگیرید که چگونه از نظر UI ظاهر شود.
در این مقاله، با ساختن یک کامپوننت پیچیده، یک لیست Dropdown از ابتدا، به یک مثال عملی خواهیم پرداخت. همانطور که ویژگی های بیشتری را به کامپوننت اضافه می کنیم، چالش های پیش آمده را مشاهده خواهیم کرد. از این طریق، نشان خواهیم داد که چگونه الگوی کامپوننت Headless میتواند به این چالشها رسیدگی کند، نگرانیهای متمایز را تقسیم بندی کند، و به ما در ساخت کامپوننت های همهکارهتر کمک کند.
پیاده سازی لیست Dropdown
لیست Dropdown کامپوننت رایجی است که در بسیاری از مکان ها استفاده می شود. اگرچه یک کامپوننت انتخاب بومی برای موارد استفاده اولیه وجود دارد، نسخه پیشرفته تر که کنترل بیشتری بر روی هر گزینه ارائه می دهد، تجربه کاربری بهتری را ارائه می دهد.
ایجاد یکی از ابتدا، یک پیاده سازی کامل، به تلاش بیشتری نسبت به آنچه در نگاه اول به نظر می رسد نیاز دارد. در نظر گرفتن ناوبری صفحه کلید، دسترسی (به عنوان مثال، سازگاری با صفحهخوان)، و قابلیت استفاده در دستگاههای تلفن همراه، از جمله موارد دیگر ضروری است.
ما با یک نسخه ساده و دسکتاپ شروع می کنیم که فقط از کلیک ماوس پشتیبانی می کند و به تدریج ویژگی های بیشتری برای واقعی سازی آن ایجاد می کنیم. توجه داشته باشید که هدف در اینجا نشان دادن چند الگوی طراحی نرم افزار به جای آموزش نحوه ایجاد یک لیست Dropdown برای استفاده در تولید است، در واقع، من انجام این کار را از ابتدا توصیه نمی کنم و در عوض استفاده از کتابخانه های بالغ تر را پیشنهاد می کنم.
اساسا، ما به یک عنصر (که آن را trigger
بنامیم) برای کلیک کاربر، و یک state برای کنترل نمایش و پنهان کردن اکشن های یک پنل لیست نیاز داریم. در ابتدا پنل را مخفی می کنیم و با کلیک روی تریگر، پنل لیست را نشان می دهیم.
import { useState } from "react";
interface Item {
icon: string;
text: string;
description: string;
}
type DropdownProps = {
items: Item[];
};
const Dropdown = ({ items }: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
return (
<div className="dropdown">
<div className="trigger" tabIndex={0} onClick={() => setIsOpen(!isOpen)}>
<span className="selection">
{selectedItem ? selectedItem.text : "Select an item..."}
</span>
</div>
{isOpen && (
<div className="dropdown-menu">
{items.map((item, index) => (
<div
key={index}
onClick={() => setSelectedItem(item)}
className="item-container"
>
<img src={item.icon} alt={item.text} />
<div className="details">
<div>{item.text}</div>
<small>{item.description}</small>
</div>
</div>
))}
</div>
)}
</div>
);
};
در کد بالا، ساختار اصلی را برای کامپوننت Dropdown خود تنظیم کرده ایم. با استفاده از هوک useState
، وضعیت های isOpen
و SelectItem
را برای کنترل رفتار Dropdown مدیریت می کنیم. یک کلیک ساده بر روی trigger
، منوی Dropdown را تغییر می دهد، در حالی که انتخاب یک مورد، وضعیت SelectItem
را به روز می کند.
بیایید کامپوننت را به قطعات کوچکتر و قابل مدیریت تقسیم کنیم تا آن را واضح تر ببینیم. این تجزیه بخشی از الگوی Headless Component نیست، اما شکستن یک کامپوننت پیچیده UI به قطعات یک فعالیت ارزشمند است.(همان طور که در مقاله جهنم آموزشی در مورد آن صحبت کردیم.)
میتوانیم با استخراج یک کامپوننت Trigger برای مدیریت کلیکهای کاربر شروع کنیم:
const Trigger = ({
label,
onClick,
}: {
label: string;
onClick: () => void;
}) => {
return (
<div className="trigger" tabIndex={0} onClick={onClick}>
<span className="selection">{label}</span>
</div>
);
};
کامپوننت Trigger یک عنصر رابط کاربری قابل کلیک اولیه است که یک label
برای نمایش و یک کنترل کننده onClick
می گیرد. نسبت به اطراف خود آگنوستیک باقی می ماند. به طور مشابه، میتوانیم یک کامپوننت DropdownMenu
را برای ارائه لیست موارد استخراج کنیم:
const DropdownMenu = ({
items,
onItemClick,
}: {
items: Item[];
onItemClick: (item: Item) => void;
}) => {
return (
<div className="dropdown-menu">
{items.map((item, index) => (
<div
key={index}
onClick={() => onItemClick(item)}
className="item-container"
>
<img src={item.icon} alt={item.text} />
<div className="details">
<div>{item.text}</div>
<small>{item.description}</small>
</div>
</div>
))}
</div>
);
};
کامپوننت DropdownMenu
لیستی از آیتم ها را نمایش می دهد که هر کدام دارای یک icon
و یک description
هستند. هنگامی که روی یک مورد کلیک می شود، تابع onItemClick
ارائه شده را با آیتم انتخاب شده به عنوان آرگومان آن فعال می کند.
و سپس در کامپوننت Dropdown، Trigger و DropdownMenu
را ترکیب کرده و وضعیت لازم را به آنها ارائه می کنیم. این رویکرد تضمین میکند که کامپوننتهای Trigger و DropdownMenu در state آگنوستیک باقی میمانند و صرفاً به موارد ارسال شده واکنش نشان میدهند.
const Dropdown = ({ items }: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
return (
<div className="dropdown">
<Trigger
label={selectedItem ? selectedItem.text : "Select an item..."}
onClick={() => setIsOpen(!isOpen)}
/>
{isOpen && <DropdownMenu items={items} onItemClick={setSelectedItem} />}
</div>
);
};
در این ساختار کد به روز شده، نگرانیها را با ایجاد کامپوننتهای تخصصی برای بخشهای مختلف Dropdown جدا کردهایم، که کد را سازماندهیتر و مدیریت آسانتر میکند.
همانطور که در تصویر بالا نشان داده شده است، میتوانید روی trigger
«انتخاب یک مورد...» کلیک کنید تا منوی Dropdown باز شود. انتخاب یک مقدار از لیست، مقدار نمایش داده شده را به روز می کند و سپس منوی Dropdown را می بندد.
در این مرحله، کد بازسازی شده ما واضح است و هر بخش ساده و قابل انطباق است. اصلاح یا معرفی یک کامپوننت Trigger متفاوت نسبتاً ساده است. با این حال، با معرفی ویژگیهای بیشتر و مدیریت وضعیتهای اضافی، آیا کامپوننت های فعلی ما باقی میمانند؟
بیایید با یک پیشرفت حیاتی برای یک لیست کاهشی جدی دریابیم: ناوبری صفحه کلید.
پیاده سازی پیمایش کیبورد
گنجاندن پیمایش صفحه کلید در لیست Dropdown ما با ارائه جایگزینی برای تعاملات ماوس، تجربه کاربر را افزایش می دهد. این به ویژه برای دسترسی مهم است و یک تجربه ناوبری یکپارچه را در صفحه وب ارائه می دهد. بیایید بررسی کنیم که چگونه می توانیم با استفاده از کنترل کننده رویداد onKeyDown
به این مهم دست پیدا کنیم.
در ابتدا، یک تابع handleKeyDown
را به رویداد onKeyDown
در کامپوننت Dropdown خود متصل می کنیم. در اینجا، ما از یک دستور switch
برای تعیین کلید خاصی که فشار داده شده است استفاده می کنیم و اقدامات را مطابق با آن انجام می دهیم. به عنوان مثال، هنگامی که کلید "Enter" یا "Space" فشار داده می شود، Dropdown تغییر می کند.
به طور مشابه، کلیدهای «ArrowDown» و «ArrowUp» امکان پیمایش در میان آیتمهای فهرست را فراهم میکنند و در صورت لزوم به شروع یا پایان فهرست برمیگردند.
const Dropdown = ({ items }: DropdownProps) => {
// ... previous state variables ...
const [selectedIndex, setSelectedIndex] = useState<number>(-1);
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
// ... case blocks ...
// ... handling Enter, Space, ArrowDown and ArrowUp ...
}
};
return (
<div className="dropdown" onKeyDown={handleKeyDown}>
{/* ... rest of the JSX ... */}
</div>
);
};
بعلاوه، ما کامپوننت DropdownMenu خود را به روز کرده ایم تا یک صفحه انتخاب شده Index
را بپذیریم. این پایه برای اعمال یک استایل CSS و تنظیم ویژگی انتخاب شده توسط aria
بر روی آیتم انتخاب شده فعلی استفاده می شود و بازخورد UX و دسترسی را افزایش می دهد.
const DropdownMenu = ({
items,
selectedIndex,
onItemClick,
}: {
items: Item[];
selectedIndex: number;
onItemClick: (item: Item) => void;
}) => {
return (
<div className="dropdown-menu" role="listbox">
{/* ... rest of the JSX ... */}
</div>
);
};
اکنون، کامپوننت «Dropdown» ما با کد مدیریت state و منطق رندر درگیر شده است. این یک سوئیچ گسترده به همراه تمام ساختارهای مدیریت state مانند «selectedItem
«selectedIndex
»، «setSelectedItem
و غیره را در خود جای داده است.
تایپ اسکریپت در مقابل جاوااسکریپت کدام یک را انتخاب کنیم
پیاده سازی کامپوننت Headless با هوک سفارشی
برای پرداختن به این موضوع، مفهوم Headless Component را از طریق یک هوک سفارشی به نام useDropdown
معرفی می کنیم. این هوک به طور موثر منطق مدیریت رویدادهای state و صفحه کلید را جمع می کند و یک شی پر از state ها و توابع ضروری را برمی گرداند. با از بین بردن ساختار آن در کامپوننت Dropdown خود، کد خود را مرتب و پایدار نگه می داریم.
سحر و جادو در استفاده از هوک Dropdown ای نهفته است، قهرمان اصلی ما - کامپوننت Headless می باشد. این واحد همه کاره همه چیزهایی را که یک فهرست Dropdown به آن نیاز دارد را در خود جای می دهد: اعم از باز بودن، مورد انتخاب شده، مورد هاور شده، واکنش به کلید Enter و غیره. زیبایی سازگاری آن است. می توانید آن را با ارائه های UI مختلف جفت کنید، عناصر JSX خود.
const useDropdown = (items: Item[]) => {
// ... state variables ...
// helper function can return some aria attribute for UI
const getAriaAttributes = () => ({
role: "combobox",
"aria-expanded": isOpen,
"aria-activedescendant": selectedItem ? selectedItem.text : undefined,
});
const handleKeyDown = (e: React.KeyboardEvent) => {
// ... switch statement ...
};
const toggleDropdown = () => setIsOpen((isOpen) => !isOpen);
return {
isOpen,
toggleDropdown,
handleKeyDown,
selectedItem,
setSelectedItem,
selectedIndex,
};
};
اکنون، کامپوننت Dropdown ما سادهتر، کوتاهتر و قابل درکتر است. این هوک useDropdown
را برای مدیریت state خود و مدیریت تعاملات صفحه کلید، نشان می دهد که جدایی واضح از نگرانی ها را نشان می دهد و درک و مدیریت کد را آسان تر می کند.
const Dropdown = ({ items }: DropdownProps) => {
const {
isOpen,
selectedItem,
selectedIndex,
toggleDropdown,
handleKeyDown,
setSelectedItem,
} = useDropdown(items);
return (
<div className="dropdown" onKeyDown={handleKeyDown}>
<Trigger
onClick={toggleDropdown}
label={selectedItem ? selectedItem.text : "Select an item..."}
/>
{isOpen && (
<DropdownMenu
items={items}
onItemClick={setSelectedItem}
selectedIndex={selectedIndex}
/>
)}
</div>
);
};
از طریق این تغییرات، ما پیمایش صفحه کلید را با موفقیت در لیست Dropdown خود پیاده سازی کرده ایم و آن را در دسترس تر و کاربرپسندتر کرده ایم. این مثال همچنین نشان میدهد که چگونه میتوان از هوکها برای مدیریت state و منطق پیچیده به شیوهای ساختاریافته و مدولار استفاده کرد و راه را برای پیشرفتها و افزودن ویژگیهای بیشتر به کامپوننت های رابط کاربری ما هموار کرد.
زیبایی این طراحی در جدایی متمایز منطق از ارائه نهفته است. با "منطق"، ما به عملکردهای اصلی یک کامپوننت انتخابی اشاره می کنیم: state باز/بستن، آیتم انتخاب شده، عنصر برجسته شده، و واکنش به ورودی های کاربر مانند فشار دادن ArrowDown هنگام انتخاب از لیست. این تقسیمبندی تضمین میکند که کامپوننت ما رفتار اصلی خود را بدون محدود شدن به یک نمایش بصری خاص حفظ میکند، و اصطلاح «کامپوننت Headless » را توجیه میکند.
تست کامپوننت Headless
منطق کامپوننت ما متمرکز است و امکان استفاده مجدد از آن را در سناریوهای مختلف فراهم می کند. برای قابل اعتماد بودن این عملکرد بسیار مهم است. بنابراین، تست جامع ضروری می شود. خبر خوب این است که آزمایش چنین رفتاری ساده است.
ما می توانیم مدیریت state را با استناد به یک روش عمومی و مشاهده تغییر state مربوطه ارزیابی کنیم. برای مثال، میتوانیم رابطه بین toggleDropdown
و استیت isOpen
را بررسی کنیم.
const items = [{ text: "Apple" }, { text: "Orange" }, { text: "Banana" }];
it("should handle dropdown open/close state", () => {
const { result } = renderHook(() => useDropdown(items));
expect(result.current.isOpen).toBe(false);
act(() => {
result.current.toggleDropdown();
});
expect(result.current.isOpen).toBe(true);
act(() => {
result.current.toggleDropdown();
});
expect(result.current.isOpen).toBe(false);
});
تستهای ناوبری صفحهکلید کمی پیچیدهتر هستند، در درجه اول به دلیل عدم وجود رابط بصری. این امر مستلزم یک رویکرد تست یکپارچه تر است. یکی از روشهای موثر، ساختن یک کامپوننت تست جعلی برای احراز هویت رفتار است. چنین تست هایی دو هدف را دنبال می کنند: آنها یک راهنمای آموزشی در مورد استفاده از کامپوننت Headless ارائه می دهند و از آنجایی که از JSX استفاده می کنند، بینشی واقعی از تعاملات کاربر ارائه می دهند.
تست زیر را در نظر بگیرید که بررسی وضعیت قبلی را با یک تست ادغام جایگزین می کند:
it("trigger to toggle", async () => {
render(<SimpleDropdown />);
const trigger = screen.getByRole("button");
expect(trigger).toBeInTheDocument();
await userEvent.click(trigger);
const list = screen.getByRole("listbox");
expect(list).toBeInTheDocument();
await userEvent.click(trigger);
expect(list).not.toBeInTheDocument();
});
SimpleDropdown زیر یک کامپوننت جعلی است که منحصراً برای آزمایش طراحی شده است. همچنین به عنوان یک مثال عملی برای کاربرانی که قصد پیاده سازی Headless Component را دارند دو برابر می شود.
const SimpleDropdown = () => {
const {
isOpen,
toggleDropdown,
selectedIndex,
selectedItem,
updateSelectedItem,
getAriaAttributes,
dropdownRef,
} = useDropdown(items);
return (
<div
tabIndex={0}
ref={dropdownRef}
{...getAriaAttributes()}
>
<button onClick={toggleDropdown}>Select</button>
<p data-testid="selected-item">{selectedItem?.text}</p>
{isOpen && (
<ul role="listbox">
{items.map((item, index) => (
<li
key={index}
role="option"
aria-selected={index === selectedIndex}
onClick={() => updateSelectedItem(item)}
>
{item.text}
</li>
))}
</ul>
)}
</div>
);
};
SimpleDropdown یک کامپوننت ساختگی است که برای آزمایش ساخته شده است. از منطق متمرکز useDropdown
برای ایجاد یک لیست Dropdown استفاده می کند. هنگامی که دکمه "انتخاب" کلیک می شود، لیست ظاهر می شود یا ناپدید می شود. این لیست شامل مجموعه ای از آیتم ها (سیب، نارنجی، موز) است و کاربران می توانند با کلیک بر روی آن، هر موردی را انتخاب کنند. تستهای بالا تضمین میکنند که این رفتار همانطور که در نظر گرفته شده عمل میکند.
با وجود کامپوننت SimpleDropdown
، ما برای آزمایش سناریوی پیچیدهتر و در عین حال واقعیتر مجهز شدهایم.
it("select item using keyboard navigation", async () => {
render(<SimpleDropdown />);
const trigger = screen.getByRole("button");
expect(trigger).toBeInTheDocument();
await userEvent.click(trigger);
const dropdown = screen.getByRole("combobox");
dropdown.focus();
await userEvent.type(dropdown, "{arrowdown}");
await userEvent.type(dropdown, "{enter}");
await expect(screen.getByTestId("selected-item")).toHaveTextContent(
items[0].text
);
});
این تست تضمین میکند که کاربران میتوانند با استفاده از ورودیهای صفحهکلید، موارد را از فهرست Dropdown انتخاب کنند. پس از رندر SimpleDropdown و کلیک بر روی دکمه ماشه آن، Dropdown فوکوس می شود. متعاقباً، آزمون یک فشردن فلش رو به پایین صفحه کلید برای پیمایش به اولین مورد و یک فشار enter برای انتخاب آن شبیه سازی می کند. سپس آزمایش بررسی می کند که آیا مورد انتخاب شده متن مورد انتظار را نمایش می دهد یا خیر.
در حالی که استفاده از هوک های سفارشی برای کامپوننت هایHeadless معمول است، این تنها روش نیست. در واقع، قبل از ظهور هوک، توسعهدهندگان از ابزارهای رندر یا کامپوننتهای درجه بالاتر برای پیادهسازی کامپوننتهای Headless استفاده میکردند. امروزه، حتی اگر کامپوننتهای مرتبه بالاتر برخی از محبوبیت قبلی خود را از دست دادهاند، یک API اعلامی که از زمینه React استفاده میکند همچنان نسبتاً مورد علاقه است.
کامپوننت Headless Declarative با Context API
من یک روش اعلامی جایگزین را برای دستیابی به یک نتیجه مشابه، با استفاده از React context API در این نمونه به نمایش خواهم گذاشت. با ایجاد یک سلسله مراتب در درخت کامپوننت و جایگزین کردن هر کامپوننت، میتوانیم رابط ارزشمندی را به کاربران ارائه دهیم که نه تنها به طور مؤثر عمل میکند (پشتیبانی از ناوبری صفحه کلید، دسترسی، و غیره)، بلکه انعطافپذیری برای سفارشی کردن کامپوننتهای خود را نیز فراهم میکند.
import { HeadlessDropdown as Dropdown } from "./HeadlessDropdown";
const HeadlessDropdownUsage = ({ items }: { items: Item[] }) => {
return (
<Dropdown items={items}>
<Dropdown.Trigger as={Trigger}>Select an option</Dropdown.Trigger>
<Dropdown.List as={CustomList}>
{items.map((item, index) => (
<Dropdown.Option
index={index}
key={index}
item={item}
as={CustomListItem}
/>
))}
</Dropdown.List>
</Dropdown>
);
};
کامپوننت Headless DropdownUsage یک آیتم از نوع آرایه آیتم را می گیرد و یک کامپوننت Dropdown را برمی گرداند. در داخل Dropdown، یک Dropdown.Trigger
برای رندر کامپوننت CustomTrigger
، یک Dropdown.List برای رندر کامپوننت CustomList، و از طریق آرایه آیتم ها برای ایجاد یک Dropdown.Option برای هر آیتم، رندر کامپوننت CustomListItem تعریف می کند.
این ساختار یک روش منعطف و اعلامی را برای سفارشی کردن رندر و رفتار منوی Dropdown در حالی که یک رابطه سلسله مراتبی واضح بین کامپوننت ها حفظ می کند، امکان پذیر می کند. لطفاً توجه داشته باشید که کامپوننتهای Dropdown.Trigger، Dropdown.List و Dropdown.Option عناصر HTML پیشفرض بدون استایل (به ترتیب دکمه، ul و li) را ارائه میکنند. هر یک از آنها یک پایه را می پذیرند و کاربران را قادر می سازد تا کامپوننت ها را با استایل ها و رفتارهای خود سفارشی کنند.
برای مثال، میتوانیم این کامپوننتهای سفارشیشده را تعریف کرده و مانند بالا از آن استفاده کنیم.
const CustomTrigger = ({ onClick, ...props }) => (
<button className="trigger" onClick={onClick} {...props} />
);
const CustomList = ({ ...props }) => (
<div {...props} className="dropdown-menu" />
);
const CustomListItem = ({ ...props }) => (
<div {...props} className="item-container" />
);
پیاده سازی پیچیده نیست. ما میتوانیم به سادگی یک کانتکست را در Dropdown تعریف کنیم (عنصر ریشه) و تمام stateهایی را که باید مدیریت شوند در داخل قرار دهیم، و از آن در گرههای فرزند استفاده کنیم تا بتوانند به state ها دسترسی داشته باشند (یا این stateها را از طریق API در متن تغییر دهند).
type DropdownContextType<T> = {
isOpen: boolean;
toggleDropdown: () => void;
selectedIndex: number;
selectedItem: T | null;
updateSelectedItem: (item: T) => void;
getAriaAttributes: () => any;
dropdownRef: RefObject<HTMLElement>;
};
function createDropdownContext<T>() {
return createContext<DropdownContextType<T> | null>(null);
}
const DropdownContext = createDropdownContext();
export const useDropdownContext = () => {
const context = useContext(DropdownContext);
if (!context) {
throw new Error("Components must be used within a <Dropdown/>");
}
return context;
};
کد یک نوع DropdownContextType عمومی و یک تابع createDropdownContext را برای ایجاد یک کانتکست با این نوع تعریف می کند. DropdownContext با استفاده از این تابع ایجاد می شود. useDropdownContext یک هوک سفارشی است که به این زمینه دسترسی پیدا میکند، اگر خارج از کامپوننت <Dropdown/>
استفاده شود، خطا ایجاد میکند و از استفاده مناسب در سلسله مراتب کامپوننت مورد نظر اطمینان میدهد.
سپس می توانیم کامپوننت هایی را تعریف کنیم که از Context استفاده می کنند. می توانیم با ارائه دهنده زمینه شروع کنیم:
const HeadlessDropdown = <T extends { text: string }>({
children,
items,
}: {
children: React.ReactNode;
items: T[];
}) => {
const {
//... all the states and state setters from the hook
} = useDropdown(items);
return (
<DropdownContext.Provider
value={{
isOpen,
toggleDropdown,
selectedIndex,
selectedItem,
updateSelectedItem,
}}
>
<div
ref={dropdownRef as RefObject<HTMLDivElement>}
{...getAriaAttributes()}
>
{children}
</div>
</DropdownContext.Provider>
);
};
کامپوننت HeadlessDropdown دو ویژگی دارد: فرزند ها و آیتمها، و از یک هوک سفارشی useDropdown برای مدیریت وضعیت و رفتار خود استفاده میکند. از طریق DropdownContext.Provider زمینه ای را برای به اشتراک گذاشتن state و رفتار با فرزندان خود فراهم می کند. در یک div، یک ref تنظیم میکند و ویژگیهای ARIA را برای دسترسی اعمال میکند، سپس فرزندان خود را برای نمایش کامپوننتهای تو در تو رندر میکند و یک عملکرد Dropdown ساختاریافته و قابل تنظیم را فعال میکند.
توجه داشته باشید که چگونه از useDropdown هوک که در قسمت قبل تعریف کردیم استفاده می کنیم و سپس این مقادیر را به فرزندان HeadlessDropdown منتقل می کنیم. در ادامه می توانیم کامپوننت های فرزند را تعریف کنیم:
HeadlessDropdown.Trigger = function Trigger({
as: Component = "button",
...props
}) {
const { toggleDropdown } = useDropdownContext();
return <Component tabIndex={0} onClick={toggleDropdown} {...props} />;
};
HeadlessDropdown.List = function List({
as: Component = "ul",
...props
}) {
const { isOpen } = useDropdownContext();
return isOpen ? <Component {...props} role="listbox" tabIndex={0} /> : null;
};
HeadlessDropdown.Option = function Option({
as: Component = "li",
index,
item,
...props
}) {
const { updateSelectedItem, selectedIndex } = useDropdownContext();
return (
<Component
role="option"
aria-selected={index === selectedIndex}
key={index}
onClick={() => updateSelectedItem(item)}
{...props}
>
{item.text}
</Component>
);
};
ما یک نوع GenericComponentType را برای مدیریت یک کامپوننت یا یک تگ HTML به همراه هر ویژگی اضافی تعریف کردیم. سه تابع HeadlessDropdown.Trigger، HeadlessDropdown.List و HeadlessDropdown.Option برای ارائه بخش های مربوطه از یک منوی Dropdown تعریف شده اند. هر تابع از as prop استفاده می کند تا امکان رندر سفارشی یک کامپوننت را فراهم کند و ویژگی های اضافی را بر روی کامپوننت رندر شده پخش کند. همه آنها از طریق useDropdownContext به وضعیت و رفتار مشترک دسترسی دارند.
HeadlessDropdown.Trigger به طور پیش فرض دکمه ای را ارائه می کند که منوی Dropdown را تغییر می دهد.
HeadlessDropdown.List یک محفظه لیست را در صورت باز بودن فهرست ارائه می دهد.
HeadlessDropdown.Option آیتمهای فهرست را ارائه میکند و با کلیک کردن، آیتم انتخابشده را بهروزرسانی میکند.
این توابع در مجموع به یک ساختار منوی Dropdown قابل تنظیم و در دسترس اجازه می دهند.
این تا حد زیادی به ترجیح کاربر در مورد نحوه استفاده از کامپوننت Headless در پایگاه کد خود خلاصه می شود. من شخصاً به سمت هوک ها متمایل می شوم زیرا آنها هیچ گونه تعامل DOM (یا DOM مجازی) ندارند. تنها پل بین منطق state مشترک و UI شی ref است. از سوی دیگر، با پیادهسازی مبتنی بر زمینه، زمانی که کاربر تصمیم بگیرد آن را سفارشی نکند، یک پیادهسازی پیشفرض ارائه میشود.
نتیجه
ما این مقاله را در دو بخش منتشر می کنیم. قسمت دوم نشان خواهد داد که چگونه کامپوننت هدلس را می توان برای پشتیبانی از یک رابط کاربری جدید وفق داد و چگونه می توانیم آن را گسترش دهیم تا داده های آن را از یک منبع راه دور دریافت کنیم.
برای اطلاع از زمان انتشار قسمت بعدی در آنوفل یا در شبکه های اجتماعی ما عضو باشید!