ツクログネット

【React】時刻や生年月日などをセレクトボックスで選択するピッカーを作る

eye catch

今回は、時刻を選択するピッカーと仮定して進めていきます。

コンポーネントを作成する

はじめに、ピッカー単体を描画する役割を持つItemPickerコンポーネントを作成します。

components/ItemPicker.js
import React from 'react';
import styled from 'styled-components'
import Spacer from './Spacer'

const StyledItemPicker = styled.div`
	text-align: center;

	& .item-name {
		color: ${({ textColor }) => textColor ? textColor : `#333`};
	}

	& select {
		background-color: transparent;
		border: none;
		border-radius: .2em;
		color: ${({ textColor }) => textColor ? textColor : `#333`};
		cursor: pointer;
		font-size: 1em;
		outline: none;
		padding: .2em;
	}
`

const ItemPicker = ({ className, itemName, values, textColor, handleChange }) => {
	return (
		<StyledItemPicker className={className} textColor={textColor}>
			<div className="item-name">
				{itemName}
			</div>
			<Spacer size=".5em" />
			<select id={itemName} name={itemName} onChange={handleChange}>
				{
					values.map((value, i) => (
						<option
							id={value}
							name={value}
							value={value}
							key={i}
						>
							{typeof value !== 'string' ? String(value).padStart(2, "0") : value}
						</option>
					))
				}
			</select>
		</StyledItemPicker>
	)
}

export default ItemPicker

このItemPickerコンポーネントは次のプロパティを受け取ります。

プロパティ名
className クラス名
itemName 選択項目の名前
values 選択項目の値がセットされた配列
textColor 文字の色
handleChange セレクトボックスの値が選択されたときに実行する関数

ItemPickerコンポーネント内で使用しているSpacerコンポーネントは、コンポーネント間に余白を加える役割を持ちます。SpacerコンポーネントについてはReactで余白をどうスタイリングするかを参考にしました。

このItemPickerコンポーネントを使用すると、ブラウザに次のようなピッカーが表示されます。

select-box-item-picker

次に、選択項目の数だけピッカーを描画する役割を持つItemPickersコンポーネントを作成します。

components/ItemPickers.js
import React from 'react';
import styled from 'styled-components'
import ItemPicker from './ItemPicker'
import Spacer from './Spacer'

const StyledItemPickers = styled.div`
	display: flex;
	justify-content: center;
`

const ItemPickers = ({ className, items, textColor, handleChange }) => {

  // プロパティ名を値に持つ配列に変換
	const itemNames = Object.keys(items)

	return (
		<StyledItemPickers className={className} textColor={textColor}>
			{
				itemNames.map((itemName, i) => (
				<>
					<Spacer size=".2em" horizontal={true} />
					<ItemPicker
						className="item"
						itemName={itemName}
						values={items[itemName]}
						handleChange={handleChange}
						key={i}
					/>
					<Spacer size=".5em" horizontal={true} />
				</>
			))}
		</StyledItemPickers>
	)
}

export default ItemPickers

ItemPickersコンポーネントでは、先ほど作成したItemPickerコンポーネントを選択項目の数だけ追加しています。

また、このItemPickersコンポーネントは次のプロパティを受け取ります。

プロパティ名
className クラス名
items プロパティ名が各選択項目のタイトル、値が各選択項目の値がセットされた配列で構成されるオブジェクト
textColor 文字の色
handleChange セレクトボックスの値が選択されたときに実行する関数

このItemPickersコンポーネントを使用する際、itemsには次のような形式のオブジェクトを渡します。

{
		hours: [0, 1, 2, 3, ..., 59],
		mins: [0, 1, 2, 3, ..., 59],
		secs: [0, 1, 2, 3, ..., 59]
}

ピッカーを表示する

では、作成したItemPickersコンポーネントを使用してブラウザにピッカーを表示します。

App.js
import React, {
  useState,
  useCallback
} from 'react';
import styled, { createGlobalStyle } from 'styled-components'
import ItemPickers from './components/ItemPickers'

const TIMES = {
  hours: [...Array(60)].map((u, i) => i),
	mins: [...Array(60)].map((u, i) => i),
	secs: [...Array(60)].map((u, i) => i)
}

const GlobalStyle = createGlobalStyle`
  body {
  	background-color: var(--bg-color);
	color: var(--text-color);
	display: flex;
	align-items: center;
	justify-content: center;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
	font-size: 4vmin;
	margin: 0;
	height: 100vh;
	width: 100%;
  }
