A custom date picker

I have tried several date-picker libraries, but I have never found anything that is easy enough to style the way I want it to. After endless hours of trying to work with the Ruby on Rails date_field, third-party libraries like Flatpicker (which haven't been updated in years), and ending up disappointed, I finally decided to create my own.

I figured I could achieve something similar: https://mhenrixon.com/articles/taggable-array and extend my existing form builder with a date select field.
lang-ruby
class TailwindFormBuilder < ActionView::Helpers::FormBuilder
  include ActionView::Helpers::TagHelper

  def custom_date_field(method, options = {}) # rubocop:disable Metrics/MethodLength
    default_options = {
      class: "input input-bordered input-primary w-full",
      data: {
        dates_target: "input",
      },
    }
    merged_options = default_options.merge(options)

    tag.div(
      class: "dropdown dropdown-hover w-full",
      data: {
        controller: "dates",
        dates_target: "dropdown",
      },
    ) do
      tag.div(class: "relative") do
        text_field(method, value: @object.send(method)&.to_fs(:db), **merged_options)
      end +
        tag.div(
          class: "dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-64",
          data: { dates_target: "calendar" },
        )
    end
  end
end

ActiveSupport.on_load(:action_view) do
  ActionView::Base.default_form_builder = TailwindFormBuilder
end
I decided to use the dayjs library due to its size and simplicity:
lang-js
import { Controller } from "@hotwired/stimulus"
import dayjs from "dayjs"
import customParseFormat from "dayjs/plugin/customParseFormat"
import weekday from "dayjs/plugin/weekday"

dayjs.extend(customParseFormat)
dayjs.extend(weekday)

export default class extends Controller {
  static targets = ["input", "calendar", "dropdown"]
  static values = {
    selectedDate: String,
  }

  connect() {
    this.selectedDateValue = dayjs(this.selectedDateValue || undefined).format('YYYY-MM-DD')
    this.renderCalendar()
    this.inputTarget.addEventListener("change", this.handleInputChange.bind(this))
  }

  handleInputChange(event) {
    const inputDate = dayjs(event.target.value, 'YYYY-MM-DD', true)
    if (inputDate.isValid()) {
      this.selectedDateValue = inputDate.format('YYYY-MM-DD')
      this.renderCalendar()
    } else {
      console.warn("Invalid date entered:", event.target.value)
    }
  }

  renderCalendar() {
    const selectedDate = dayjs(this.selectedDateValue)
    const firstDay = selectedDate.startOf('month')
    const lastDay = selectedDate.endOf('month')

    let calendarHTML = `
      <div class="flex justify-between items-center mb-4">
        <button data-action="click->dates#previousMonth" class="btn btn-sm btn-ghost">&lt;</button>
        <span class="text-lg font-semibold">${selectedDate.format('MMMM YYYY')}</span>
        <button data-action="click->dates#nextMonth" class="btn btn-sm btn-ghost">&gt;</button>
      </div>
      <div class="grid grid-cols-7 gap-1 text-center">
        ${['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(day => `<div class="text-xs font-medium text-gray-500">${day}</div>`).join("")}
    `

    for (let i = 0; i < firstDay.day(); i++) {
      calendarHTML += "<div></div>"
    }

    for (let day = 1; day <= lastDay.date(); day++) {
      const currentDate = selectedDate.date(day)
      const isSelected = currentDate.format('YYYY-MM-DD') === this.selectedDateValue
      calendarHTML += `
        <div>
          <button data-action="click->dates#selectDate"
                  data-date="${currentDate.format('YYYY-MM-DD')}"
                  class="w-8 h-8 rounded-full ${isSelected ? "bg-primary text-primary-content" : "hover:bg-base-200"}">${day}</button>
        </div>
      `
    }

    calendarHTML += "</div>"
    this.calendarTarget.innerHTML = calendarHTML
  }

  selectDate(event) {
    this.selectedDateValue = event.currentTarget.dataset.date
    this.inputTarget.value = this.selectedDateValue
    this.renderCalendar()
    this.closeCalendar()
  }

  previousMonth() {
    this.selectedDateValue = dayjs(this.selectedDateValue).subtract(1, 'month').format('YYYY-MM-DD')
    this.renderCalendar()
  }

  nextMonth() {
    this.selectedDateValue = dayjs(this.selectedDateValue).add(1, 'month').format('YYYY-MM-DD')
    this.renderCalendar()
  }

  toggleCalendar() {
    this.dropdownTarget.classList.toggle("dropdown-open")
  }

  closeCalendar() {
    this.dropdownTarget.classList.remove("dropdown-open")
  }
}
This is a little demo of it all:
CleanShot 2024-08-18 at 18.30.34.gif 1.07 MB
It is amazing what one can do with minimal effort these days. To try it out, we can get away with something super simple:
lang-ruby
div(
  class: "sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-neutral sm:pt-5"\
) do
  f.label :published_at, class: "sm:mt-px sm:pt-2"

  div(class: "mt-1 sm:mt-0 sm:col-span-2") do
    f.custom_date_field :published_at, tabindex: 3
  end
end