Use Turbo in your Phoenix app. Sample App
- Add PhoenixTurbo to your list of dependencies in mix.exs:
def deps do
[
{:phoenix_turbo, "~> 0.1.0"}
]
end
- Install Turbo
- Install Stimulus (Optional)
in lib/[my_app]_web.ex
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
defmacro __using__(which) when is_atom(which) do
- apply(__MODULE__, which, [])
+ quote do
+ unquote(apply(__MODULE__, which, []))
+ unquote(apply(PhoenixTurbo, which, []))
+ end
end
end
Configure new mime type
in config/config.exs
+ config :mime, :types, %{
+ "text/vnd.turbo-stream.html" => ["turbo-html"]
+ }
And recompile the mime plug after that
mix deps.clean mime --build
mix deps.get
in lib/[my_app]_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :chat
+ use PhoenixTurbo.StreamHelper
in lib/[my_app]_web/channels/user_socket.ex
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
+ channel "turbo-streams:*", PhoenixTurbo.Channel
+
## Channels
# channel "room:*", ChatWeb.RoomChannel
in lib/[my_app]_web/router.ex
pipeline :browser do
+ plug :accepts, ["html", "turbo-html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
+ plug :handle_turbo_frame
end
Add javascript file:
// cable_stream_source_element.js
import { connectStreamSource, disconnectStreamSource } from "@hotwired/turbo"
import socket from "./socket"
class TurboCableStreamSourceElement extends HTMLElement {
connectedCallback() {
connectStreamSource(this)
const channelName = this.getAttribute("channel")
const sign = this.getAttribute("signed-stream-name")
this.channel = socket.channel(`turbo-streams:${channelName}`, { sign })
this.channel.join()
this.channel.on("update_stream", ({ data }) => {
this.dispatchMessageEvent(data)
})
}
disconnectedCallback() {
disconnectStreamSource(this)
if (this.channel) this.channel.leave()
}
dispatchMessageEvent(data) {
const event = new MessageEvent("message", { data })
return this.dispatchEvent(event)
}
}
customElements.define("turbo-cable-stream-source", TurboCableStreamSourceElement)
turbo_stream_request?/1
: Return true if it's a "turbo-stream" request.render_turbo_stream
: Renderturbo_stream
template. The same effect withPhoenix.Controller.render/3
except subtracting layout and putting turbo stream content type to the HTTP response content type.
defmodule ChatWeb.MessageController do
use ChatWeb, :controller
alias Chat.Rooms
alias Chat.Rooms.Message
plug :set_room
def create(conn, %{"message" => message_params, "room_id" => room_id}) do
message = Rooms.create_message!(Map.put(message_params, "room_id", room.id))
if turbo_stream_request?(conn) do # <- turbo_stream_request?
render_turbo_stream(conn, "create_turbo_stream.html", message: message) # <- render_turbo_stream
else
redirect(conn, to: Routes.room_path(conn, :show, room_id))
end
end
end
turbo_stream_tag
: <%= turbo_stream_tag @conn, @post %>
would generate
<turbo-cable-stream-source channel="posts_1" signed-stream-name="xxxxx"></turbo-cable-stream-source>
which will work with cable_stream_source_element.js
MyApp.Endpoint.update_stream
: Send stream updates via websocket.
defmodule ChatWeb.MessageController do
use ChatWeb, :controller
alias Chat.Rooms
alias Chat.Rooms.Message
plug :set_room
def create(conn, %{"message" => message_params}) do
room = conn.assigns.room
message = Rooms.create_message!(Map.put(message_params, "room_id", room.id))
if turbo_stream_request?(conn) do
ChatWeb.Endpoint.update_stream( # <-- send stream updates
room, # <-- send stream updates
ChatWeb.MessageView, # <-- send stream updates
"create_turbo_stream.html", # <-- send stream updates
message: message # <-- send stream updates
) # <-- send stream updates
send_resp(conn, 201, message.content)
else
redirect(conn, to: Routes.room_path(conn, :show, conn.assigns.room))
end
end
defp set_room(conn, _) do
room_id = conn.params["room_id"]
room = Rooms.get_room!(room_id)
assign(conn, :room, room)
end
end