Phoenix Live View Basics
Live View is a Genserver that turns Elixir or Phoenix into a Websocket Server. This eliminates the need for Javascript frameworks like React or Vue. Live Components replace React Components. Though you can still use React if you want to make life difficult for yourself.
GET
in REST is mount
or patch
for websockets
Change a state by sending an event:
phx-value-id="<%= struct.id %>"
%{"input-data" => elixir-variable-repersenting-a-string}
SOCKET
- a struct that has an ‘assigns’ map
- the assigns map has the LiveView state info
- ‘socket assigns’ therefore means the map-data
{:ok, socket}
{:no_reply, socket}
{:error, socket}
mount(map_of_query_router_params, session_data, socket_struct)
-
creates a LiveView process from the router
-
called twice:
- To load the HTML
- After it has connected to the websocket
-
assigns the state of the LiveView process to the socket struct
-
must return a
{:ok, socket}
tuple -
can be revealed by
IO.inspect(socket)
def mount(map_of_query_router_params, session_data, socket_struct) do
{:ok, assign(socket_struct, :key, value)}
end
render(assigns_map)
- needs to output content as a ‘sigil’
~L"""
<%= @key_in_the_assigns %>
"""
BINDINGS IN TEMPLATE
phx-click
- binds a click event to an element then sends it to the LiveView process through the socket
LIVE ROUTES
Live Routes are PATCH
because it updates or patches the LiveView process with the new data and sends a new ‘diff’ to the DOM
<%= live_patch "Text",
to: Routes.live_path(@socket, __CURRENTMODULE__, id: struct.id) %>
__CURRENTMODULE__
is a shortcut for the Module Name
HANDLERS
- These render the changes in the data (or “state”) inside the socket-struct-assign-map.
handle_params
- handles URL parameters that affect Routes via live_patch
- why not call it as handle_url??
- sends an event as data-phx-link-state
- always invoked after
mount
- handles dynamic states, just as mount handles static states
handle_params(parameters_of_url, url, socket) do
# manipulate the socket data here to automatically output and update the view
{:noreply, socket}
# has parameters
def handle_params(%{"id" => id}, parameters_of_url, url, socket) do
# manipulate the socket data via the id state
{:noreply, socket}
end
# empty parameters
def handle_params(%{_, _url, socket) do
# if no url parameter is entered then just show the socket
{:noreply, socket}
end
# rendering whatever
def render(data) do
always_needed_assigns = %{key: data}
~L"""
<%= @key %>
"""
end
handle_event
- handles external messages (events) from the template bindings such as
phx-click
- why not call it
handle_external???
def handle_event("name_of_external_message_or_event_in_html", metadata_about_the_event, socket_struct) do
assign(socket, :key, value)
{:no_reply, socket}
end
handle_info
- handles internal messages (info)
def handle_info(name_of_internal_message, socket_struct) do
assign(socket, :key, value)
{:no_reply, socket}
end
Subscribe - Broadcast
Subscribe
def subscribe do
Phoenix.PubSub.subscribe(App.PubSub, "topic_name")
end
Broadcast
def broadcast({:ok, message}, event) do
Phoenix.PubSub.broadcast(App.PubSub, "topic_name", message)
end`
message = {:message_name, actual_message}
LIVE COMPONENTS
@impl true
Tells LiveView to implement the callback
@impl true
def render(assigns) do
~L"""
whatever
"""
Stateful by adding id
and pointing to the Component with @myself
Add id
to the live_component to make it ‘stateful’
<%= live_component @socket, MessageComponent, id: 1 %>
Add @myself
to the stateful form to make the Component handle the event
<form phx-submit="send-message" phx-target="<%= @myself %>" %>
Card Component with Form
defmodule CardComponent do
def render(assigns) do
~H"\""
<form phx-submit="..." phx-target={@myself}>
<input name="title"><%= @card.title %></input>
...
</form>
"\""
end
Listing of cards
<%= for card <- @cards do %>
<%= live_component CardComponent, card: card, id: card.id, board_id: @id %>
<% end %>
Form submission triggers CardComponent.handle_event/3
which must update the card.
LiveView as the Source: self()
The component and the view run in the same process. So, sending an internal message from the LiveComponent to the parent LiveView is done by sending a message to self()
:
send self(), {:message_name, %{content_of_message}}
defmodule CardComponent do
...
def handle_event("update_title", %{"title" => title}, socket) do
send self(), {:updated_card, %{socket.assigns.card | title: title}}
{:noreply, socket}
LiveView then receives this event using c:Phoenix.LiveView.handle_info/2
:
defmodule BoardView do
...
def handle_info({:updated_card, card}, socket) do
# update the list of cards in the socket
{:noreply, updated_socket}
The LiveView will send the updated card to the component.
Alternatively, the could be updated via broadcast by using Phoenix.PubSub
to all users subscribed to it.
defmodule CardComponent do
...
def handle_event("update_title", %{"title" => title}, socket) do
message = {:updated_card, %{socket.assigns.card | title: title}}
Phoenix.PubSub.broadcast(MyApp.PubSub, board_topic(socket), message)
{:noreply, socket}
end
defp board_topic(socket) do
"board:" <> socket.assigns.board_id
LiveComponent as the Source
In this case, LiveView must only fetch the card ids, then render each component only by passing an ID:
<%= for card_id <- @card_ids do %>
<%= live_component CardComponent, id: card_id, board_id: @id %>
<% end %>
Each CardComponent will load its own card making expensive N queries, where N is the number of cards. So we use the c:preload/1
callback to make it efficient.
Once the card components are started, they can each manage their own card, without concern for the parent LiveView.
Components do not have a c:Phoenix.LiveView.handle_info/2
callback. Therefore, if you want to track distributed changes on a card, you must have LiveView receive those events and redirect them to the appropriate card.
For example, if card updates are sent to the “board:ID” topic, and that the board LiveView is subscribed to the said topic, one could do:
def handle_info({:updated_card, card}, socket) do
send_update CardComponent, id: card.id, board_id: socket.assigns.id
{:noreply, socket}
With Phoenix.LiveView.send_update/3
, the CardComponent
given by id
will be invoked, triggering both preload and update callbacks, which will load the most up to date data from the database.
LiveComponent blocks
When live_component/3
(Phoenix.LiveView.Helpers.live_component/3)
is invoked, it is also possible to pass a do/end
block:
<%= live_component GridComponent, entries: @entries do %>
<% entry -> %>New entry: <%= entry %>
<% end %>
The do/end
will be available in an assign named @inner_block
.
You can render its contents by calling render_block
with the assign itself and a keyword list of assigns to inject into the rendered
content. For example, the grid component above could be implemented as:
defmodule GridComponent do
use Phoenix.LiveComponent
def render(assigns) do
~H"\""
<div class="grid">
<%= for entry <- @entries do %>
<div class="column">
<%= render_block(@inner_block, entry) %>
</div>
<% end %>
</div>
"\""
end
end
Where the entry
variable was injected into the do/end
block.
Note the @inner_block
assign is also passed to c:update/2
along all other assigns. So if you have a custom update/2
implementation, make sure to assign it to the socket like so:
def update(%{inner_block: inner_block}, socket) do
{:ok, assign(socket, inner_block: inner_block)}
end