Reactで作ってるWebサービスにFullcalendarなどライブラリを使わずスクラッチでカレンダーを作ったので実装書いてみる。
前にVueでカレンダー作ったときみたいにフロントエンド側でゴリゴリ書いてこうと思ったんだけどちょっと今回は要件的にその実装方針だと辛かったんでロジックをバックエンドに寄せたらフロントで頑張るよりは書くの楽だった。
前にVueで実装したもの
バックエンドのロジック
今回はRailsで作った
週表示など月表示以外もいろんなパターンの実装作るんで共通処理を切り出すモジュールを用意
# カレンダー描画用json生成に使うモジュール module CalendarContent BIG_MOON = [1, 3, 5, 7, 8, 10, 12] WEEK_DAYS = 7 def last_month(month) month == 1 ? 12 : month - 1 end def next_month(month) month == 12 ? 1 : month + 1 end def display_last_year(year, month) (month == 1) ? (year - 1) : year end def month_end_date(month) BIG_MOON.include?(month) ? 31 : month === 2 ? 28 : 30 end # 指定した年、年の最終日の曜日 def month_end_wday(year, month) end_date = month_end_date(month) Date.new(year, month, end_date).wday end # 引数で指定した月の初日の曜日 def month_first_wday(year, month) Date.new(year, month, 1).wday end # 引数で指定した月の最終日の曜日 def month_end_wday(year, month) end_date = month_end_date(month) Date.new(year, month, end_date).wday end end
上記のモジュールを読み込んでカレンダー描画用のJSON生成。日付描画の部分だけ書いたけど実際はJSONの中に表示テキストやURLなど埋め込んでいく。
で、その時複数モデル絡んでくるのでどこか特定のモデルに書くのは違和感があったのでServiceクラスに切り出して書いてみた。
include CalendarContent class MonthCalendarService def initialize(year, month) @target_year = year @target_month = month end WEEK_DAYS = 7 SUN = 0 MON = 1 TUE = 2 WED = 3 THU = 4 FRI = 5 SAT = 6 # 日曜始まり月表示カレンダーの行数 def week_count(year, month) if month == 1 last_month = 12 last_year = year - 1 else last_month = month - 1 last_year = year end ((month_first_wday(year, month) + month_end_date(month)).quo(WEEK_DAYS).to_f).ceil end # [{date: 27, text: '', url: ''},{date: 28, text: '', url: ''}] def content_json result = [] # カレンダー行数 week_count_num = week_count(@target_year, @target_month) # 表示月の最終日 current_month_end_date = month_end_date(@target_month) # 表示される先月の年と月 display_last_year = display_last_year(@target_year, @target_month) last_month = last_month(@target_month) # 表示される最終日付と曜日 display_last_month_end_date = month_end_date(last_month) display_last_month_end_date_wday = month_end_wday(display_last_year, last_month) # 先月の最終日時が土曜の場合は先月の日付表示不要 if display_last_month_end_date_wday == SAT display_last_month_start_date = nil else display_last_month_start_date = display_last_month_end_date - display_last_month_end_date_wday end current_date = 0 week_count_num.times do |row| week_days_array = [] # カレンダー1行目 # 前月を表示する場合 if row == 0 && display_last_month_start_date.present? (display_last_month_start_date..display_last_month_end_date).each do |num| week_days_array.push({date: num}) end (WEEK_DAYS - display_last_month_end_date_wday - 1).times do |num| current_date = num + 1 week_days_array.push({date: current_date}) end result.push(week_days_array) else WEEK_DAYS.times do |num| if current_date == current_month_end_date current_date = 1 else current_date = current_date + 1 end week_days_array.push({date: current_date}) end result.push(week_days_array) end end result end end
上記のcontent_jsonメソッドを実行すると以下のようなJSONが生成される(実際はdate以外のkeyもあり
[ [ { "date": 26 }, { "date": 27 }, { "date": 28 }, { "date": 29 }, { "date": 30 }, { "date": 1 }, { "date": 2 } ], [ { "date": 3 }, { "date": 4 }, { "date": 5 }, { "date": 6 }, { "date": 7 }, { "date": 8 }, { "date": 9 } ], [ { "date": 10 }, { "date": 11 }, { "date": 12 }, { "date": 13 }, { "date": 14 }, { "date": 15 }, { "date": 16 } ], [ { "date": 17 }, { "date": 18 }, { "date": 19 }, { "date": 20 }, { "date": 21 }, { "date": 22 }, { "date": 23 } ], [ { "date": 24 }, { "date": 25 }, { "date": 26 }, { "date": 27 }, { "date": 28 }, { "date": 29 }, { "date": 30 } ], [ { "date": 31 }, { "date": 1 }, { "date": 2 }, { "date": 3 }, { "date": 4 }, { "date": 5 }, { "date": 6 } ] ]
あとはこれをReactに渡してカレンダーを作る
interface
export interface MonthCalendarContentJson { date: number }
state
const [calendarContentArray, setCalendarContentArray] = useState<Array<Array<MonthCalendarContentJson>>>([[]])
カレンダー表示部分
<table className={calendarStyles.calendar}> <thead> <tr> <th>日</th> <th>月</th> <th>火</th> <th>水</th> <th>木</th> <th>金</th> <th>土</th> </tr> </thead> {calendarContentArray.map((array, i) => { return ( <tbody key={i}> <tr> {array.map((a, i) => { return ( <td key={i}> {a.date} </td> ) })} </tr> </tbody> ) })} </table>
できたもの。CSSでtdにpaddingを追加している