I have a schema that is going to make appearances on quite a few different pages in my application, so I’ve started the process of reworking the default live_view index route to contain a nested LiveView that displays the record or a list of records, and handles the edit and new actions without patching away to a new URL.
So I extracted most of the index.html.heex file to a new LiveView that gets rendered in its place, and I replaced the patch with a phx-click to show the modal and populate the contents:
and a handle_event call instead of handle_params to set the action and show/hide the edit controls.
Other than doing some cleverness to get the flash to appear in the parent view instead of getting swallowed, it all (eventually) worked out and validation and the replace call are firing properly…
Except for the tests. render_click for the New X and the edit button seems to be doing the wrong thing. Tests complain that the parent view has no handle_event function. Took stepping away from the code a couple times to realize this is the problem with the tests. No event, no action. No action, no modal is rendered, since it’s <.modal :if= no modal dialog, no match on the contents of the form.
Is there some settings I need to change in the test setups? Or do I need to call something other than render_click() when working on nested liveviews? If I pull the view out and test it separately I will get part of the coverage from the old test but then my flash assertions won’t work, so I’ll still need some of these tests regardless.
Looks like my other nested liveview (built by hand instead of carved out of a phx.gen.live invocation) is missing some code coverage, which may explain why I didn’t run into this on a simpler example.
From a UX perspective, I’d try to keep most state in the URL if possible. The reason is that if, for whatever reason, a user reconnects, the state will otherwise be lost. For example if you visit the new page and then refresh the page, as a user I’d expect to still be on the new page. If you only handle this with phx-click and assigns, that will most likely not be the case.
I’d suggest to consider extracing the shared functionality into one or multiple LiveComponent and when you render those in different URLs, just pass them the necessary URLs as parameters:
<.live_component module={MyAppWeb.SchemaList} new={~p"/foo/bar/new"} edit={fn id -> ~p"/foo/bar/#{id}/edit" ... />
In general, using nested LiveViews just for extracting out shared functionality is not really recommended. Nested LiveViews are primarily meant for things that require error isolation, as they are separate processes. See also the docs about the different approaches: Welcome — Phoenix LiveView v1.0.5
I’d suggest to consider extracing the shared functionality into one or multiple LiveComponent
If the search engine brings you to elixirforum with a question about reusing interactions across pages, it invariably brings you to old threads that say, “You can’t do that with LiveComponents, you need to use a NestedView” to do that. There’s a bit of that on SO as well but that’s an aside.
attach_hook/3 is certainly a lot better than building a bunch of nested liveviews, but it has a couple problems. It doesn’t seem to work for handle_info, so anything with a timer.send_interval still needs to be a liveview?
The LiveComponent is declared in the render() function, but the hooks in mount(). The component might be behind an :if clause. It might be a LiveComponent that’s inside another LiveComponent that composes several small ones. Now I have to know to register a hook for a grandchild component that I didn’t remember/know existed? If livecomponent is picking up functionality from liveview, are there architectural reasons they couldn’t ‘just’ be a subset of LV functionality?
If there’s a broader solution for things like send_interval I’d love to hear it. My other nested view has a piece of data it wants to look for in the page and the plumbing for that is going to cause me problems again in the future. It’d be good to have a component that did the same.
I think the other mistake/misunderstanding/misdirection I was suffering from is that the generated phx.gen.live CRUD pages have a hard-coded back button. So each second level route has only one place to go back to, and Phoenix has at least two pushState functions but no popState functions. I got it into my head that having the same view go ‘back’ to three, four, half a dozen locations was going to be a giant pain in the ass and then my thinking was anchored to solutions that didn’t have to deal with navigation. When I could just implement my own pushState or popState on the save/cancel links.
So I should know better, but maybe people trying Phoenix as their first FE toolchain wouldn’t, and a popStack function might be useful.
attach_hook also works for handle_info, but only inside LiveViews. If you indeed need to handle your own messages, a nested LiveView can be a good solution. That was not apparent in your initial post though. I just wanted to mention that using nested LiveViews should be done only when there’s a good reason to do so, because it’s a heavy abstraction that comes with its own downsides.
I don’t think there’s a one size fits all solution here. It highly depends on what you’re trying to achieve. For example, LiveComponents could use send_update_after and schedule a new update each time.
I guess you’re talking about push_patch and push_navigate? The problem is that there’s no easy solution for a pop_navigate as navigation is primarily a client-side concern. If LiveView stored a history on the server and then the client reloaded the page, how should the new LiveView know where to navigate on pop_navigate? Implementing a back button on the client with history.back() also has downsides. For example, imagine a user visiting your app through a link placed on another page. If they then press a “back” button, they’ll be sent back, but probably not where your app wants them to go.
I see I waited too long to edit a couple of mis-statements I made. I was mostly talking about handle_info but got my wires crossed with handle_event.
Data propagation and testing being two of those, yeah.
attach_hook also works for handle_info
I missed this the first time reading your response and the linked documentation. That’ll probably be where I end up, since form_components like to use handle_info for validation checks prior to save/create.
I started re-iterating something I said above about building components out of other components, but I think it might be moot depending on the answer to this:
The method signature for attach_hook sure looks like it hangs the hook off of the socket not the liveview. Is that true? Because if it is then there is no composition problem, or at least a simpler one.
Implementing a back button on the client with history.back() also has downsides.
back is great for cancel events. Save is much more complicated. And unfortunately I’d need to solve both in the same spots. If you only need to remember a depth of 1, then I’m hoping it’s simple enough to hang “where are we now?” off of the click event that causes the patch call. And then back() on cancel but patch(previous) on save.
But you don’t want to take someone from an ‘assigned to me’ page back to the backlog view for instance, which is the default implementation.
attach_hook inside a LiveComponent only works for handle_event (and after_render, which is very uncommon), so you cannot attach a hook to the LiveView from inside a LC.