Skip to content
On this page

calender ui

2022-10-23


自社で開発してる UI ライブラリ ingred-ui を使って、カレンダー UI のプロトタイピングをしてみた。
元々 react-dates が React のメジャーアップデートを追従していなかったり、moment.js に強く依存してるためそれを剥がしたかったという狙いがあり、今回フルスクラッチでできないかなーという意図のもと、本当にシンプルで最低限のものを作って温度感を確かめてみた。
だいぶ最低限の機能のみを割と想像を多く含む理解度で適当に書いてみただけなので、実際に使うにはほど遠く、まだまだ改善点はあるが、とりあえずの目的は達成できたので良しとする。

作ったもの

機能

日付の選択や変更が可能なカレンダーの見た目がどの程度の労力で作れるかを一旦試したかったので、すごく簡単に作った。
機能としてはデフォルトで今月のカレンダーが表示されていて、今日の日付が選択済みになっている。これは引数で上書きすることができる。
日付をクリックすると選択状態になり、それ用の handler も渡せるようにしてある。
しかしそれ以外のことはしていない。

実装

大きく、以下のように実装を分けた。

  • App.tsx
    • これはユーザー視点で見たときのコンポーネント
  • DatePicker.tsx
    • ここでカレンダーのレンダリングや日付の計算等メインの処理を書いている
  • Day.tsx
    • ここでは日付の見た目を作っている

また、各コンポーネントの中で styled-components を使用して色付けをおこなっている。

使用技術

ingred-ui に入れる前提で適当に作ってみてるという温度感なので、ingred-ui とそれを取り巻く React、styled-components を使っている。
また、カレンダーの日付の計算には day.js を使っている。

WARNING

日付のライブラリだと、moment.js がメジャーではあるが、moment.js は現在メンテナンスモードに切り替わっているため、day.js を使っている。
参考: Project Status | moment.js

App.tsx

App.tsx はカレンダーの見た目を作るためのコンポーネントであり、ユーザーが使う側のコンポーネントとしての役割を持つ。
ここでは、DatePicker コンポーネントを呼び出しているだけで、カレンダーの見た目を作るためのコンポーネントは DatePicker に任せている。

import dayjs, { Dayjs } from "dayjs";
import { createTheme, ThemeProvider } from "ingred-ui";
import { useState } from "react";
import { DatePicker } from "./DatePicker";

