かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript 通常の input[type="text"] で妥当な日付を入力させたい

日付の入力は input[type="date"] がデバイスの選択 UI が表示され存在しない日付が選ばれることもなく使いやすいと思っているのですが、今回 PC のブラウザはカレンダーが表示されたりしなかったり、入力が同じ input の中で年月日別になってたりするのが使いにくいから通常のテキスト入力 (input[type="text"])で入力させたいという要望がありサンプルを作ってみたメモ

仕様を考える

input type="text" で日付を入力させたい

  • 要望
    • 見た目は input[type="text"]
      • input[type="number"] はマウスホイールのスクロールなどで数字が動いてしまうので使いたくない
    • 20230315 のような YYYYMMDD 形式でユーザーに入力させる
    • スマートフォンinput[type="date"] を使う
  • Validation
    • 存在しない日付が入力された場合エラーにする
    • YYYYMMDD 形式でない場合エラーにする
  • UX
    • 全角数字の YYYYMMDD は許容したい

方針

  1. モバイルサイズでは input[type="date"] を表示して使う
  2. PC サイズでは input[type="text"] を表示し入力した値を 日付形式にする
  3. input で最終的な日付の値を同期する
  4. input[type="date"] を正として、フォームに送る値はこちらを使う

PC サイズ input[type="text"] で日付を入力させる方針

  1. ユーザーは input[type="text"] に入力をする
  2. 入力値が 数字 6 + 2 + 2 桁かどうか判定をする
    • 全角の場合は半角数字に変換する
  3. 日付として妥当かバリデーションをする
  4. 入力値を input[type="date"]value にセットする (モバイル時の input と同期)

input[type="text"]` 妥当なで日付を入力できるコンポーネント

今回は日付の操作に dayjs を使用しました ( date-fns を codepen で使うのが面倒そうだったため)

import { useState, FC } from 'React';
import * as dayjs from 'dayjs';
const FORMAT = 'YYYY-MM-DD' as const;

const convertDate = (dateStr: string): string => {
  if (!dateStr) { return ''; }
  const formatDate = dayjs(dateStr, FORMAT).format(FORMAT);  
  return formatDate;
};

// 妥当でない日付の場合フォーマットすると日付が変わることを利用して妥当性を判定する
const isValidDate = (dateStr: string): string => {
  const formatDate = convertDate(dateStr);  
  return dateStr === formatDate; 
};

// 英数の全角を半角に変換する
const convertFullAlphaNumericToHalf = (str: string): string => {
  return str.replace(/[A-Za-z0-9]/g, (s) => {
    return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
  };
);

const App = (): FC => {
  const [date, setDate] = useState<string>("");
  const [message, setMessage] = useState<string>("");
  const timerRef = useRef<number | null>(null);
  
  const onUpdateDate = (value: string): void => {
    if (!/^\d{8}$/.test(value)) {
      setDate("");
      setMessage('入力エラー');
      return;
    }
    // "YYYY-MM-DD" の形に変換
    const dateStr = `${value.slice(0,4)}-${value.slice(4,6)}-${value.slice(6,8)}`;
    if (!isValidDate(dateStr)) {
      setDate("");
      setMessage("存在しない日付です"); 
      return;
    }

    setDate(dateStr);
    setMessage("");
  };

  const onChange = (evt: React.ChangeEvent<HTMLInputElement>): void => {
    const val = evt.currentTarget.value;
    // debounce で変換処理を行う
    clearTimeout(timerRef.current);
    timerRef.current = window.setTimeout(() => {
      const str = convertFullAlphaNumericToHalf(val);
      onUpdateDate(str);
    }, 300);
  };

  return (
    <div className="dateFieldApp">
      <label>生年月日 <small>半角数字で入力してください</small></label>
      <input type="text" onChange={onChange} placeholder="19950107" />
      {message && <span class="error">{message}</span>}
      <input type="date" value={convertDate(date)} required />
    </div>
  );
};

Sample

See the Pen input date by input text by KIKIKI (@kikiki_kiki) on CodePen.

所管

今回作ったのは簡易なサンプルコンポーネントですが、自由に入力できる input[type="text" を使ってユーザー自身に特定のフォーマットでの入力を強いるのは validation が大変になるのと、特に日本語の場合は 全角 ⇄ 半角 の問題があるのでコストをかけないなら素直に input[type="date"] など HTML が用意してくれている要素を使うのが良いのではないかと思いました。
とはいえ React 使って npm のライブラリ使えば処理をコンポーネントに閉じ込めることができるのでまぁ作れなくはないよな〜って印象でした。

久々に React 触って楽しかった!


[参考]

前使ってた USB 充電のタイマーが壊れたからこのタイマー買いました。かわいくて気に入ってる