-
Notifications
You must be signed in to change notification settings - Fork 959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unclear documentation for code organization (such as extracting related handle_events to a new module) #3679
Comments
Basically, there are 3 situations when we extract/organize code in LiveViews:
1 is covered by FunctionComponents. 3 is covered by LiveComponents. But a seamless or doc-recommended way for handling 2 seems to be missing |
This does feel like the sort of place https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#attach_hook/4 shines. For the livebook example, the hook is attached on mount, which is called from the router https://github.com/livebook-dev/livebook/blob/2796d5f1d76841e54fb99a765d457b4c34b74199/lib/livebook_web/router.ex#L62 A pattern I often use, is to attach the hooks explicitly when mounting the LiveView. Something like this: Mix.install([
{:phoenix_playground, "~> 0.1.6"},
{:phoenix_live_view, "~> 1.0.0"}
])
defmodule MySortComponent do
use Phoenix.Component
import Phoenix.LiveView, only: [attach_hook: 4]
def enable_sorting(socket) do
attach_hook(socket, :sort, :handle_event, fn
"sort", %{"list" => key}, socket ->
key = String.to_existing_atom(key)
sorted = Enum.sort(socket.assigns[key])
{:halt, assign(socket, key, sorted)}
_, _, socket ->
{:cont, socket}
end)
end
end
defmodule DemoLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
first_list = for(i <- 1..9, do: "First List #{i}") |> Enum.shuffle()
second_list = for(i <- 1..9, do: "Second List #{i}") |> Enum.shuffle()
socket = assign(socket, first_list: first_list, second_list: second_list)
{:ok, MySortComponent.enable_sorting(socket)}
end
def render(assigns) do
~H"""
<div>
<div :for={{key, list} <- [first_list: @first_list, second_list: @second_list]}>
<ul><li :for={item <- list}>{item}</li></ul>
<button phx-click="shuffle" phx-value-list={key}>Shuffle</button>
<button phx-click="sort" phx-value-list={key}>Sort</button>
</div>
</div>
"""
end
def handle_event("shuffle", %{"list" => key}, socket) do
key = String.to_existing_atom(key)
shuffled = Enum.shuffle(socket.assigns[key])
{:noreply, assign(socket, key, shuffled)}
end
end
PhoenixPlayground.start(live: DemoLive, port: 4200) |
I agree with @Gazler that @ericridgeway do you want to adjust your PR to mention |
@Gazler Wow that was a good explanation. The example code helped wrap my mind around how re.
This definitely works, but I worry it's a bit less readable/easy-to-understand, and it almost feels like we're overly bending the use-case for hooks. I thought they were more for escape-hatch'ing to extra JavaScript functionality |
Those are a different type of hook, I appreciate that the terminology can
be confusing. I use the terms "lifecycle hooks" and "JavaScript hooks" to
disambiguate.
…On Wed, 19 Feb 2025, 19:20 Eric Ridgeway, ***@***.***> wrote:
@Gazler <https://github.com/Gazler> Wow that was a good explanation. The
example code helped wrap my mind around how attach_hook works here
Thank you!
re. attach_hook for organizing event handling, do we mind that:
- The extracted code needs to change it's return value from {:noreply,
socket} to {:halt, socket}
- AND requires the destination module add the extra _, _, socket ->
{:cont, socket} clause
This definitely works, but I worry it's a bit less
readable/easy-to-understand, and it almost feels like we're overly bending
the use-case for hooks. I thought they were more for escape-hatch'ing to
extra JavaScript functionality
—
Reply to this email directly, view it on GitHub
<#3679 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AADUSR7U5T3CGULEQ42P72T2QTKOPAVCNFSM6AAAAABXHLG5M2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMNRZGU2TANZQGM>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
[image: ericridgeway]*ericridgeway* left a comment
(phoenixframework/phoenix_live_view#3679)
<#3679 (comment)>
@Gazler <https://github.com/Gazler> Wow that was a good explanation. The
example code helped wrap my mind around how attach_hook works here
Thank you!
re. attach_hook for organizing event handling, do we mind that:
- The extracted code needs to change it's return value from {:noreply,
socket} to {:halt, socket}
- AND requires the destination module add the extra _, _, socket ->
{:cont, socket} clause
This definitely works, but I worry it's a bit less
readable/easy-to-understand, and it almost feels like we're overly bending
the use-case for hooks. I thought they were more for escape-hatch'ing to
extra JavaScript functionality
—
Reply to this email directly, view it on GitHub
<#3679 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AADUSR7U5T3CGULEQ42P72T2QTKOPAVCNFSM6AAAAABXHLG5M2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMNRZGU2TANZQGM>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Alright, if I wish I suppose regular old functions are also always a fallback, even with the annoying repetition of the function parameters
|
Clever use of I have been testing out moving handlers to modules but keeping the module handlers a little more focused so say rather than def handle_event("sort_by_key", params, socket), do: SortingComponent.handle_event("sort_by_key", params, socket)
def handle_event("sort_by_key", params, socket), do: SortingComponent.sort_by_key(params, socket) Then I have also been trying out unit testing those handlers with mocked sockets so I don't have to spin up an entire LV in a test to confirm the handler does what I expect. Still early and agree I would love a way to pattern match on |
This line of the Phoenix.LiveComponent doc correctly says
But it gives no explanation of how to do so.
In
.heex
files, repetition and related functionality can be extracted to other modules as a FunctionComponentBut often those FunctionComponents bind to
handle_event
s in the parent we just extracted them fromIn most tutorial's I've seen, that's done by unnecessarily making a LiveComponent
Which, yes, organizes the render/heex AND it's related handle_events into a single file
BUT as the quote above warns, this should be avoided when we're only doing it to organize code.
The LiveComponent we just made does not need it's own state. All state is used and owned by the parent LiveView.
We were just looking for a way to group function components & related handle_events
Without stealing management of @ assigns from the parent who actually uses them
Here's an example of using a LiveComponent to organize:
It DOES keep the render/heex and related handle_event's in a single file
BUT it doesn't actually want to manage state (the @ sorting assign). It just sends state updates straight to the parent LiveView with
send(self(), {:update, opts})
That parent LiveView is the real owner of the state. The parent actually SETS those assigns from url params, and actually USES those assigns to make calls to the db, etc.
So, how can we delegate related handle_events out of the parent LiveView and into other modules? How should we organize code according to the above quote without resorting to a LiveComponent when the additional state of a LiveComponent is unnecessary?
^ This seems like a possibility, except we can't use pattern matching in defdelegate
^ This works, but is that the best we can do? Repeating
("sort_by_key", params, socket)
like that is so ugly. It seems like the exact reason defdelegate exists in the first placeAnother possibility I saw from this answer. It looks like LiveBook is using attach_hook to group extracted function components and their related handle_events into a Function Component module (NOT a Live Component)
Should we be doing this handle_event organization with defdelegate somehow? Must we use attach_hook instead? And if so, is it possible to add a short example of doing so to the doc? <3
The text was updated successfully, but these errors were encountered: