DEV Community

Rails Designer
Rails Designer

Posted on • Originally published at railsdesigner.com

3

Recurring Calendar Events in Rails

This article was originally published on Rails Designer


Last week I released v1.14 of Rails Designer's UI Components. With that release came a fully-customizable Calendar Component, built with ViewComponent and designed with Tailwind CSS.

Since that release I got two times the question via email about recurring events. Does that work? And indeed it does. The Calendar Component simply accepts an events array/collection. And while this kind of functionality is out-of-scope of a UI component (and the support for it), I am currently working on something that just happens to need this kind of feature. It is not at all too difficult to start (the tricky bits start when hundreds of thousands of events are created 😬). So what else can I do then to share how I would approach this in an article?

Image description

The repo for this article can be found here (it does not include the Calendar Component!). Be sure to run bin/rails db:seed!

The foundation of the recurring rules is done with ice_cube (there are various other gems out there, but this is the one I've used before and know well). It works by storing a JSON-serialized rule set in the Event model (in this example: recurring_rule and recurring_until for starters), which defines patterns like “every Monday” or “first day of month”. The gem then provides methods to expand these rules into actual occurrence dates and handles complex recurrence patterns including exceptions, specific weekdays, monthly/yearly rules, and rule combinations.

Let's add it: bundle add ice_cube.

Next create the Event model: rails g model Event title description:text start:datetime end:datetime recurring_rule:string recurring_until:datetime.

Simple enough. What I like is to have is an API like this: @events = Event.all.include_recurring that has sane defaults or when I want to override the default timeframe: @events = Event.all.include_recurring(within: 1.month.from..2.months.from_now). Looks pretty good, right?

It's done like this:

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
Enter fullscreen mode Exit fullscreen mode

Wow—intense! It's not too difficult really! The most interesting things happen in include_recurring. It looks at all the events and generates future occurrences for the ones that repeat. These occurrences are lightweight Event::Recurring objects that behave just like regular events but only exist in memory (they mirror the original event but with adjusted dates). The concern then combines the regular events with the generated occurrences and returns them all sorted by starts_at date. This gives you a complete list of all events, both one-off and recurring, without storing each occurrence in the database. Pretty awesome!

Don't forget to include in the event model:

class Event < ApplicationRecord
  include Recurrence
end
Enter fullscreen mode Exit fullscreen mode

And with that you have the basics for recurring events! Sweet!

Creating New Events

The repo with this article has the basics to list and create new (recurring) events. Most of it is basic Rails stuff, but I wanted to highlight the 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
Enter fullscreen mode Exit fullscreen mode

This concern handles the form-to-database conversion for recurring events. It adds virtual attributes for the form (recurring_type and recurring_until) and converts these into IceCube rules before saving. It uses the event's starts_at to determine which day to repeat on, so a weekly event starting on Tuesday will always repeat on Tuesdays.

Don't forget to include this concern in the Event model: include Recurrence::Builder.

Of course, as often, there are plenty of things you could add: exceptions (canceling/modifying single occurrences), multiple days per week (“Monday and Wednesday”) and editing future occurrences only. But this is a good foundation to start with.

Image of Datadog

Diagram Like A Pro

Bring your cloud architecture to life with expert tips from AWS and Datadog. In this ebook, AWS Solutions Architects Jason Mimick and James Wenzel reveal pro strategies for building clear, compelling diagrams that make an impact.

Get the Guide

Top comments (0)

ACI image

ACI.dev: Best Open-Source Composio Alternative (AI Agent Tooling)

100% open-source tool-use platform (backend, dev portal, integration library, SDK/MCP) that connects your AI agents to 600+ tools with multi-tenant auth, granular permissions, and access through direct function calling or a unified MCP server.

Star our GitHub!

👋 Kindness is contagious

Explore this insightful post in the vibrant DEV Community. Developers from all walks of life are invited to contribute and elevate our shared know-how.

A simple "thank you" could lift spirits—leave your kudos in the comments!

On DEV, passing on wisdom paves our way and unites us. Enjoyed this piece? A brief note of thanks to the writer goes a long way.

Okay