目的
Headkess UI のListBoxがキレイなのでコンポーネント化したかった。ちなみにサンプルのコードはだいぶ複雑なので、コンポーネント化しないと逆に使わないかも。
なかなかキレイですよね。
【Headless UI】https://headlessui.com/react/listbox
MyListBoxというコンポーネントにして、こんな感じで使いたい!
...
<p className="text-sm -mb-2">Time</p>
<MyListBox regiName="time" items={["10:00", "11:00", "12:00"]} />
...
完成形
https://github.com/yahsmassa/myNextFormTips
Headless UIとは
HeadはStyleのことを言っていて、Style自体は持たず、「Active、Open、Selected」といった状態だけを内部に持っていて、それを使えば、マニアックなカスタマイズができる。サンプルのコードを読めばイメージできますが、しばらく自分も謎のままでした。使ってみて初めてわかります。
ちょっとやろうと思っただけなのに、使おうとすると困難の嵐 〜
- 本家サイトのサンプルが微妙に不親切?
- リストボックスやコンボボックスで選んだ値をフォームに戻すサンプル欲しかった!
- Githubにサンプルあまりなし、記事も少ない
意外と難しく、くじけそうになりましたが、なんとかできました。
〜 ポイント 〜
- CreateContextでフォーム用ステートをグローバル変数化
- コンポーネント側でフォームの変数更新
- 分割構文でフォーム用のオブジェクトを更新
- Formタグは結局使わず(もちろん使っても良い)
あとは、実際のコードをご覧いただいて、物好きな人は参考にしてください。
import React, { createContext, useState } from "react";
import MyListBox from "../components/MyListBox";
interface IForm {
time: string;
stylist: string;
menu: string;
reset: boolean;
}
const initFormData: IForm = {
menu: "",
stylist: "",
time: "",
reset: false,
};
export const myfm = createContext({});
export default function App() {
const [fmFields, setfmFields] = useState<IForm>(initFormData);
const value = {
fmFields,
setfmFields,
};
const reset = () => {
setfmFields({
...initFormData,
reset: true,
});
};
const btnCss =
"mt-3 ml-5 w-[100px] text-blue-500 border-2 border-blue-500 rounded-2xl hover:bg-blue-300 hover:text-white";
return (
<myfm.Provider value={value}>
<div className="m-5">
<h1 className="mb-3">Reservation Menu</h1>
<p className="text-sm -mb-2">Time</p>
<MyListBox regiName="time" items={["10:00", "11:00", "12:00"]} />
<p className="text-sm mt-2 -mb-1">Stylist</p>
<MyListBox regiName="stylist" items={["Lisa", "Bob", "Steve"]} />
<p className="text-sm mt-2 -mb-2">Menu</p>
<MyListBox regiName="menu" items={["cut", "perm", "color"]} />
<button onClick={reset} className={btnCss}>
Reset
</button>
<button
onClick={() => alert(JSON.stringify(fmFields))}
className={btnCss}
>
Submit
</button>
</div>
</myfm.Provider>
);
}
そんでもって、肝心のコンポーネントです。本家サンプルをコピペして編集した感じ。
import { Fragment, useState, useEffect, useContext } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
import { myfm } from "../pages/index";
import cn from "clsx";
interface Props {
items: any[];
className?: string;
regiName: string;
}
export default function MyListBox({ items, className, regiName }: Props) {
const [selected, setSelected] = useState("");
const { fmFields, setfmFields }: any = useContext(myfm);
useEffect(() => {
setfmFields({
...fmFields,
[regiName]: selected,
reset: false,
});
}, [selected]);
useEffect(() => {
if (fmFields.reset) setSelected("");
}, [fmFields]);
return (
<div className={cn("w-72", className)}>
<Listbox value={selected} onChange={setSelected}>
<div className="relative mt-1">
<Listbox.Button
className="relative h-9 w-full cursor-default rounded-lg bg-white py-2 pl-3
pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500
focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75
focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm"
>
<span className="block truncate">{selected}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="pl-0 z-50 absolute mt-1 max-h-60 w-full overflow-auto
rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5
focus:outline-none sm:text-sm"
>
{items.map((item, idx) => (
<Listbox.Option
key={idx}
className={({ active }) =>
cn(
"relative cursor-default select-none py-2 pl-10 pr-4",
active ? "bg-amber-100 text-amber-900" : "text-gray-900"
)
}
value={item}
>
{({ selected }) => (
<>
<span
className={cn(
"block truncate",
selected ? "font-medium" : "font-normal"
)}
>
{item}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-amber-600">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
);
}
〜 失敗したこと 〜
React-hook-formで実装したら、Headless UIの大きな特徴である、Selectedが効かなくなってしまった。https://gist.github.com/jasonabullard/dfa2ec92bff5ff0a498f649c3c4cb432 を真似すればわかります。
問題点
親コンポーネントと、子コンポーネントの両方で状態変数を持ってるので、どうしても制御が複雑になります。Headless UIは、ConboboxやListBox以外のものを使うのが良さそうです。