function App() {
  const theme = createTheme();
  const [date, setDate] = useState<Dayjs>(dayjs());

  return (
    <ThemeProvider theme={theme}>
      <DatePicker date={date} onDateChange={setDate} />
    </ThemeProvider>
  );
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

DatePicker.tsx

肝となってくるのが、この DatePicker コンポーネント。ここで状態を持つようにしてる。
だいぶ感覚だけで書いてしまってるので、もっと良い書き方があるかもしれないが、とりあえず動くものを作ってみた。

まずは型定義、単純に選択中の日付と変更時に呼ばれる callback を props で受け取るようにしている。

import dayjs, { Dayjs } from "dayjs";

type Props = {
  date?: Dayjs;
  onDateChange?: (date: Dayjs) => void;
};
1
2
3
4
5
6

そして次にコンポーネントの定義。
そこまで難しいことはしておらず、引数で受け取った date を元に、カレンダーの日付を計算している。

変数をコンポーネント内で 3 つ定義した。

const vdate = date.clone();
const daysList = Array.from(new Array(date.daysInMonth()), (_, i) => i + 1);
const dayOfWeek = (date.startOf("month").day() + 7) % 7;
1
2
3
  • vdate
    • カレンダーの日付を計算するための変数
    • 引数から受け取った選択中の日付を clone して value として保持するようにしている
    • 実際に更新されるのは date の方なので vdate はこのコンポーネントの中でのみ利用する変数
  • daysList
    • dayjs にはその月が何日あるかを取得する daysInMonth という関数があるため、それを用いてカレンダーに表示する日数分のリストを定義している
    • これを使ってカレンダーの見た目を grid で表現することができる
  • dayOfWeek
    • その月が何曜日から始まるかを計算する
    • dayjs では day 関数を使うことで曜日を取得することができる(日曜日が 0 でそこからインクリメントされていき、土曜日が 6)
    • これを使って、その月が何曜日から始まるかを計算し、カレンダー上での開始位置を決める

また、曜日と月の定数も用意しておくと便利。
これは copilot 様がいい感じに補完を出してくれた。

export const weekList = {
  en: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
  ja: ["", "", "", "", "", "", ""],
};

export const monthList = {
  en: [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "June",
    "July",
    "Aug",
    "Sept",
    "Oct",
    "Nov",
    "Dec",
  ],
  ja: [
    "1月",
    "2月",
    "3月",
    "4月",
    "5月",
    "6月",
    "7月",
    "8月",
    "9月",
    "10月",
    "11月",
    "12月",
  ],
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

上記で定義した変数と定数を使い、カレンダーの見た目を作っていく。
実際のコードは以下のようになる。
そこまで難しいことをしてるわけではなく、変数と定数をループで回してその戻り値をレンダリングして grid で見た目を書いていくだけという形になっている。

export const DatePicker: FC<Props> = ({ date = dayjs(), onDateChange }) => {
  const vdate = date.clone();
  const daysList = Array.from(new Array(date.daysInMonth()), (_, i) => i + 1);
  const dayOfWeek = (date.startOf("month").day() + 7) % 7;

  const handleDateChange = useCallback(
    (newDay: Dayjs) => {
      onDateChange?.(newDay);
    },
    [onDateChange]
  );

  return (
    <Container>
      <Typography align="center" component="h1" weight="bold">
        {date.format("MM月")}
      </Typography>
      <DatePickerContainer>
        {weekList["ja"].map((week) => (
          <DayStyle key={week}>{week}</DayStyle>
        ))}

        {Array.from(new Array(dayOfWeek), (_, i) => (
          <DayStyle key={i} />
        ))}
        {daysList.map((day) => (
          <DayStyle key={day}>
            <Day
              key={day}
              value={dayjs(new Date(vdate.year(), vdate.month(), day))}
              onClickDate={handleDateChange}
              selected={date.date() === day}
            >
              {day}
            </Day>
          </DayStyle>
        ))}
      </DatePickerContainer>
    </Container>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

色付けの部分は以下のようになっている。
<DatePickerContainer /> で grid を表現している。

import styled from "styled-components";
import { Flex } from "ingred-ui";

export const Container = styled(Flex)`
  padding: ${({ theme }) => theme.spacing}px;
  box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2);
  border-radius: ${({ theme }) => theme.radius}px;
  width: fit-content;
`;

export const DatePickerContainer = styled(Flex)`
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  grid-gap: ${({ theme }) => theme.spacing}px;
`;

export const DayStyle = styled.span`
  padding: ${({ theme }) => theme.spacing / 2}px ${({ theme }) => theme.spacing}px;
  text-align: center;
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Day.tsx

ここでは選択中の日付は青丸で囲う、他の日付はイベントを仕込んでクリックされたら日付を更新する callback を実行するだけのコンポーネントになっている。
props はこのようになっており、選択中の日付とクリックされたら日付を更新する callback を実行する関数を受け取ることを想定している。

import { Dayjs } from "dayjs";
import { ReactNode } from "react";

type Props = {
  selected: boolean;
  value: Dayjs;
  onClickDate?: (newDate: Dayjs) => void;
  children: ReactNode;
};
1
2
3
4
5
6
7
8
9

実際のコードは本当にこれらをレンダリングするだけのシンプルなものになっている。

import { Dayjs } from "dayjs";
import { FC, ReactNode } from "react";
import { DayContainer } from "./styled";

type Props = {
  selected: boolean;
  value: Dayjs;
  onClickDate?: (newDate: Dayjs) => void;
  children: ReactNode;
};

export const Day: FC<Props> = ({ selected, value, onClickDate, children }) => (
  <DayContainer
    selected={selected}
    onClick={() => {
      onClickDate?.(value);
    }}
  >
    {children}
  </DayContainer>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

まとめ

カレンダー UI のライブラリは世の中に数多く存在するが、dayjs を使ってるものは少ないように感じた。
今回 dayjs を使って作ってみたのは moment.js との互換性を意識した結果であり、他のたとえば date-fns や luxon などを使っても同じようなことができると思う。
カレンダーの見た目に関して、個人的に難しそうと感じていたが作ってみたところ想像以上に容易で、少しの grid の知識と dayjs のドキュメントを読む力があれば簡単に作れるものだと感じたので、今回の目的である「本当にシンプルで最低限のものを作って温度感を確かめる」に関しては達成できたと思う。
このカレンダーに関してはもう少し拡張の余地があるので引き続き進めていく。