【Reactレシピ】バリデーションつきセレクトボックスコンポーネントを作る

こんにちは。

先日、頭痛と吐き気で1日寝込んでいたはこだてたろうです。
熱はなかったのですが、逆に平熱より1度低いという謎の症状でした。
毛布を何枚もかけても汗一つかかなかったです。
あれは一体なんだったのか。。。

目次

はじめに

さて、前回の記事ではバリデーションつき入力欄コンポーネントを作成しました。

今回は入力欄の作りにならって、バリデーションつきセレクトボックスコンポーネントを作りたいと思います。

実物はこちら

あわせて読みたい
React Recipe Reactアプリのレシピ集

リポジトリはこちら

GitHub
GitHub - hakoratory/react-recipe Contribute to hakoratory/react-recipe development by creating an account on GitHub.

この記事を読むことで作れるもの

この記事を読むことで、以下のような画面が作れるようになります。

実装

先にコード全容です。

せっかくなので、前回作った バリデーションつき入力欄コンポーネント LabeledTextInputWithValidation も使っています。

'use client'

import React, { useState, ChangeEvent, FormEvent } from "react";
import './style.css'
import LabeledTextInputWithValidation from "@/app/input-field/select-box-component/components/LabeledTextInputWithValidation";
import {prefectures} from "@/data/prefectures";
import Label from "@/app/input-field/select-box-component/components/Label";
import ErrorMessage from "@/app/input-field/select-box-component/components/ErrorMessage";

type Option = {
  id: string;
  name: string;
}

type SelectBoxProps = {
  id?: string;
  value: string;
  options: Option[];
  onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
}

function SelectBox({ id, value, options, onChange }: SelectBoxProps) {
  return (
    <select id={id} value={value} onChange={onChange}>
      <option value="">選択してください</option>
      {options.map((option) => (
        <option key={option.id} value={option.id}>{option.name}</option>
      ))}
    </select>
  );
}

type LabeledSelectBoxProps = SelectBoxProps & {
  label: string;
};

function LabeledSelectBox({ id, label, value, options, onChange }: LabeledSelectBoxProps) {
  return (
    <>
      <Label htmlFor={id} text={label}/>
      <div>
        <SelectBox
          id={id}
          value={value}
          options={options}
          onChange={onChange}
        />
      </div>
    </>
  )
}

function LabeledSelectBoxWithValidation(
  {
    id,
    label,
    value,
    options,
    onChange,
    required
  }: {
    id?: string;
    label: string;
    value: string;
    options: Option[];
    onChange: (event: ChangeEvent<HTMLSelectElement>) => void;
    required?: true;
  }
) {
  const [error, setError] = useState<string>('');

  const validate = (value: string): string => {
    if (required && value.trim() === '') {
      return `必須項目です。`;
    }
    return '';
  };

  const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
    const newValue = event.target.value;
    onChange(event);
    const errorMessage = validate(newValue);
    setError(errorMessage);
  };

  return (
    <>
      <LabeledSelectBox
        id={id}
        label={label}
        value={value}
        options={options}
        onChange={handleChange}
      />
      <div style={{color: 'red', fontSize: '1rem', minHeight: '1.5rem', marginTop: '0.5rem'}}>
        <ErrorMessage text={error}/>
      </div>
    </>
  )
}

function SelectBoxComponent() {
  const [name, setName] = useState<string>('');
  const [nameKana, setNameKana] = useState<string>('');
  const [postalCode, setPostalCode] = useState<string>('');
  const [prefecture, setPrefecture] = useState<string>('');
  const [address, setAddress] = useState<string>('');
  const [phoneNumber, setPhoneNumber] = useState<string>('');

  const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {
    event.preventDefault();
    console.log({name, nameKana});
  };

  return (
    <main>
      <div style={{marginTop: '2rem', marginLeft: '2rem'}}>
        <form onSubmit={handleSubmit}>
          <div>
            <LabeledTextInputWithValidation
              label={'名前'}
              value={name}
              onChange={(event: ChangeEvent<HTMLInputElement>) => setName(event.target.value)}
              placeholder={'名前を入力してください'}
              required
              max={50}
              min={2}
            />
          </div>
          <div>
            <LabeledTextInputWithValidation
              label={'名前(カナ)'}
              value={nameKana}
              onChange={(event: ChangeEvent<HTMLInputElement>) => setNameKana(event.target.value)}
              placeholder={'名前(カナ)を入力してください'}
              required
              max={50}
              min={2}
              pattern={[
                {regex: /^[ァ-ヴ]+$/, message: 'カタカナのみ使用できます'}
              ]}
            />
          </div>
          <div style={{marginTop: '1rem'}}>
            <LabeledTextInputWithValidation
              label={'郵便番号'}
              value={postalCode}
              onChange={(event: ChangeEvent<HTMLInputElement>) => setPostalCode(event.target.value)}
              placeholder={'郵便番号を入力してください'}
              required
              max={7}
              min={7}
              pattern={[
                {regex: /^[0-9]+$/, message: '数字のみ使用できます'}
              ]}
            />
          </div>
          <div>
            <LabeledSelectBoxWithValidation
              value={prefecture}
              options={prefectures}
              onChange={e => setPrefecture(e.target.value)}
              label={'都道府県'}
              required
            />
          </div>
          <div>
            <LabeledTextInputWithValidation
              label={'市区町村以降の住所'}
              value={address}
              onChange={(event: ChangeEvent<HTMLInputElement>) => setAddress(event.target.value)}
              placeholder={'市区町村以降の住所を入力してください'}
              required
              max={50}
            />
          </div>
          <div>
            <LabeledTextInputWithValidation
              label={'電話番号'}
              value={phoneNumber}
              onChange={(event: ChangeEvent<HTMLInputElement>) => setPhoneNumber(event.target.value)}
              placeholder={'電話番号を入力してください'}
              required
              max={11}
              min={10}
              pattern={[
                {regex: /^[0-9]+$/, message: '数字のみ使用できます'}
              ]}
            />
          </div>
          <div style={{marginTop: '0.5rem'}}>
            <button type="submit">SUBMIT</button>
          </div>
        </form>
      </div>
    </main>
  );
}

