Model Comment
class Comment < ApplicationRecord
belongs_to :user
has_rich_text :content
after_create_commit do
broadcast_append_to :comments, target: "comments", partial: "comments/comment_for_stream", locals: { comment: self }
update_counter
end
def render_not_allowed(user, flash)
broadcast_update_to([user, :comments], target: :notifications, partial: 'shared/flash', locals: { flash: flash })
end
private
def update_counter
broadcast_update_to :comments, target: "comments_counter", partial: 'comments/count', locals: { count: Comment.count }
end
end
CommentsController
def create
@comment = current_user.comments.new(comment_params)
respond_to do |format|
if @comment.save
format.turbo_stream {
render turbo_stream: turbo_stream.replace("new_comment",
partial: "comments/form",
locals: { comment: Comment.new }
)
}
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
def destroy
if current_user.id != @comment.user.id
flash.now[:alert] = "Not allowed!"
@comment.render_not_allowed(current_user, flash)
return
end
@comment.destroy
end
views/comments/_comment_for_stream.html.erb (for broadcasting)
<%#= content_tag :div, id: dom_id(comment), class: "card mb-3" do %>
<%= turbo_frame_tag dom_id(comment), class: "card mb-3" do %>
<div class="card-body">
<%= render 'comments/button_section', comment: comment %>
<p class="card-text"><%= comment.content %></p>
<p id='<%= dom_id(comment)%>_count'>
<%= comment.vote_count %>
</p>
<%= render 'comments/votes_link', comment: comment, static: true %>
</div>
<% end %>
views/comments/_button_section.html.erb
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex">
<span class="card-title fs-6 fw-lighter">
<%= time_ago_in_words(comment.created_at) %> ago by
</span>
<span class='fw-light'>
<%= comment.user.email.split('@').first %>
</span>
</div>
<div class="d-flex">
<div class="">
<%= link_to "Show", comment,
class: 'btn btn-link text-decoration-none fw-light',
data: {turbo_frame: :comments } %>
</div>
<#% Stimulus controller connected! %>
<div class="d-flex d-none"
data-controller='check-author'
data-check-author-author-id-value='<%= comment.user.id %>' >
<%= link_to 'Edit', edit_comment_path(comment),
data: { turbo_frame: dom_id(comment) },
class: 'btn btn-link text-decoration-none fw-light' %>
<%= button_to comment,
method: :delete,
form: { data: { turbo_confirm: 'Are you sure?' }},
# data: { turbo_method: :delete,
# turbo_confirm: "Are you sure?" },
id: 'close-custom',
class: 'btn-close btn-close-white mt-2',
data: { turbo_frame_target: :_top } do %>
<% end %>
</div>
</div>
</div>
javascript/controllers/check_author_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="check-author"
export default class extends Controller {
static values = {
authorId: String
}
connect() {
if (this.currentUserId === this.authorId) {
this.element.classList.remove("d-none")
}
}
get authorId() {
return this.authorIdValue
}
get currentUserId() {
return document.querySelector("[name='current-user-id']").content
}
}
views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Turbo Comments</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="current-user-id" content="<%= current_user&.id %>">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
..other code..
javascript/controllers/index.js
import CheckAuthorController from "./check_author_controller.js"
application.register("check-author", CheckAuthorController)
Alternative
We send the same partial to everyone. For the user, we open a personal channel in addition to the general one. In this channel, we send the author's partial. It sounds simple, but there is a race condition. We cannot guarantee that the author's broadcast will arrive later than the general one. We need to introduce a delay, but even that cannot always guarantee the correct order.
If there aren't many users, like in a chat, you can simply get a list of all participants except the author, send them the general partial, and for the author, render without a broadcast in Turbo Stream.