Calendar
A calendar that lets users pick a date, with keyboard navigation.
Usage
Connect to input
div(class: 'space-y-4') do
Input(placeholder: "Select a date", class: 'rounded-md border shadow', id: 'date', data_controller: 'ruby-ui--calendar-input')
Calendar(input_id: '#date', class: 'rounded-md border shadow')
end
Format date
Format dates with date-fns
div(class: 'space-y-4') do
Input(placeholder: "Select a date", class: 'rounded-md border shadow', id: 'formatted-date', data_controller: 'ruby-ui--calendar-input')
Calendar(input_id: '#formatted-date', date_format: 'PPPP', class: 'rounded-md border shadow')
end
Minimum date
Disable dates before a given date
div(class: 'space-y-4') do
Input(placeholder: "Select a date", class: 'rounded-md border shadow', id: 'minimum-date', data_controller: 'ruby-ui--calendar-input')
Calendar(input_id: '#minimum-date', min_date: Date.current, class: 'rounded-md border shadow')
end
Week start
Weeks start on Sunday by default; pass week_starts_on to change it
Calendar(week_starts_on: :monday, class: 'rounded-md border shadow')
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component Calendar
Manual installation
1
Add RubyUI::Calendar to app/components/ruby_ui/calendar/calendar.rb
# frozen_string_literal: true
module RubyUI
# An inline month calendar, styled after shadcn's Calendar (Base UI + vega).
# The days are rendered by the ruby-ui--calendar Stimulus controller from the
# templates in CalendarWeekdays and CalendarDays.
#
# week_starts_on: :sunday (default, as in shadcn) through :saturday, or 0-6.
class Calendar < Base
WEEKDAYS = %i[sunday monday tuesday wednesday thursday friday saturday].freeze
def initialize(selected_date: nil, min_date: nil, input_id: nil, date_format: "yyyy-MM-dd", week_starts_on: :sunday, **attrs)
@selected_date = selected_date
@min_date = min_date
@input_id = input_id
@date_format = date_format
@week_starts_on = self.class.week_start_index(week_starts_on)
super(**attrs)
end
def self.week_start_index(value)
index = value.is_a?(Integer) ? value : WEEKDAYS.index(value.to_s.to_sym)
raise ArgumentError, "week_starts_on must be one of #{WEEKDAYS.join(", ")} or 0-6, got #{value.inspect}" unless index&.between?(0, 6)
index
end
def view_template
div(**attrs) do
RubyUI.CalendarHeader do
nav(aria_label: "Navigation bar", class: "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1") do
RubyUI.CalendarPrev
RubyUI.CalendarNext
end
RubyUI.CalendarTitle
end
RubyUI.CalendarBody # Where the calendar is rendered (Weekdays and Days)
RubyUI.CalendarWeekdays(week_starts_on: @week_starts_on) # Template for the weekdays
RubyUI.CalendarDays # Template for the days
end
end
private
def default_attrs
{
class: "w-fit bg-background p-3 in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
data: {
controller: "ruby-ui--calendar",
ruby_ui__calendar_selected_date_value: @selected_date&.to_s,
ruby_ui__calendar_min_date_value: @min_date&.to_s,
ruby_ui__calendar_format_value: @date_format,
ruby_ui__calendar_week_starts_on_value: @week_starts_on,
ruby_ui__calendar_ruby_ui__calendar_input_outlet: @input_id
}
}
end
end
end
2
Add RubyUI::CalendarBody to app/components/ruby_ui/calendar/calendar_body.rb
# frozen_string_literal: true
module RubyUI
class CalendarBody < Base
def view_template
table(**attrs)
end
private
# The controller fills the grid and sets aria-label to the month and year.
def default_attrs
{
role: "grid",
class: "w-full border-collapse",
data: {
ruby_ui__calendar_target: "calendar",
action: "keydown->ruby-ui--calendar#navigate"
}
}
end
end
end
3
Add RubyUI::CalendarDays to app/components/ruby_ui/calendar/calendar_days.rb
# frozen_string_literal: true
module RubyUI
# Mustache templates the ruby-ui--calendar controller renders each day with.
# Placeholders: {{date}} (yyyy-mm-dd), {{dayDate}} (day of month),
# {{label}} (full date, e.g. "Monday, March 16th, 2026") and {{tabindex}}
# (0 on the one day in the Tab sequence, -1 elsewhere).
class CalendarDays < Base
BASE_CLASS = "inline-flex size-8 items-center justify-center rounded-md border-0 border-transparent p-0 text-sm leading-none font-normal whitespace-nowrap " \
"transition-all outline-none select-none focus-visible:relative focus-visible:z-10 " \
"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50"
HOVER_CLASS = "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50"
CELL_CLASS = "relative rounded-md p-0 text-center select-none"
def view_template
render_disabled_date_template
render_selected_date_template
render_today_date_template
render_current_month_date_template
render_other_month_date_template
end
private
def render_disabled_date_template
date_template("disabledDateTemplate") do
day_button([BASE_CLASS, "text-muted-foreground"], disabled: true, tabindex: "-1", data: {action: nil})
end
end
def render_selected_date_template
date_template("selectedDateTemplate", aria_selected: "true") do
day_button([BASE_CLASS, "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground"])
end
end
def render_today_date_template
date_template("todayDateTemplate", cell_class: [CELL_CLASS, "rounded-md bg-muted text-foreground"]) do
day_button([BASE_CLASS, "text-foreground", HOVER_CLASS])
end
end
def render_current_month_date_template
date_template("currentMonthDateTemplate") do
day_button([BASE_CLASS, "text-foreground", HOVER_CLASS])
end
end
def render_other_month_date_template
date_template("otherMonthDateTemplate") do
day_button([BASE_CLASS, "text-muted-foreground", HOVER_CLASS])
end
end
def day_button(classes, tabindex: "{{tabindex}}", **options)
data = {day: "{{date}}", action: "click->ruby-ui--calendar#selectDay"}.merge(options.delete(:data) || {}).compact
button(
type: "button",
name: "day",
class: classes,
tabindex: tabindex,
aria_label: "{{label}}",
data: data,
**options
) { "{{dayDate}}" }
end
def date_template(target, cell_class: CELL_CLASS, **cell_attrs, &block)
template(data: {ruby_ui__calendar_target: target}) do
td(class: cell_class, role: "gridcell", **cell_attrs, &block)
end
end
def default_attrs
{}
end
end
end
4
Add RubyUI::CalendarDocs to app/components/ruby_ui/calendar/calendar_docs.rb
# frozen_string_literal: true
class Views::Docs::Calendar < Views::Base
def view_template
component = "Calendar"
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Calendar", description: "A calendar that lets users pick a date, with keyboard navigation.")
Heading(level: 2) { "Usage" }
render Docs::VisualCodeExample.new(title: "Connect to input", context: self) do
<<~RUBY
div(class: 'space-y-4') do
Input(placeholder: "Select a date", class: 'rounded-md border shadow', id: 'date', data_controller: 'ruby-ui--calendar-input')
Calendar(input_id: '#date', class: 'rounded-md border shadow')
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Format date", description: "Format dates with date-fns", context: self) do
<<~RUBY
div(class: 'space-y-4') do
Input(placeholder: "Select a date", class: 'rounded-md border shadow', id: 'formatted-date', data_controller: 'ruby-ui--calendar-input')
Calendar(input_id: '#formatted-date', date_format: 'PPPP', class: 'rounded-md border shadow')
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Minimum date", description: "Disable dates before a given date", context: self) do
<<~RUBY
div(class: 'space-y-4') do
Input(placeholder: "Select a date", class: 'rounded-md border shadow', id: 'minimum-date', data_controller: 'ruby-ui--calendar-input')
Calendar(input_id: '#minimum-date', min_date: Date.current, class: 'rounded-md border shadow')
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Week start", description: "Weeks start on Sunday by default; pass week_starts_on to change it", context: self) do
<<~RUBY
Calendar(week_starts_on: :monday, class: 'rounded-md border shadow')
RUBY
end
render Components::ComponentSetup::Tabs.new(component_name: component)
render Docs::ComponentsTable.new(component_files(component))
end
end
end
5
Add RubyUI::CalendarHeader to app/components/ruby_ui/calendar/calendar_header.rb
# frozen_string_literal: true
module RubyUI
class CalendarHeader < Base
def view_template(&)
div(**attrs, &)
end
private
def default_attrs
{
class: "relative mb-4 flex h-8 w-full items-center justify-center px-8"
}
end
end
end
6
Add RubyUI::CalendarNext to app/components/ruby_ui/calendar/calendar_next.rb
# frozen_string_literal: true
module RubyUI
class CalendarNext < Base
def view_template
button(**attrs) do
icon
end
end
private
# lucide chevron, as in shadcn.
def icon
svg(
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewbox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
stroke_linecap: "round",
stroke_linejoin: "round",
class: "size-4",
aria_hidden: "true"
) do |s|
s.path(d: "m9 18 6-6-6-6")
end
end
# shadcn's ghost Button at the calendar cell size.
def default_attrs
{
name: "next-month",
aria_label: "Go to the Next Month",
class: CalendarPrev::NAV_CLASS,
type: "button",
data_action: "click->ruby-ui--calendar#nextMonth"
}
end
end
end
7
Add RubyUI::CalendarPrev to app/components/ruby_ui/calendar/calendar_prev.rb
# frozen_string_literal: true
module RubyUI
class CalendarPrev < Base
NAV_CLASS = "group/button inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-transparent " \
"bg-clip-padding p-0 text-sm font-medium whitespace-nowrap transition-all outline-none select-none " \
"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px " \
"disabled:pointer-events-none disabled:opacity-50 aria-disabled:opacity-50 " \
"hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 [&_svg]:pointer-events-none [&_svg]:shrink-0"
def view_template
button(**attrs) do
icon
end
end
private
# lucide chevron, as in shadcn.
def icon
svg(
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewbox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
stroke_linecap: "round",
stroke_linejoin: "round",
class: "size-4",
aria_hidden: "true"
) do |s|
s.path(d: "m15 18-6-6 6-6")
end
end
# shadcn's ghost Button at the calendar cell size.
def default_attrs
{
name: "previous-month",
aria_label: "Go to the Previous Month",
class: NAV_CLASS,
type: "button",
data_action: "click->ruby-ui--calendar#prevMonth"
}
end
end
end
8
Add RubyUI::CalendarTitle to app/components/ruby_ui/calendar/calendar_title.rb
# frozen_string_literal: true
module RubyUI
class CalendarTitle < Base
def initialize(default: "Month Year", **attrs)
@default = default
super(**attrs)
end
def view_template
div(**attrs) { @default }
end
private
def default_attrs
{
class: "text-sm font-medium select-none",
role: "status",
aria_live: "polite",
data: {
ruby_ui__calendar_target: "title"
}
}
end
end
end
9
Add RubyUI::CalendarWeekdays to app/components/ruby_ui/calendar/calendar_weekdays.rb
# frozen_string_literal: true
module RubyUI
class CalendarWeekdays < Base
DAYS = %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday].freeze
def initialize(week_starts_on: 0, **attrs)
@week_starts_on = RubyUI::Calendar.week_start_index(week_starts_on)
super(**attrs)
end
def view_template
template(data: {ruby_ui__calendar_target: "weekdaysTemplate"}) do
# As in shadcn, the column headers are hidden: each day button carries
# its full date as its accessible name.
thead(**attrs) do
tr(class: "flex") do
DAYS.rotate(@week_starts_on).each do |day|
render_day(day)
end
end
end
end
end
private
def render_day(day)
th(
scope: "col",
class: "w-8 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none",
aria_label: day
) { day[0..1] }
end
def default_attrs
{aria_hidden: "true"}
end
end
end
10
Add calendar_controller.js to app/javascript/controllers/ruby_ui/calendar_controller.js
import { Controller } from "@hotwired/stimulus";
import Mustache from "mustache";
// Renders the month grid from the templates in CalendarWeekdays and
// CalendarDays. Keyboard support follows the APG date picker grid:
// one day is in the Tab sequence; arrows move by day/week, Home/End to the
// start/end of the week, PageUp/PageDown by month (Shift: by year).
export default class extends Controller {
static targets = [
"calendar",
"title",
"weekdaysTemplate",
"disabledDateTemplate",
"selectedDateTemplate",
"todayDateTemplate",
"currentMonthDateTemplate",
"otherMonthDateTemplate",
];
static values = {
selectedDate: {
type: String,
default: null,
},
minDate: {
type: String,
default: null,
},
viewDate: {
type: String,
default: new Date().toISOString().slice(0, 10),
},
format: {
type: String,
default: "yyyy-MM-dd", // Default format
},
weekStartsOn: {
type: Number,
default: 0, // Sunday
},
};
static outlets = ["ruby-ui--calendar-input"];
initialize() {
this.focusedDate = null;
this.shouldFocus = false;
// Nothing selected and today before min_date: open on the min_date month,
// where days can be picked, instead of a month that is all disabled.
const minDate = this.minDate();
if (
!this.selectedDate() &&
minDate &&
this.startOfDay(new Date()) < this.startOfDay(minDate)
) {
this.viewDateValue = this.toISODate(
new Date(minDate.getFullYear(), minDate.getMonth(), 2),
);
}
this.updateCalendar(); // Initial calendar render
}
nextMonth(e) {
e.preventDefault();
this.viewDateValue = this.adjustMonth(1);
}
prevMonth(e) {
e.preventDefault();
this.viewDateValue = this.adjustMonth(-1);
}
selectDay(e) {
e.preventDefault();
const date = this.parseDate(e.currentTarget.dataset.day);
if (!date || this.isDateDisabled(date)) return;
// Selecting the selected day again: make it the Tab stop again and write
// the date back into the linked input, which may have been edited since.
const value = this.toISODate(date);
if (value === this.selectedDateValue) {
this.focusedDate = date;
this.shouldFocus = true;
this.updateCalendar();
this.syncInputs(date);
return;
}
this.focusedDate = date;
this.shouldFocus = true;
this.selectedDateValue = value;
}
navigate(e) {
const button = e.target.closest("button[data-day]");
if (!button) return;
const current = this.parseDate(button.dataset.day);
const offset = this.weekdayOffset(current);
let step = 0;
let next;
switch (e.key) {
case "ArrowLeft":
step = -1;
break;
case "ArrowRight":
step = 1;
break;
case "ArrowUp":
step = -7;
break;
case "ArrowDown":
step = 7;
break;
case "Home":
next = this.addDays(current, -offset);
break;
case "End":
next = this.addDays(current, 6 - offset);
break;
case "PageUp":
next = this.addMonths(current, e.shiftKey ? -12 : -1);
break;
case "PageDown":
next = this.addMonths(current, e.shiftKey ? 12 : 1);
break;
default:
return;
}
e.preventDefault();
if (step !== 0) {
next = this.addDays(current, step);
// Skip disabled days, as react-day-picker does.
for (let i = 0; i < 366 && this.isDateDisabled(next); i++) {
next = this.addDays(next, step);
}
if (this.isDateDisabled(next)) return;
} else {
// Home/End/PageUp/PageDown landing on a disabled day: walk back toward
// the current (enabled) day until an enabled one, so focus never lands
// on a month with no focusable day.
const back = next < current ? 1 : -1;
while (this.isDateDisabled(next) && !this.sameDay(next, current)) {
next = this.addDays(next, back);
}
}
this.focusDay(next);
}
focusDay(date) {
this.focusedDate = date;
this.shouldFocus = true;
const viewDate = this.toISODate(
new Date(date.getFullYear(), date.getMonth(), 2),
);
if (viewDate === this.viewDateValue) {
this.updateCalendar();
} else {
this.viewDateValue = viewDate; // triggers updateCalendar()
}
}
selectedDateValueChanged(value, prevValue) {
const selectedDate = this.selectedDate();
if (!selectedDate) {
this.updateCalendar();
return;
}
// update the viewDateValue to the month of the selected date (This will trigger updateCalendar() function)
this.viewDateValue = this.toISODate(
new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 2),
);
// Re-render the calendar
this.updateCalendar();
this.syncInputs(selectedDate);
}
// Write the date into the linked inputs (input_id).
syncInputs(date) {
const formattedDate = this.formatDate(date);
this.rubyUiCalendarInputOutlets.forEach((outlet) => {
outlet.setValue(formattedDate);
});
}
viewDateValueChanged(value, prevValue) {
this.updateCalendar();
}
adjustMonth(adjustment) {
const date = this.viewDate();
return this.toISODate(
new Date(date.getFullYear(), date.getMonth() + adjustment, 2),
);
}
updateCalendar() {
if (!this.hasCalendarTarget) return;
// Re-rendering replaces the day buttons: keep focus in the grid if it was there.
const keepFocus =
this.shouldFocus || this.calendarTarget.contains(document.activeElement);
// Update the title with month and year
const title = this.monthAndYear();
this.titleTarget.textContent = title;
this.calendarTarget.setAttribute("aria-label", title);
this.calendarTarget.innerHTML = this.calendarHTML();
if (keepFocus) {
this.calendarTarget.querySelector('button[tabindex="0"]')?.focus();
}
this.shouldFocus = false;
}
calendarHTML() {
return this.weekdaysTemplateTarget.innerHTML + this.calendarDays();
}
calendarDays() {
const tabbable = this.tabbableDate();
return `<tbody>${this.weeks()
.map((week) => this.renderWeek(week, tabbable))
.join("")}</tbody>`;
}
renderWeek(week, tabbable) {
const days = week.map((day) => this.renderDay(day, tabbable)).join("");
return `<tr class="flex w-full mt-2">${days}</tr>`;
}
renderDay(day, tabbable) {
const selectedDate = this.selectedDate();
const isSelected = this.sameDay(day, selectedDate);
const isToday = this.sameDay(day, new Date());
const data = {
date: this.toISODate(day),
dayDate: day.getDate(),
label: this.dayLabel(day, { isSelected, isToday }),
tabindex: this.sameDay(day, tabbable) ? "0" : "-1",
};
let template;
if (this.isDateDisabled(day)) {
template = this.disabledDateTemplateTarget;
} else if (isSelected) {
template = this.selectedDateTemplateTarget;
} else if (isToday) {
template = this.todayDateTemplateTarget;
} else if (day.getMonth() === this.viewDate().getMonth()) {
template = this.currentMonthDateTemplateTarget;
} else {
template = this.otherMonthDateTemplateTarget;
}
return Mustache.render(template.innerHTML, data);
}
// The one day in the Tab sequence: the focused day, else the selected day,
// else today, else the first enabled day of the month on view, else the
// first enabled day anywhere in the rendered grid (outside days included).
tabbableDate() {
const view = this.viewDate();
const inView = (date) =>
date &&
date.getFullYear() === view.getFullYear() &&
date.getMonth() === view.getMonth() &&
!this.isDateDisabled(date);
const candidate = [this.focusedDate, this.selectedDate(), new Date()].find(
inView,
);
if (candidate) return candidate;
const lastDay = new Date(view.getFullYear(), view.getMonth() + 1, 0);
for (let day = 1; day <= lastDay.getDate(); day++) {
const date = new Date(view.getFullYear(), view.getMonth(), day);
if (!this.isDateDisabled(date)) return date;
}
return (
this.weeks()
.flat()
.find((date) => !this.isDateDisabled(date)) || null
);
}
// Full weeks covering the month on view, starting on weekStartsOn.
weeks() {
const view = this.viewDate();
const year = view.getFullYear();
const month = view.getMonth();
const first = new Date(year, month, 1);
const last = new Date(year, month + 1, 0);
const start = 1 - this.weekdayOffset(first);
const weeks = [];
for (let day = start; new Date(year, month, day) <= last; day += 7) {
weeks.push(
Array.from({ length: 7 }, (_, i) => new Date(year, month, day + i)),
);
}
return weeks;
}
// Days since the start of the week (0-6).
weekdayOffset(date) {
return (date.getDay() - this.weekStartsOnValue + 7) % 7;
}
// "Monday, March 16th, 2026", as react-day-picker labels its day buttons.
dayLabel(date, { isSelected, isToday }) {
const weekday = date.toLocaleString("en-US", { weekday: "long" });
const month = date.toLocaleString("en-US", { month: "long" });
const day = date.getDate();
let label = `${weekday}, ${month} ${day}${this.getDaySuffix(day)}, ${date.getFullYear()}`;
if (isToday) label = `Today, ${label}`;
if (isSelected) label = `${label}, selected`;
return label;
}
monthAndYear() {
const month = this.viewDate().toLocaleString("en-US", { month: "long" });
const year = this.viewDate().getFullYear();
return `${month} ${year}`;
}
selectedDate() {
return this.parseDate(this.selectedDateValue);
}
viewDate() {
return (
this.parseDate(this.viewDateValue) || this.selectedDate() || new Date()
);
}
addDays(date, days) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
}
// Same day in another month, clamped to that month's last day.
addMonths(date, months) {
const lastDay = new Date(
date.getFullYear(),
date.getMonth() + months + 1,
0,
).getDate();
return new Date(
date.getFullYear(),
date.getMonth() + months,
Math.min(date.getDate(), lastDay),
);
}
sameDay(a, b) {
return !!(a && b && a.toDateString() === b.toDateString());
}
toISODate(date) {
const pad = (n) => String(n).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
formatDate(date) {
const format = this.formatValue;
const day = date.getDate();
const month = date.getMonth() + 1;
const year = date.getFullYear();
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
const dayOfWeek = date.toLocaleString("en-US", { weekday: "long" });
const monthName = date.toLocaleString("en-US", { month: "long" });
const daySuffix = this.getDaySuffix(day);
const map = {
yyyy: year,
MM: ("0" + month).slice(-2),
dd: ("0" + day).slice(-2),
HH: ("0" + hours).slice(-2),
mm: ("0" + minutes).slice(-2),
ss: ("0" + seconds).slice(-2),
EEEE: dayOfWeek,
MMMM: monthName,
do: day + daySuffix,
PPPP: `${dayOfWeek}, ${monthName} ${day}${daySuffix}, ${year}`,
};
// Longer tokens first: with MM before MMMM, "MMMM" came out as "0303".
const formattedDate = format.replace(
/yyyy|MMMM|EEEE|PPPP|MM|dd|do|HH|mm|ss/g,
(matched) => map[matched],
);
return formattedDate;
}
getDaySuffix(day) {
if (day > 3 && day < 21) return "th";
switch (day % 10) {
case 1:
return "st";
case 2:
return "nd";
case 3:
return "rd";
default:
return "th";
}
}
minDate() {
return this.parseDate(this.minDateValue);
}
isDateDisabled(date) {
const minDate = this.minDate();
const candidate = this.parseDate(date);
if (!minDate || !candidate) return false;
return this.startOfDay(candidate) < this.startOfDay(minDate);
}
parseDate(value) {
if (!value) return null;
if (value instanceof Date) return new Date(value);
const isoDate = value.toString().match(/^(\d{4})-(\d{2})-(\d{2})/);
if (isoDate) {
return new Date(
Number(isoDate[1]),
Number(isoDate[2]) - 1,
Number(isoDate[3]),
);
}
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
startOfDay(date) {
const normalizedDate = new Date(date);
normalizedDate.setHours(0, 0, 0, 0);
return normalizedDate;
}
}
11
Add calendar_input_controller.js to app/javascript/controllers/ruby_ui/calendar_input_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="input"
export default class extends Controller {
setValue(value) {
this.element.value = value
}
}
12
Update the Stimulus controllers manifest file
rake stimulus:manifest:update13
Install mustache Javascript dependency
// with yarn
yarn add mustache
// with npm
npm install mustache
// with importmaps
bin/importmap pin mustache
Components
| Component | Built using | Source |
|---|---|---|
Calendar | Phlex | |
CalendarBody | Phlex | |
CalendarDays | Phlex | |
CalendarDocs | Phlex | |
CalendarHeader | Phlex | |
CalendarNext | Phlex | |
CalendarPrev | Phlex | |
CalendarTitle | Phlex | |
CalendarWeekdays | Phlex | |
CalendarController | Stimulus JS | |
CalendarInputController | Stimulus JS |