Skip to content

Instantly share code, notes, and snippets.

@humphreyja
Created January 5, 2021 00:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save humphreyja/6d87a1cbbbb29c8be3c54924b45b3c2a to your computer and use it in GitHub Desktop.
Save humphreyja/6d87a1cbbbb29c8be3c54924b45b3c2a to your computer and use it in GitHub Desktop.
Hotwire Server Rendered Popup Date Picker
<% frame_name = 'calendar_interface' %>
<% frame_name = "#{frame_name}_#{@target}" if @target %>
<% start_of_month = @display_date.beginning_of_month %>
<% cache "ui/calendar/#{frame_name}/#{start_of_month}--#{@selected_date}" do %>
<% end_of_month = @display_date.end_of_month %>
<% dow_start = @display_date.beginning_of_month.days_to_week_start + 2 %>
<% dow_start = dow_start >= 7 ? dow_start - 7 : dow_start %>
<% dow_start = dow_start <= 0 ? dow_start + 7 : dow_start %>
<% today = Date.today %>
<%= turbo_frame_tag frame_name do %>
<div class="calendar">
<header>
<span class="flex row align-items-center">
<%= link_to calendar_interface_path(selected: @selected_date, show: @display_date - 1.month, target: @target), 'data-turbo-frame': 'calendar_interface', 'data-popup-keepalive': 'true', 'aria-label': "previous month" do %>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="arrow-circle-left" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-arrow-circle-left fa-w-16 fa-5x"><path fill="currentColor" d="M256 504C119 504 8 393 8 256S119 8 256 8s248 111 248 248-111 248-248 248zm28.9-143.6L209.4 288H392c13.3 0 24-10.7 24-24v-16c0-13.3-10.7-24-24-24H209.4l75.5-72.4c9.7-9.3 9.9-24.8.4-34.3l-11-10.9c-9.4-9.4-24.6-9.4-33.9 0L107.7 239c-9.4 9.4-9.4 24.6 0 33.9l132.7 132.7c9.4 9.4 24.6 9.4 33.9 0l11-10.9c9.5-9.5 9.3-25-.4-34.3z" class=""></path></svg>
<% end %>
<% if start_of_month != today.beginning_of_month %>
<%= link_to calendar_interface_path(selected: @selected_date, show: today, target: @target), 'data-turbo-frame': 'calendar_interface', 'data-popup-keepalive': 'true', 'aria-label': "today" do %>
<svg style="height:16px" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="share" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-share fa-w-16 fa-5x"><path fill="currentColor" d="M503.691 189.836L327.687 37.851C312.281 24.546 288 35.347 288 56.015v80.053C127.371 137.907 0 170.1 0 322.326c0 61.441 39.581 122.309 83.333 154.132 13.653 9.931 33.111-2.533 28.077-18.631C66.066 312.814 132.917 274.316 288 272.085V360c0 20.7 24.3 31.453 39.687 18.164l176.004-152c11.071-9.562 11.086-26.753 0-36.328z" class=""></path></svg>
<% end %>
<% else %>
<span style="width:24px"></span>
<% end %>
</span>
<h4>
<%= @display_date.strftime("%B %Y") %>
</h4>
<span class="flex row align-items-center">
<span style="width:24px"></span>
<%= link_to calendar_interface_path(selected: @selected_date, show: @display_date + 1.month, target: @target), 'data-turbo-frame': 'calendar_interface', 'data-popup-keepalive': 'true', 'aria-label': "next month" do %>
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="arrow-circle-right" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-arrow-circle-right fa-w-16 fa-5x"><path fill="currentColor" d="M256 8c137 0 248 111 248 248S393 504 256 504 8 393 8 256 119 8 256 8zm-28.9 143.6l75.5 72.4H120c-13.3 0-24 10.7-24 24v16c0 13.3 10.7 24 24 24h182.6l-75.5 72.4c-9.7 9.3-9.9 24.8-.4 34.3l11 10.9c9.4 9.4 24.6 9.4 33.9 0L404.3 273c9.4-9.4 9.4-24.6 0-33.9L271.6 106.3c-9.4-9.4-24.6-9.4-33.9 0l-11 10.9c-9.5 9.6-9.3 25.1.4 34.4z" class=""></path></svg>
<% end %>
</span>
</header>
<header>
<ol>
<% ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map do |day_name| %>
<li>
<%= day_name %>
</li>
<% end %>
</ol>
</header>
<ol>
<% current_day = start_of_month - dow_start.days %>
<% 6.times do |week_num| %>
<% next if (current_day + 1.day) > end_of_month %>
<li>
<ol>
<% 7.times do |day_num| %>
<% current_day += 1.day %>
<li data-popup-dismiss="true" data-calendar-day-value="<%= current_day %>" class="<%= current_day.month != @display_date.month ? 'outside' : '' %> <%= current_day == @selected_date ? 'selected' : '' %> <%= current_day == today ? 'today' : '' %>">
<%= current_day.day %>
</li>
<% end %>
</ol>
</li>
<% end %>
</ol>
</div>
<% end %>
<% end %>
// Again I use stimulus to control changing the input, label, and selected but you can do the same with just plain JS
import { Controller } from 'stimulus'
export default class extends Controller {
static get targets() { return ['input', 'link', 'label'] }
calendarClicked({ target }) {
if (target.dataset.calendarDayValue) {
// Set the hidden input value
this.inputTarget.value = target.dataset.calendarDayValue
// Change the selected value in the turbo frame param
this.linkTarget.href = this.linkTarget.href.replace(/selected\=.*\&/, `selected=${target.dataset.calendarDayValue}&`)
// you may want to reformat the date before inserting it into the label
this.labelTarget.textContent = target.dataset.calendarDayValue
}
}
}
class InterfacesController < ApplicationController
def calendar
@selected_date = params[:selected] ? Date.parse(params[:selected]) : Date.today
@display_date = params[:show] ? Date.parse(params[:show]) : @selected_date
@target = params[:target] # Since we are rendering turbo frames, this is used to add a "_@target" to the frame name for displaying multiple calendars
end
end
// I use stimulus to control clicking the link when the user clicks the details summary but you can do the same with just plain JS
import { Controller } from 'stimulus'
export default class extends Controller {
static get targets() { return ['link'] }
initialize() {
if (this.hasLinkTarget) this.linkTarget.hidden = true
}
open() {
this.element.open = true;
}
close() {
this.element.open = false;
}
closeOnClickOutside({ target }) {
if (this.element.contains(target)) {
// Close if clicking a link in the popup
if (target !== this.linkTarget && target.tagName === 'A' && !target.dataset.popupKeepalive) {
this.close()
}
if (target.dataset.popupDismiss) {
this.close()
}
return
}
this.close()
}
update() {
if (!this.element.open) return
if (this.hasLinkTarget) this.linkTarget.click() // clicking the link will load the turboframe
}
}
$day-width: 40px;
$day-height: 40px;
$popup-bg-color: white;
$popup-text-color: black;
.calendar {
text-align: center;
header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 5px 0;
h4 {
font-size: 16px;
white-space: nowrap;
}
ol {
display: flex;
flex-direction: row;
opacity: 0.5;
li {
width: $day-width;
}
}
a {
padding: 0 5px;
text-decoration: none;
svg {
height: 20px;
}
}
}
ol {
list-style: none;
margin: 0;
padding: 0;
}
& > ol ol {
display: flex;
flex-direction: row;
li {
width: $day-width;
height: $day-height;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s ease;
text-shadow: 0px 1px rgba(0, 0, 0, 0.3);
&:hover {
background-color: rgba(white, 0.07);
}
&.selected {
background-color: rgba(white, 0.2);
}
&.outside {
opacity: 0.6;
text-shadow: none;
}
&.today {
position: relative;
&::after {
position: absolute;
content: '•';
top: -1px;
font-size: 10px;
}
}
}
}
}
details[data-controller~="popup"] {
position: relative;
outline: none;
z-index: 10;
a[data-popup-target~="link"] {
display: none;
}
summary {
list-style: none;
outline: none;
user-select: none;
&::-webkit-details-marker { display: none; }
}
.popup-menu {
margin-top: 4px;
margin-bottom: 4px;
position: absolute;
background: $popup-bg-color;
color: $popup-text-color;
box-shadow: 0 0 42px -10px rgba(black, 0.43);
padding: 8px 12px;
min-width: 100px;
font-size: 16px;
&.right { right: 0 }
&.left { left: 0 }
}
}
<details data-controller="popup input_calendar" data-action="toggle->popup#update click@window->popup#closeOnClickOutside">
<summary data-action="toggle->popup#update">
<h4 data-input_calendar-target="label"><%= my_selected_date %></h4>
<%= link_to 'Select Date', calendar_interface_path(selected: my_selected_date.to_date, target: 'MYFORM'), 'data-turbo-frame': 'calendar_interface_MYFORM', 'data-popup-target': 'link', 'data-input_calendar-target': 'link' %>
</summary>
<%= form.hidden_field :my_selected_date, value: my_selected_date.to_date, 'data-input_calendar-target': 'input' %>
<%= turbo_frame_tag "calendar_interface_MYFORM", class: 'popup-menu', 'data-action': 'click->input_calendar#calendarClicked' %>
</details>
@humphreyja
Copy link
Author

Demo of this is located here https://hotwire-components.herokuapp.com

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment