My solution is definitely making me think of Elm now. I got things working for a while but then I bumped into the fact that you don’t have access to routing inside nested live views. (Views that aren’t mounted via the router.) I started experimenting with just one single live view at the root and using LiveComponent for anything else.
All my routes now point to a single live view:
# router.ex
live "/", PageLive, :index
live "/nutrition/", PageLive, :nutrition
live "/nutrition/:food_id", PageLive, :nutrition_detail
I do wish actions could be tuples. This would make it easier to pattern match within the hierarchy
For each sub-route, (In my case only :nutrition
but more will follow), I define a state entry in the socket’s assigns and pass all messages and information to the corresponding sub-route module.
handle_info
and apply_action
both use pattern matching to select the correct module to handle the update:
# index.ex
defmodule FireweedWeb.PageLive do
use FireweedWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, socket |> assign(page: :index, nutrition: %{})}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Home")
end
defp apply_action(socket, action, params) when action in [:nutrition, :nutrition_detail] do
FireweedWeb.Nutrition.Index.apply_action(socket, action, params) |> assign(page: :nutrition)
end
@impl true
def handle_info(message, socket) do
case elem(message, 0) do
:nutrition -> FireweedWeb.Nutrition.Index.handle_info(message, socket)
_ -> {:noreply, socket}
end
end
end
In components I make use of phx-target
to force component modules to handle events.
My main template pattern matches against @page
(which is set in apply_action):
# index.html.leex
<%=
case @page do
:nutrition -> live_component(@socket, FireweedWeb.Nutrition.Index, id: "nutrition", nutrition: @nutrition)
_ -> "Hello World"
end
%>
I’m passing the .nutrition
state to it so that updates to that slice will propagate.
In FireweedWeb.Nutrition.Index
's mount I initialize state:
# /nutrition/index.ex
defmodule FireweedWeb.Nutrition.Index do
use FireweedWeb, :live_component
@impl true
def mount(socket) do
{:ok,
socket
|> assign(
nutrition: %{
foods: [],
food_id: nil,
food: nil,
query: ""
}
)}
end
I’ll probably add a initial_state method that is called in the live view to initialise the .nutrition
slice state. Since the live view passes updates using apply_action this is also defined in my sub module:
...
def apply_action(socket, action, params) do
case {action, params} do
{:nutrition_detail, %{"food_id" => food_id}} ->
send(self(), {:nutrition, :fetch, food_id})
socket |> assign(nutrition: Map.put(socket.assigns.nutrition, :food_id, food_id))
_ ->
socket
end
end
Only updating socket.assigns.nutrition using Map.put (I’ll make a helper function for that). For any expected messages handle_info is also defined as long as the first element in the tuple is :nutrition
...
def handle_info({:nutrition, :search, term}, socket) do
With the single live view in place I can route from anywhere in the hierarchy and now have 3 levels of routes with the nutrition sub module handling anything nutrition related be it routing, messages, or actions.