0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Headless UIのListBoxをコンポーネント化

Last updated at Posted at 2022-09-19

目的

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タグは結局使わず(もちろん使っても良い)

あとは、実際のコードをご覧いただいて、物好きな人は参考にしてください。

index.tsx
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>
  );
}

そんでもって、肝心のコンポーネントです。本家サンプルをコピペして編集した感じ。

MyListBox.tsx
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以外のものを使うのが良さそうです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?