Infinite Y-Axis Calendar View Scroll
Tags: ruby, javascript, rails, turbo
Published: 2024-01-03 06:55:00

---
title: Infinite Y-Axis Calendar View Scroll
published: true
description: Using ruby on rails to create an infinite scroll of the y-axis for a calendar view.
tags: ruby,javascript,rubyonrails,turbo
# cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r77uxj9q8u5q6m9gfmva.png
# Use a ratio of 100:42 for best results.
published_at: 2024-01-03 06:55 +0000
---

![A snapshop of the calendar view from ektarOS (FarmSwop) journal app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r77uxj9q8u5q6m9gfmva.png)

I admired the `ScrollView` on iOS. When done right, you get a smooth infinite scrolling of the y-axis while it handles loading of data onto the view. A similar design was done for the DayOne iOS app; it's an infinite scroll back in the past as it's a journaling app so that make sense. I'm building a similar app but for farmers for my app called [ektarOS (FarmSwop)](https://farmswop.com/).

```erb
# index.html.erb: ie farmswop.com/calendar

<div class="journal-calendar-container">
  <header class="journal-calendar-header bg-earth-50 z-50 dark:bg-gray-850 dark:text-bubble-800 grid grid-cols-7 p-1">
    <span class="journal-calendar-header__item">mon</span>
    <span class="journal-calendar-header__item">tue</span>
    <span class="journal-calendar-header__item">wed</span>
    <span class="journal-calendar-header__item">thu</span>
    <span class="journal-calendar-header__item">fri</span>
    <span class="journal-calendar-header__item">sat</span>
    <span class="journal-calendar-header__item">sun</span>
  </header>

  <div id="journal-container" class="h-screen overflow-y-auto">
    <%= render "journals/calendar/scrollable", direction: "past" %>
    <%= render "journals/calendar/scrollable", direction: "future" %>
  </div>
</div>
```
As you can see, I'm rendering `journals/calendar/scrollable` twice; the top one that fetches past data while the other fetches present and future data. Let's take a look at the partial:

`views/journals/calendar/scrollable`:

```erb
<%= turbo_frame_tag "#{direction}_journal_timeline",
  src: journal_timeline_path(direction: direction, month_year: local_assigns[:month_year] || nil ),
  loading: :lazy, target: "_top" do %>
    <p>Loading...</p>
<% end %>

```
On page load, we make two requests; one for past data and the other for present/future data. `journal_timeline_path` renders a repeat of `views/journals/calendar/scrollable` in an odd way:

```erb
<%= turbo_frame_tag :"#{@direction}_journal_timeline" do %>
  <div class="flex flex-col flex-grow">

    <% if @direction == "past" %>
      <%= render "journals/calendar/scrollable", month_year: @month_year, direction: @direction %>

      <%= render "shared/journal_calendar",direction: @direction, calendar_data: @calendar_data %>
      
    <% else %>
      <%= render "shared/journal_calendar", direction: @direction, calendar_data: @calendar_data %>
      
      <%= render "journals/calendar/scrollable", month_year: @month_year, direction: @direction %>
    <% end %>

  </div>
<% end %>
```
For past data, I try to keep `journals/calendar/scrollable` from not being in view until the user scrolls up. It's not perfect as yet, we have to use JavaScript for this section. I'll come to that later.

`shared/journal_calendar`:

```erb
<%= turbo_frame_tag :"#{direction}_journal_timeline" do %>
  <article class="bg-earth-100 dark:bg-gray-850 invisible" data-controller="journal" data-direction="<%= direction %>">
    <div class="bg-earth-50 dark:bg-gray-840 dark:text-bubble-900 px-4 py-1 text-sm font-stdmedium">
      <%= @date.strftime('%B %Y') %>
    </div>
    <div class="flex items-center justify-between overflow-x-auto">
      <table class="w-full">
        <thead>
          
        </thead>
        <tbody>
          <% calendar_data.each do |week| %>
            <tr>
              <% week.each do |day| %>
                <td class="journal__cell <%= day.present? ? 'journal__cell--on' : 'journal__cell--off' %>">
                  <!-- Your rendered cell -->
                  <%# render "shared/journal_cal_cell", day: %>
                </td>
              <% end %>
            </tr>
          <% end %>
        </tbody>
      </table>
    </div>
  </article>
<% end %>
```

