
Reactでselectタグの代わりにドロップダウンを作る方法
htmlのselectタグとoptionタグで、特にoptionタグはブラウザに依存しててcssを適用してリッチなドロップダウンを作るのは結構大変です。
多くの場合、無理やりoptionタグにcssをつけるよりも、独自にコンポーネントを作って、代替することが多いです。
ということで、今日はselectタグを独自コンポーネントに置き換える方法を紹介します。
ドロップダウンメニューコンポーネントの視覚的構造
技術的な話に入る前に、ドロップダウンメニューコンポーネントのビジュアル構造を見て、要件を決めましょう。
ドロップダウンメニューは、4つの基本コンポーネントで構成されています。
- ヘッダーラッピング
- ヘッダータイトル
- リストラッピング
- リストアイテム
対応するHTMLは以下のようになります。
つまり、dd-headerをクリックするとdd-listが切り替わり、
dd-wrapperの外でクリックされると閉じることができるようになって、
尚且つデータに基づいて<button>タグを自動的に入力したいわけです。
そして、ヘッダーのタイトルを動的に制御する必要もありますよね。
コンポーネントの親子関係
親コンポーネントは、単一または複数のドロップダウンメニューを保持し、
各ドロップダウンメニューは固有のコンテンツを持っているので、
情報をプロップとして渡してパラメータ化する必要があります。
ここでは、複数の場所を選択するドロップダウンメニューがあるとします。
親コンポーネントの中に次のようなステート変数があるとします。
constructor(){
super()
this.state = {
location: [
{
id: 0,
title: 'New York',
selected: false,
key: 'location'
},
{
id: 1,
title: 'Dublin',
selected: false,
key: 'location'
},
{
id: 2,
title: 'California',
selected: false,
key: 'location'
},
{
id: 3,
title: 'Istanbul',
selected: false,
key: 'location'
},
{
id: 4,
title: 'Izmir',
selected: false,
key: 'location'
},
{
id: 5,
title: 'Oslo',
selected: false,
key: 'location'
}
]
}
}
配列を入力する際にmapメソッドのkey propで使用するユニークなid、
リストの各アイテムのタイトル、それから、
リストの選択されたアイテムを切り替えるためのselectedという名前のブール変数
(ドロップダウンメニューで複数選択されている場合)と、
そしてkey変数があります。
key変数は、setState関数で使用するのに便利です。これについては後ほど触れます。
それでは、これまでにDropdownコンポーネントにプロップとして渡したものを見てみましょう。
下の図は、Dropdownコンポーネントが親コンポーネントで使用されており、
表示するタイトルと、ドロップダウンリストに入力するデータの配列を渡しています。
<Dropdown
title="Select location"
list={this.state.location}
/>
render()メソッドを編集する前に、
ドロップダウンコンポーネントに以下のステート変数が必要です。
<constructor(props){
super(props)
this.state = {
isListOpen: false,
headerTitle: this.props.title
}
}
ここでは、メニューリストを切り替えるためのisListOpenブール変数と、
デフォルトのタイトルプロップに等しいheaderTitleがあります。
それでは、コンポーネントの render() メソッドを見てみましょう。
ここでは、renderメソッドを使用しています。
前述のヘッダーと、リストアイテムを含むリストの構造ができています。
レンダリング・メソッドでは、toggleList()関数とselectItem()関数が使われているので、
これらを作成してみましょう。
render() {
const { isListOpen, headerTitle } = this.state;
const { list } = this.props;
return (
<div className="dd-wrapper">
<button
type="button"
className="dd-header"
onClick={this.toggleList}
>
<div className="dd-header-title">{headerTitle}</div>
{isListOpen
? <FontAwesome name="angle-up" size="2x" />
: <FontAwesome name="angle-down" size="2x" />}
</button>
{isListOpen && (
<div
role="list"
className="dd-list"
>
{list.map((item) => (
<button
type="button"
className="dd-list-item"
key={item.id}
onClick={() => this.selectItem(item)}
>
{item.title}
{' '}
{item.selected && <FontAwesome name="check" />}
</button>
))}
</div>
)}
</div>
)
}
toggleList()関数が行うことは、単純にisListOpenステート変数をトグルすることで、アイテムリストの表示・非表示を切り替えることです。
toggleList = () => {
this.setState(prevState => ({
isListOpen: !prevState.isListOpen
}))
}
一方、selectItem()関数は、headerTitleステートを選択されたアイテムのタイトルに設定し、isListOpenステートをfalseに設定して、選択時にリストを閉じるようにしています。
これらの状態を設定した後、コールバック関数resetThenSet()を呼び出しますが、これはDropdown /に渡す必要のあるpropです。
これについては、次の章で説明します。
このコールバック関数を呼び出すと、ロケーションの状態が更新されます
selectItem = (item) => {
const { resetThenSet } = this.props;
const { title, id, key } = item;
this.setState({
headerTitle: title,
isListOpen: false,
}, () => resetThenSet(id, key));
}
子コンポーネントから親の状態を制御する
子コンポーネントにpropとして何かを渡した場合、そのデータは使用することしかできず、追加のpropを配置しない限り変更することはできません。
親コンポーネントで状態を制御する関数を定義し、その関数を子コンポーネントにpropとして渡しておけば、
子コンポーネントからその関数を呼び出して親コンポーネントの状態を設定することができます。
ドロップダウンの場合、リスト要素がクリックされると、親コンポーネントの位置情報の状態で、対応するオブジェクトの選択されたキーをトグルできるようにする必要があります。
これには、ドロップダウンコンポーネントのプロップとして渡されるresetThenSet()関数を使用します。
この関数は、location stateをクローンし、配列内の各オブジェクトのselected keyをfalseに設定した後、クリックされたアイテムのselected keyのみをtrueに設定します。
この関数は、親コンポーネントで定義されています。
resetThenSet = (id, key) => {
const temp = [...this.state[key]];
temp.forEach((item) => item.selected = false);
temp[id].selected = true;
this.setState({
[key]: temp,
});
}
そして、
<Dropdown
title="Select location"
list={this.state.location}
resetThenSet={this.resetThenSet}
/>
おわりに
以上になります。
この設定はシングルセレクトのドロップダウンに必要でした。しかし、ドロップダウンで複数のアイテムを選択できるようにしたい場合は、resetThenSet()の代わりに別の関数が必要です。
今回の記事は、参考にした記事を分かりやすい部分を抜粋したもので、より詳しいことは本記事に書いてあるので、よければ見てみてください。