📆

【更新】西暦/和暦切り替えられる、Datepickerを作成してみた

2024/05/14に公開

【更新】問題と解決法

  • 初期表示に年だけ今年のが入ってしまう
    初期表示は今日の日付にしていい?確認する

背景

MUIを使って、Next.tsにて和暦と西暦で切り替えられる日付入力欄の実装をすることになり、なんとかできたのでまとめました

やりたいこと

以下の条件で日付入力フォームを実装

  • Datepickerのデフォルトのフォームではなく、元号、年、月、日それぞれ表示するフォームを分けたい。
  • 以下のように、入力欄とカレンダーの表示を連動させたい
  1. カレンダーアイコンを押す→カレンダー出現、日付選択→TextFieldやSelectに日付が反映
  2. Selectで元号選択、TextFieldで日にちを入力したのちEnterを押し、カレンダーアイコンを押すと、入力した日付が選択されている状態で表示される

実装と解説

結論、こんな感じになりました。

"use client";
import * as React from "react";
import dayjs, { Dayjs } from "dayjs";
import {
  Grid,
  TextField,
  Select,
  MenuItem,
  IconButton,
  Popper,
  Button,
  Box,
} from "@mui/material";
import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";

interface Era {
  value: string;
  label: string;
  start: number | "";
}

const eras: Era[] = [
  //   { value: "", label: "選択してね", start: "" },
  { value: "R", label: "令和", start: 2019 },
  { value: "H", label: "平成", start: 1989 },
  { value: "S", label: "昭和", start: 1926 },
  { value: "T", label: "大正", start: 1912 },
  { value: "M", label: "明治", start: 1868 },
];

export default function DatePickerValue() {
  const [era, setEra] = React.useState<string>("R");
  const [year, setYear] = React.useState<string>("");
  const [month, setMonth] = React.useState<string>("");
  const [day, setDay] = React.useState<string>("");
  const [selectedDate, setSelectedDate] = React.useState<Dayjs>(dayjs());

  const handleDateChange = (date: Dayjs) => {
    setSelectedDate(date);
    const era = eras.find((e) => date.year() >= e.start);
    setEra(era ? era.value : "");
    setYear((date.year() - (era ? era.start : 0) + 1).toString());
    setMonth((date.month() + 1).toString());
    setDay(date.date().toString());
  };

  const handleEraChange = (event: React.ChangeEvent<{ value: unknown }>) => {
    const value = event.target.value as string;
    setEra(value);
    const era = eras.find((e) => e.value === value);
    setSelectedDate(
      dayjs()
        .year(
          (era ? era.start : 0) +
            (year ? year - 1 : dayjs().year() - (era ? era.start : 0))
        )
        .month(month ? month - 1 : selectedDate.month())
        .date(day ? day : selectedDate.date())
    );
  };
  const handleYearChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value;
    const yearValue = parseInt(value);
    setYear(value);
    if (value === "") {
      const selectedEra = eras.find((e) => e.value === era);
      if (selectedEra) {
        setSelectedDate(
          dayjs()
            .year(selectedEra.start)
            .month(month ? parseInt(month) - 1 : selectedDate.month())
            .date(day ? parseInt(day) : selectedDate.date())
        );
      }
    } else {
      if (isGregorian) {
        setSelectedDate(
          dayjs()
            .year(yearValue)
            .month(month ? parseInt(month) - 1 : selectedDate.month())
            .date(day ? parseInt(day) : selectedDate.date())
        );
        const era = eras.find((e) => yearValue >= e.start);
        setEra(era ? era.value : "");
      } else {
        const selectedEra = eras.find((e) => e.value === era);
        if (selectedEra) {
          setSelectedDate(
            dayjs()
              .year(selectedEra.start + yearValue - 1)
              .month(month ? parseInt(month) - 1 : selectedDate.month())
              .date(day ? parseInt(day) : selectedDate.date())
          );
        }
      }
    }
  };

  const handleMonthChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value ? parseInt(event.target.value) : "";
    setMonth(value);
    if (value !== "") {
      const selectedEra = eras.find((e) => e.value === era);
      setSelectedDate(
        selectedDate
          .year(
            (selectedEra ? selectedEra.start : 0) +
              (year
                ? year - 1
                : dayjs().year() - (selectedEra ? selectedEra.start : 0))
          )
          .month(value - 1)
          .date(day ? day : selectedDate.date())
      );
    }
  };

  const handleDayChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value ? parseInt(event.target.value) : "";
    setDay(value);
    if (value !== "") {
      setSelectedDate(
        selectedDate
          .year(
            (eras.find((e) => e.value === era)
              ? eras.find((e) => e.value === era)!.start
              : 0) +
              (year
                ? year - 1
                : dayjs().year() -
                  (eras.find((e) => e.value === era)
                    ? eras.find((e) => e.value === era)!.start
                    : 0))
          )
          .month(month ? month - 1 : selectedDate.month())
          .date(value)
      );
    }
  };

  const [open, setOpen] = React.useState(false);
  const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
    null
  );
  const [isGregorian, setIsGregorian] = React.useState(false);

  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    setOpen(true);
    setAnchorEl(event.currentTarget);
  };

  React.useEffect(() => {
    if (isGregorian) {
      setYear("");
    } else {
      const era = eras.find((e) => selectedDate.year() >= e.start);
      setYear((selectedDate.year() - (era ? era.start : 0) + 1).toString());
    }
  }, [isGregorian, selectedDate]);

  const toggleCalendar = () => {
    const newIsGregorian = !isGregorian;
    setIsGregorian(newIsGregorian);
    if (!newIsGregorian) {
      const era = eras.find((e) => selectedDate.year() >= e.start);
      setEra(era ? era.value : "");
    }
  };

  return (
    <>
      <Box sx={{ height: "100vh" }}>
        <LocalizationProvider dateAdapter={AdapterDayjs}>
          <Grid container spacing={2} alignItems="center">
            {!isGregorian && (
              <Grid item>
                <Select value={era} onChange={handleEraChange}>
                  {eras.map((era) => (
                    <MenuItem key={era.value} value={era.value}>
                      {era.label}
                    </MenuItem>
                  ))}
                </Select>
              </Grid>
            )}
            <Grid item>
              <TextField
                type="number"
                value={isGregorian ? selectedDate.year() : year}
                onChange={handleYearChange}
                InputProps={{ inputProps: { min: 1 } }}
                sx={{ width: isGregorian ? "170px" : "76px" }}
              />
            </Grid>
            <Grid item>
              <TextField
                type="number"
                value={month}
                onChange={handleMonthChange}
                InputProps={{ inputProps: { min: 1, max: 12 } }}
              />
            </Grid>
            <Grid item>
              <TextField
                type="number"
                value={day}
                onChange={handleDayChange}
                InputProps={{ inputProps: { min: 1, max: 31 } }}
              />
            </Grid>

            <Grid item>
              <IconButton onClick={handleClick}>
                <CalendarTodayIcon sx={{ position: "relative" }} />
              </IconButton>
              <Popper open={open} anchorEl={anchorEl}>
                <DatePicker
                  sx={{ position: "relative", left: "160%", opacity: 0.0 }}
                  open={open}
                  onClose={() => setOpen(false)}
                  value={selectedDate}
                  onChange={handleDateChange}
                  TextFieldComponent={() => null}
                  disableFuture={true}
                />
              </Popper>
            </Grid>
            <Grid item>
              <Button onClick={toggleCalendar} variant="contained">
                {isGregorian ? "西暦" : "和暦"}
              </Button>
            </Grid>
          </Grid>
        </LocalizationProvider>
      </Box>
    </>
  );
}


コードの解説ですが、まず、カレンダーのアイコンを押したらカレンダーを出現させる、という表示がMUIのDatePickerではできないようで、どうしてもデフォルトの日付入力フォーム内のアイコンを押す→カレンダー出現、としないとカレンダーを表示できないようです。日付入力フォームアリの状態だと、その横にアイコンを追加し、それをクリックしてカレンダーを表示することができるようにできるようです。

ということで、opacityを使って日付入力フォームを強制的に消し去り、カレンダーの表示位置をカレンダーアイコンの真下にいいことスタイルをいじって動かしました。

javascriptでもっと違う西暦→和暦の変更の仕方もあるようですが、今回はuseStateを使って再定義することも考えて上記のようにしました

西暦⇔和暦の切り替えのロジックについてはコメントしてあります

## まとめという名の感想

  • 日付入力フォームを消し去るのにかなり無駄な時間を使った上にかなり強引なやり方だった。情けない。

Discussion