##The Calendar Controller

Calendar days are server-side rendered. No JavaScript is used to render the dates nor its data.

```rb
class Journals::CalendarController < ApplicationController  
  def index; end
end
```
And that's it! Literally! Joking! `Journals::CalendarController` is only needed to render the `index` page. If you look back at `views/journals/calendar/scrollable`, it fetches `journal_timeline_path`. Here's that controller:

```rb
class Journals::TimelineController < ApplicationController
  def index
    @direction = params[:direction]
    @month_year = params[:month_year].present? ? Date.parse(params[:month_year]) : Date.today
  
    case @direction
    when "past"
      @month_year -= 1.month
    end
  
    @date = @month_year.beginning_of_month
    @calendar_data = Journal::Timeline.new(start_date: @date, bucket: @bucket).calendar_data
  
    case @direction
    when "past"
      @month_year -= 1.month
    else
      @month_year += 1.month
    end
  end
end
```
You may see references to "buckets", they are not relevant to this post. Most of this code I copied directly from my code editor.

```rb
class Journal::Timeline
  def initialize(start_date: nil, bucket: nil)
    @start_date = start_date
    @bucket = bucket
  end

  def calendar_data
    generate_calendar_data
  end

  private

    def generate_calendar_data
      calendar_data = []
      current_date = @start_date
      week = []
    
      # Determine the number of empty cells before the first day of the month
      empty_cells = (current_date.wday - 1) % 7
      empty_cells = 6 if empty_cells < 0
    
      # Calculate the date of the first day of the current month
      first_day_of_current_month = current_date.beginning_of_month
    
      # Add days from the previous month, if needed
      empty_cells.downto(1) do |i|
        date = first_day_of_current_month.prev_day(i)
        day_data = initialize_day_data(date)
        week << {} # day_data
      end
    
      # Generate calendar data for the entire month
      while current_date.month == @start_date.month
        day_data = initialize_day_data(current_date)
        week << day_data
    
        if current_date.sunday?
          calendar_data << week
          week = []
        end
    
        current_date = current_date.tomorrow
      end
    
      # Add the last week to the calendar data
      calendar_data << week unless week.empty?
    
      calendar_data
    end

    def initialize_day_data(date)
      # Get the data you need by date
      journals = Recording.select do |rec|
        observed_at_date = rec.recordable.observed_at.to_date if rec.recordable.observed_at.present?
        created_at_date = rec.recordable.created_at.to_date
    
        observed_at_date == date.to_date || created_at_date == date.to_date
      end
    
      # Your data format
      {
        day: date.day,
        date: date.strftime("%d-%m-%Y"),
        journals: journals
        # Add any other data you need for the day, such as events
      }
    end
end
```
`generate_calendar_data` took some time for me to get right. I hope it was well written?

##The Stimulus JavaScripts!

Every time `shared/journal_calendar` gets rendered, we execute some other code:

`app/javascript/controllers/journal_controller.js`:

```js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ['cell']

  connect() {
    const container = document.getElementById('journal-container');
    const scrollTopBefore = container.scrollTop;

    const direction = this.element.dataset.direction;
    
    const newItemsHeight = this.element.clientHeight; // clientHeight/offsetHeight

    if (direction === 'past') {
      // Adjust the scroll position
      container.scrollTop = scrollTopBefore + newItemsHeight;

      this.element.classList.remove('invisible')
      
    } else {
       // Not sure I need this anymore.
      this.element.classList.remove('invisible')
    }
  }
}
```
There's a bug with this JS. While the "past" data scrolls nicely on desktop, not so much on mobile devices. I suspect the issue with getting the height with `clientHeight`. Any ways, I'll look on that later.

That's all there is for an infinite y-axis scroll. For `initialize_day_data > journals`, you can put an empty array if you do not have any data and the infinite scrolling should work, but without any data.

Spot any bug(s)? Please let me know!

Have a great day! 👋