As Hitman Family we share the same contract, but we need realtime update of the contract list
That ‘s great because turbo comes with broadcasting partial page updates delivered asynchronously over a web socket connection. Thanks to Action Cable turbo will provide easy tool to implement Real Time features.
In our case we will use Redis as for our cable implementation. ( using solid is even easier as it doesn’t require redis)
In our one page application, we want to modify our contract list, turbo comes with turbo_stream_from 'channel_name' method to subscribe to a channel
#app/views/index.html.erb
[...]
<div class="list-group">
<%= turbo_frame_tag "new_contract" %>
<%= turbo_stream_from "contracts" %> # Subscribe to "contracts" channel
<% @contracts.each do |contract| %>
<%= turbo_frame_tag "contracts" do%>
<%= render 'contracts/contract', contract: contract %>
<% end %>
<% end %>
</div>
After starting redis-server AND rails s, here a are the subscription logs when loading contract’s index
Started GET "/cable" for 127.0.0.1 at 2024-11-17 17:57:26 +0100
Started GET "/cable" [WebSocket] for 127.0.0.1 at 2024-11-17 17:57:26 +0100
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
Turbo::StreamsChannel is transmitting the subscription confirmation
Turbo::StreamsChannel is streaming from contracts
To broadcast when an object is updated/created or destroyed, we add callbacks in the Contract model
class Contract < ApplicationRecord
after_update_commit -> { broadcast_update_to "contracts", partial: "contracts/contract",
locals: { contract: self }, target: "contracts" }
after_create_commit -> { broadcast_prepend_to "contracts", partial: "contracts/contract",
locals: { contract: self }, target: "contracts" }
after_destroy_commit -> { broadcast_remove_to "contracts", target: "contract_#{id}"}
[...]
end
This code can be improve with private methods instead of lambda ( Not today)
broadcast_update_to "contracts", partial: "contracts/contract",
locals: { contract: self }, target: "contracts"
"contracts" , other updates types exists (prepend, remove, …) : https://turbo.hotwired.dev/reference/streamspartial option renders a specific view template for the broadcasted content.target option identifies which DOM element to modify. In our case the element with id = "contracts" AKA the <turboframe id="contracts">[...]
if @contract.update(contract_params)
# Broadcast an update to the Turbo Streams channel
@contract.broadcast_update_to(
"contracts",
partial: "contracts/contract",
locals: { contract: @contract },
target: "contract_#{@contract.id}"
)
render turbo_stream: turbo_stream.replace("contract_#{@contract.id}", partial: "contracts/contract", locals: { contract: @contract })
else
[...]
class ContractBroadcastJob < ApplicationJob
queue_as :default
def perform(contract_id)
contract = Contract.find(contract_id)
# Broadcast the update
contract.broadcast_update_to(
"contracts",
partial: "contracts/contract",
locals: { contract: contract },
target: "contract_#{contract.id}"
)
end
end