Rails: カレンダーの「繰り返しイベント」を実装しよう(翻訳)
先週、Rails DesignerのUI Componentsのv1.14をリリースしました。このリリースから、完全にカスタマイズ可能なカレンダーコンポーネントを搭載ししました。構築にはViewComponentを、画面デザインにはTailwind CSSを使っています。
リリーズ後に、「これは本当に使えるのですか?」という問い合わせメールを2件いただきました。はい、もちろん使えます。
このカレンダーコンポーネントはevents
という配列/コレクションを渡せるようになっているだけであり、この種の機能はUIコンポーネントの範疇(とサポート範囲)には入らないものですが、たまたま私はこの機能が必要となる作業に取り組んでいるところです。
カレンダーコンポーネントを使うことについては何も難しくありません(作成イベント件数が数十万件になれば少々難しくなりますが😬)。本記事でカレンダーコンポーネントの使い方を共有するうえで、何か他にできることはあるでしょうか?
本記事に対応するリポジトリは以下です(ただし有料のカレンダーコンポーネントは含まれていません!)。必ずbin/rails db:seed
を実行してからお使いください。
繰り返しルールの基本部分はice_cubeというgemで構築しています(この種のgemはいろいろありますが、私が使い慣れているのはこれです)。このgemは、JSONシリアライズされたルールセットをEventモデル内に保存することで動作します(この例ではrecurring_rule
やrecurring_until
)。Eventモデルでは、「every Monday(毎週月曜日)」や「first day of month(月の最初の日)」といったパターンを定義します。
ice_cube gemが提供するメソッドは、これらのルールを実際の日付の繰り返しに拡張したり、「例外」「特定の曜日」「月ごと/年ごと」といったルールや、そうしたルールの組み合わせによる複雑な繰り返しパターンを処理します。
それでは、bundle add ice_cube
を実行してice_cube gemを追加しましょう。
次に、以下を実行してEventモデルを作成します。
rails g model Event title description:text start:datetime end:datetime recurring_rule:string recurring_until:datetime
簡単ですね。
私が気に入っているのは、@events = Event.all.include_recurring
のようなAPIです。これには適切なデフォルト値が設定されていて、デフォルトのタイムフレームをオーバーライドしたい場合は@events = Event.all.include_recurring(within: 1.month.from..2.months.from_now)
のように書けます。なかなか良さそうですよね?
処理は以下のRecurrence
concernで行います。
module Event::Recurrence
extend ActiveSupport::Concern
included do
serialize :recurring_rule, coder: JSON
end
class_methods do
def include_recurring(within: Time.current..6.months.from_now)
events = all.to_a
recurring_events = events.select(&:recurring_rule).flat_map do |event|
event.schedule.occurrences_between(within.begin, within.end).map do |date|
next if date == event.starts_at
Event::Recurring.new(
event,
starts_at: date,
ends_at: date + (event.ends_at - event.starts_at)
)
end
end.compact
(events + recurring_events).sort_by(&:starts_at)
end
end
def schedule
@schedule ||= IceCube::Schedule.new(starts_at) do |schedule|
schedule.add_recurrence_rule(IceCube::Rule.from_hash(JSON.parse(recurring_rule))) if recurring_rule
end
end
class Event::Recurring
include ActiveModel::Model
delegate :title, :description, :recurring_rule, :schedule, :to_param, to: :@event
attr_reader :starts_at, :ends_at
def initialize(event, starts_at:, ends_at:)
@event = event
@starts_at = starts_at
@ends_at = ends_at
end
def persisted? = false
end
end
何だか凄そうに見えますが、そんなに難しくありません!
最も興味深い部分はinclude_recurring
メソッドで、ここではすべてのイベントをチェックしてから、今後繰り返されるイベントを生成します。イベントの定期的な繰り返しは軽量なEvent::Recurring
オブジェクトで表現します。これは通常のイベントと同様に振る舞いますが、メモリ上にのみ存在する点が異なります(元のイベントをミラーリングして日付を調整しています)。このconcernでは、通常のイベントと生成イベントを組み合わせた結果をstarts_at
でソートして返します。こうすることで、繰り返しをデータベースに保存せずに、1回きりのイベントと繰り返しイベントの両方を含むすべてのイベントリストを得られるようになります。素晴らしいですね!
このRecurrence
concernを忘れずにEventモデルにinclude
しましょう。
class Event < ApplicationRecord
include Recurrence
end
これで繰り返しイベントの基本部分ができました!
🔗 新規イベントを作成する
本記事で使っているrecurring-eventsリポジトリには、新規の(繰り返し)イベントをリスト表示/作成する基本部分が既に含まれています。ほとんどはRailsの基本的なコードですが、ここでBuilder
concernに触れておきたいと思います。
# app/models/event/recurrence/builder.rb
module Event::Recurrence::Builder
extend ActiveSupport::Concern
included do
attr_accessor :recurring_type, :recurring_until
before_save :set_recurring_rule
end
private
def set_recurring_rule
return if recurring_type.blank?
rule = case recurring_type
when "daily" then IceCube::Rule.daily
when "weekly" then IceCube::Rule.weekly.day(starts_at.wday)
when "biweekly" then IceCube::Rule.weekly(2).day(starts_at.wday)
when "monthly" then IceCube::Rule.monthly.day_of_month(starts_at.day)
end
rule = rule.until(recurring_until) if recurring_until.present?
self.recurring_rule = rule.to_hash.to_json
end
end
このBuilder
concernは、繰り返しイベントをフォーム形式からデータベースに変換する処理を担当しています。フォームに仮想属性(recurring_type
とrecurring_until
)を追加して、それらをIceCube
のルールに変換してから保存します。どの日付から繰り返すかは、イベントのstarts_at
で決定されます。これで、火曜日から開始される週イベントは常に火曜日に繰り返されるようになります。
このBuilder
concernもEvent
モデルにinclude
することをお忘れなく。
include Recurrence::Builder
もちろん、例外の扱い(特定の繰り返しをキャンセル・変更するなど)や、曜日を複数指定する(「月曜と木曜」など)、以後の繰り返しだけを編集する、といったよくある機能も追加できます。これは、そうした機能を追加する基盤としても適しています。
概要
元サイトの許諾を得て翻訳・公開いたします。