I found a solution to my problem!
I hooked up some Javascript via Stimulus to detect when a new resource is coming via Turbo Streams, then make a URL request to check permissions, and update the button visibility accordingly. Of course, this means one extra request whenever a new resource is added via ActionCable, but that is pretty much unavoidable if the permissions differ per resource.
Anyway, here's the code showing how I did everything.
I started by creating a helper function to detect server side if the rendering was going to be sent over ActionCable as a TurboStream.
# app/controllers/application_controller.rb
def turbo_stream?
formats.any?(:turbo_stream)
end
helper_method :turbo_stream?
This helper can then be used in the view to skip the policy
check if the request was inside the Turbo Stream context (as this will result in Devise could not find the `Warden::Proxy` instance on your request environment
).
I also decided to hide the buttons by default in that case, so incoming updates will be without buttons for a brief fraction of a second, and they will then appear if the user has permission.
I also added a URL to the resource, and tags to the action buttons I want to hide or show. This will be used later in the Javascript controller.
<!-- app/views/resource/_resource.html.erb -->
<%= turbo_frame_tag resource do %>
<div id="<%= dom_id resource %>"
data-resource-url="<%= resource_path(resource, format: :json) %>">
<!-- omitted code -->
<% if turbo_stream? || policy(resource).edit? %>
<%= link_to edit_resource_path(resource),
class: "btn btn-primary #{'d-none' if turbo_stream?}",
data: { resource_action: :edit } do %>
<i class="las la-edit"></i>
<span class="d-none d-lg-inline">
<%= t("buttons.edit") %>
</span>
<% end %>
<% end %>
<% if turbo_stream? || policy(resource).destroy? %>
<%= link_to resource,
class: "btn btn-danger #{'d-none' if turbo_stream?}",
data: {
resource_action: :destroy,
turbo_confirm: t("confirm.short"),
turbo_method: :delete
} do %>
<i class="las la-trash-alt"></i>
<span class="d-none d-lg-inline">
<%= t("buttons.remove") %>
</span>
<% end %>
<% end %>
</div>
<% end %>
I adjusted my JSON template to include permissions for the requesting user.
# app/views/resources/_resource.json.jbuilder
json.permissions do
json.edit policy(resource).edit?
json.destroy policy(resource).destroy?
end
I then created a controller which would listen to the event turbo:before-stream-render
and modify the event.detail.render
method to run my processing after the default action.
That processing makes a URL request to fetch the permissions, and updates the visibility of the tagged action buttons based on the response.
// app/javascript/controllers/turbostream_controller.js
import Rails from "@rails/ujs"
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
addEventListener("turbo:before-stream-render",
(e) => { this.beforeStreamRender(e) })
}
beforeStreamRender(event) {
const defaultAction = event.detail.render
event.detail.render = (streamElement) => {
defaultAction(streamElement)
try { this.processStream(streamElement) }
catch(error) { console.error(error) }
}
}
processStream(streamElement) {
if (["prepend", "append", "update"].includes(streamElement.action)) {
var template = streamElement.children[0].content
var templateDiv = template.querySelector('[data-resource-url]')
if (templateDiv != null) {
var id = templateDiv.getAttribute('id')
this.setActionButtonVisibility(id)
}
}
}
setActionButtonVisibility(id) {
var div = document.querySelector(`div#${id}`)
var url = div.getAttribute('data-resource-url')
var edit = div.querySelector('[data-resource-action="edit"]')
var destroy = div.querySelector('[data-resource-action="destroy"]')
Rails.ajax({
type: "GET",
url: url,
success: (data, _status, _xhr) => {
try {
edit.classList.toggle('d-none', !data.permissions.edit)
destroy.classList.toggle('d-none', !data.permissions.destroy)
} catch(error) {
console.error(error)
}
}
})
}
}
Last thing was to wrap my resource list in a div with the controller:
<!-- app/views/resource/index.html.erb -->
<div data-controller="turbostream">
<!-- my original code -->
</div>
And that's it!
There is probably room for improvement but if someone else in the future faces a similar use case, I hope this could contribute.
Cheers!