# Terminal
rails g model comment name content:rich_text
rails g controller comments index
rails g controller comments/polls show
rails g stimulus poller
# config/routes.rb
Rails.application.routes.draw do
resources :comments, only: [:index, :create]
namespace :comments do
resource :poll, only: :show
end
root to: 'comments#index'
get "up" => "rails/health#show", as: :rails_health_check
end
# app/controllers/comments/polls_controller.rb
class Comments::PollsController < ApplicationController
def show
last_seen_id = params[:last_seen_id].to_i
@comments = Comment.with_rich_text_content_and_embeds.where("id > ?", last_seen_id)
.order(id: :desc)
render partial: "comments/comment", collection: @comments, layout: false
end
end
# app/javascript/controllers/poller_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="poller"
export default class extends Controller {
static targets = ["list"]
static values = {
url: String,
lastSeenId: Number,
interval: { type: Number, default: 3000 }
}
connect() {
this.timer = setInterval(() => this.poll(), this.intervalValue)
}
disconnect() {
clearInterval(this.timer)
}
async poll() {
const response = await fetch(`${this.urlValue}?last_seen_id=${this.lastSeenIdValue}`)
const html = await response.text()
if (html.trim() === "") return
this.listTarget.insertAdjacentHTML("afterbegin", html)
console.log(`poll id: ${Number(this.listTarget.firstElementChild.dataset.pollId)}`)
this.lastSeenIdValue = Number(this.listTarget.firstElementChild.dataset.pollId)
}
}
# app/views/comments/_comment.html.erb
<%= tag.article id: dom_id(comment), "data-poll-id": comment.id,
class: "flex gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition hover:shadow-md" do %>
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-blue-100 font-semibold text-blue-700">
<%= comment.name.to_s.first&.upcase %>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-baseline justify-between gap-2">
<span class="font-semibold text-slate-900"><%= comment.name %></span>
<time class="shrink-0 text-xs text-slate-400"><%= comment.created_at.strftime("%b %-d, %-l:%M %p") %></time>
</div>
<div class="mt-1 text-slate-700">
<%= comment.content %>
</div>
</div>
<% end %>
# app/views/comments/index.html.erb
<div
data-controller="poller"
data-poller-url-value="<%= comments_poll_path %>"
data-poller-last-seen-id-value="<%= @comments.first&.id.to_i %>"
data-poller-interval-value="3000">
<div data-poller-target="list" exclass="space-y-3">
<%= render @comments %>
</div>
<% if @comments.empty? %>
<p
class="
rounded-xl border border-dashed border-slate-300 bg-white p-8
text-center text-slate-400
"
>
No comments yet. Be the first to post one.
</p>
<% end %>
</div>