`;

const App = () => {
	const [selectItems, setSelectItems] = useState(() => {
		const newItems = {}

		Object.keys(TIMES).forEach((name, i) => {
			newItems[name] = TIMES[name][0]
		})

		return newItems
	})

	const handleChange = useCallback((e) => {
		setSelectItems(p => ({
			...p,
			[e.target.id]: Number(e.target.value)
		}))
	}, [])

	return (
		<>
			<GlobalStyle />
			<div>
				<ItemPickers
					className="time-picker"
					items={TIMES}
					handleChange={handleChange}
				/>
			</div>
		</>
	);
};

export default App

これにより、ブラウザに次のようなピッカーが表示されます。

select-box-item-picker2

selectItemsは選択中の各値を次のようなオブジェクト形式で保存するステートです。

selectItemsステートの初期値は、プロパティ名にTIMESオブジェクトのプロパティ名、プロパティの値にTIMESオブジェクトの各プロパティの値である配列内の先頭の数値で構成されたオブジェクトです。

const [selectItems, setSelectItems] = useState(() => {
		const newItems = {}

		Object.keys(TIMES).forEach((name, i) => {
			newItems[name] = TIMES[name][0]
		})

		return newItems
	})

handleChange関数は、selectItemsオブジェクトを選択した各項目の値に更新します。その方法は、選択したoption要素の値e.target.valueをselect要素のid名と一致するselectItemsステートのプロパティ名の値に上書きします。

setSelectItems(p => ({
			...p,
			[e.target.id]: Number(e.target.value)
		}))

ピッカーで選択した値をブラウザに反映させる

ピッカーで選択した各値をブラウザに反映させてみます。

App.js
// 省略

import TimeDisplay from './components/TimeDisplay'
import Spacer from './components/Spacer'

// 省略

const App = () => {
	// 省略

	return (
		<>
			<GlobalStyle />
			<div>
				<ItemPickers
					// 省略
				/>
        <Spacer size="1.6em" />
				<TimeDisplay
					className="time"
					time={selectItems}
					delimiter=":"
					fontSize="2.8em"
				/>
			</div>
		</>
	);
};

export default App

次のようにピッカーで選択中の値が、ピッカーの下に表示されるようになりました。

select-box-item-picker3

上記の時刻の部分は、TimeDisplayコンポーネントを使用して表示しています。

TimeDisplayコンポーネントは、時刻のデータをデジタル時計のように表示する役割を持つコンポーネントです。

components/TimeDisplay.js
import React from 'react';
import styled from 'styled-components'

const StyledTimeDisplay = styled.div`
	color: ${({ textColor }) => textColor ? textColor : `#333`};
	font-family: 'Share Tech Mono', monospace;
	font-weight: bold;
	font-size: ${({ fontSize }) => fontSize ? fontSize : '1em'};
`;

const TimeDisplay = ({ className, time, delimiter, fontSize, textColor }) => {
	const newTime = Array.isArray(time) ? time : Object.values(time)

	return (
		<StyledTimeDisplay
			className={className}
			fontSize={fontSize}
			textColor={textColor}
		>
			{
				newTime.map((n, i, array) => (
					<>
            {/* padStartは文字列のメソッドであるため、Stringでnを文字列に変換 */}
						<span>{String(n).padStart(2, "0")}</span>

            {/* 末尾の区切り文字は表示しない */}
						{(i !== array.length - 1) && delimiter}
					</>
				))
			}
		</StyledTimeDisplay>
	);
};

export default TimeDisplay

timeプロパティには時刻のデータを次のようなオブジェクトか配列で指定します。

// オブジェクト
{
  hour: 0,
  min: 1,
  sec: 40
}

// 配列
[0, 1, 40]

受け取ったtimeがオブジェクトだとmapメソッドが使えないため、配列に変換します。

const newTime = Array.isArray(time) ? time : Object.values(time)

DEMO

下記で実際の動作を確認できます。