export default SelectBoxComponent;

各部分の解説

セレクトボックスコンポーネントを作る

まずはセレクトボックスのコンポーネントを作ります。

SelectBox コンポーネントは select 要素のみを返します。
div などでくくったりせず、レイアウトに関しては何も指定していないことに注目してください。

type Option = {
  id: string;
  name: string;
}

type SelectBoxProps = {
  id?: string;
  value: string;
  options: Option[];
  onChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
}

function SelectBox({ id, value, options, onChange }: SelectBoxProps) {
  return (
    <select id={id} value={value} onChange={onChange}>
      <option value="">選択してください</option>
      {options.map((option) => (
        <option key={option.id} value={option.id}>{option.name}</option>
      ))}
    </select>
  );
}

SelectBoxProps.id このプロパティは select 要素に id を指定するためのものです。
ラベルをクリックしたらセレクトボックスにフォーカスがあたるようにするために使用します。

SelectBoxProps.options このプロパティはセレクトボックスの選択肢を指定するためのものです。
option 要素を表示するために、idname を持つオブジェクトを渡す必要がありますが、それは TypeScript の型で制限しています。

ラベルつきのセレクトボックスコンポーネントを作る

次に、ラベルつきのセレクトボックスコンポーネントを作ります。

type LabeledSelectBoxProps = SelectBoxProps & {
  label: string;
};

function LabeledSelectBox({ id, label, value, options, onChange }: LabeledSelectBoxProps) {
  return (
    <>
      <Label htmlFor={id} text={label}/>
      <div>
        <SelectBox
          id={id}
          value={value}
          options={options}
          onChange={onChange}
        />
      </div>
    </>
  )
}

propsSelectBox コンポーネントとほぼ同じです。
ラベルを表示するために、label を追加しています。

Label コンポーネントは、前回の記事で作成したものです。

ラベルとセレクトボックスを横並びにしたくないので、SelectBox コンポーネントをdiv でくくっています。

このコンポーネントはラベルとセレクトボックスを束ねて表示するだけ、つまりレイアウトのみに集中していることに注目してください。

バリデーションつきのセレクトボックスコンポーネントを作る。

最後に、バリデーションつきのセレクトボックスコンポーネントを作ります。

function LabeledSelectBoxWithValidation(
  {
    id,
    label,
    value,
    options,
    onChange,
    required
  }: {
    id?: string;
    label: string;
    value: string;
    options: Option[];
    onChange: (event: ChangeEvent<HTMLSelectElement>) => void;
    required?: true;
  }
) {
  const [error, setError] = useState<string>('');

  const validate = (value: string): string => {
    if (required && value.trim() === '') {
      return `必須項目です。`;
    }
    return '';
  };

  const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
    const newValue = event.target.value;
    onChange(event);
    const errorMessage = validate(newValue);
    setError(errorMessage);
  };

  return (
    <>
      <LabeledSelectBox
        id={id}
        label={label}
        value={value}
        options={options}
        onChange={handleChange}
      />
      <div style={{color: 'red', fontSize: '1rem', minHeight: '1.5rem', marginTop: '0.5rem'}}>
        <ErrorMessage text={error}/>
      </div>
    </>
  )
}

props.requiredtrue が指定されると必須チェックが行われ、未選択だった場合にエラーメッセージを表示します。

おわりに

いかがだったでしょうか?

今回は、入力欄コンポーネントの作りにならってセレクトボックスコンポーネントを作成しました。

ついでに郵便番号や住所の入力欄もいくつか追加してみましたが、いまのところ問題なく使えそうですね。

ハコラトリでは、「できる」を増やしてワクワクする人生を送るための情報をシェアしています。
ぜひ一緒にワクワクな人生を送れるように「できる」を増やしていきましょう。

これからもReactで色々なUIを作って紹介していきますので、「こんなUIを作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

28歳のときに音楽家からエンジニアに転向。
WEB系のシステム開発(Java, C#, Vue, React など)に携わっています。

「できる」を増やしてワクワクな人生を送るための情報をシェアしています。

コメント

コメントする

目次