Responsibilites of components in a phoenix liveview

Hi there, im pretty new to Elixir, Phoenix and Liveview. I come from an app developer background.

Im having a little hard time, wrapping my head around, how to properly compartmentalize (is that a word?) an advanced view, and which responsibilities falls to the liveview, and which falls to the live_component.

How the view was structured: (several live views, each implementing the header, above its own content)
Before

And how im thinking of structuring it now:
after

Because every tab was its own liveview previously, the presentation of specific modals, were each handled by their own “tab” liveview (using patch, and toggling visibility using live_action).

When refactoring this I want to rewrite the old live views into live_components. However, im unsure of wether or not it makes sense keep the toggling of the modals, in the components, or if this functionality should be moved to the “mother” liveview.

My gut feeling and experience from other platforms and languages, tells me that a “view” should handle navigation, and that its best to delegate this responsibility to the controller. However there are a lot of modals, which would mean that my liveview, will get cluttered by many functions. (there will be 9 tabs)

My current plan was to have the component be “dumb” and just fire a send self() from it, like what is shown in the docs here: docs
But if im piping the events through the component to the liveview, does it even make sense for it to be a live_component, or could I just as well use a function component? (im a little lost on when to use what, and for what purpose)

I need good suggestions for how to architecture a screen like this, and comments about best practice in the phoenix/liveview world.

Bonus info: This is an admin tool, where each tab is a specific view, for working and tweaking specific parts of a project.

I think if your state is complex enough to divide it up, you should put that in a live_component as they can manage their own state, otherwise its normally best in a function component. I think this is where your head was already at anyway.

Here is a layout in a recent project that is similar in structure to yours, just as an example I guess.

In this, each “green” live component manages its own structs & form – specific to that live component and passes data back up to the parent. This could all be done with function components but the main live view would get very bloated. Technically the tab bar is a function component.

So I think this is a spin on your “after”, where instead of nesting the tab content inside a tab component, I cut out the middle man. The “which tab are we on” state isn’t sufficiently complex to need a separate live component in my case. I have used this kind of layout many times on complicated multi-stage forms.

All the tabs are rendered into the page at once and just display: hidden in this case. This does have an impact on the initial page load time if the data structure (well, if the html derived from the datastructure) is complex, so if you can, I would try and only render one at a time. In this case it’s simpler to just load them all instead of passing data up and down and re/de-hydrating all the time and it’s an internal tool so the extra 100-300ms initial render time isn’t too problematic.

So I guess my advice is to start with functional components until the state being tracked is complicated enough that you want to modularise it; at which point you can put it into its own live_component and focus your state changes through messages. This send pattern is really great when you want to only send “valid” data back up to the parent as you can localise all the validation cruft to section-specific changesets & “views” (live_components).

Hope some of that is helpful?

But if im piping the events through the component to the liveview, does it even make sense for it to be a live_component, or could I just as well use a function component? (im a little lost on when to use what, and for what purpose)

If I understand what you mean here, I would not pipe the “handle_params” event down to the component, just use that to determine which to render?

3 Likes

If you’re tracking the selected tab as an assign, you can keep that in the liveview and use live/function components for the content. The content could be a slot in your tab component or sit below it.

I aim for function components but if I want to handle state and events, then consider a live component.

1 Like

The state has to be synchronized across the different tabs, as some of them work on the same data, which is also why, I think its easier to have the liveview be in charge.

My real big question is, the modals I show (which are the ones actually modifying the data, the tabs actually only display data) where should their displaying originate from? Currently It feels kind funky to make a patch call from the tab, only to then have the liveview pass down to the tab, that it should now display the modal component. It also couple the tab to the liveview, preventing reusability (I could probably fix this by injecting the destinations, into the component)

Any thoughts on this?

It looks to me like the liveview should be in charge of showing and hiding the tabs and modals, as they’re basically the same concept no? And whether a modal is visible is “view state”, not “tab state”?

e: Not sure I’m being real helpful down here, might be a bit too tangential to your question as your coupling is really just about an event and effect.

Can you make the argument that the state is related? What about the tab changes when the modal changes and does that warrant the coupling?

(You may update the data between the tab-view and modal-edit on change but IMO this is actually an anti pattern as the modal isn’t “saved” yet, otherwise just edit in the tab – so I would not view this as a reasonable reason to couple and if done would prefer to only send valid data to the tab-view anyway meaning the state is not actually “related” but “derived”?)

1 Like

It sounds like state belongs in the liveview.

You can have one modal component for everything and dynamically set its content. Here is some inspiration:

The tabs more or loss add/edit/delete data, from a list.
It makes sense for me that the live component in the tab, could be in charge of the modals for editing, but it feels wrong to do it through a patch, as we do now, as that is hard coupling the tab to the liveview.

However if I show the modal without a patch, then the url won’t update.

Any ideas here?

So ive been thinking, i think there is 2 ways to go about this:

  1. The component is selfcontained, it handles when modals are showed. I also include a Call to the parent liveview, for when it needs to update the url, and update the State for the other tabs that use the same dataset.

  2. I make it a function component, and let the liveview handle everything. (Gonna make the liveview Big, but Its probably were the events and State belong in this case)

Any comments?

Btw is there any Way to split a liveview into multiple .ex files?

You can split any module into submodules and import/etc those, or you could split a live view into a collection of live components (:thinking:).

Not sure I can judge the validity of any approach much more from the outside beyond what I wrote before – showing tabs and showing modals both seem like “page content” and so should be probably controlled by the “page” (which is our liveview here), separating them also means you can reuse the modal (not the modal wrapper but maybe the actual modal form) in other places without requiring a tab to control it. I just can’t see the reasoning of tying them together but I don’t have the whole context.

Honestly it sounds like you’re over thinking it a bit? Build it one way and discover why you hate it and build it the other way (and discover that way also sucks for other reasons :wink:).

2 Likes

Just to clarify, the modal is already a component.

Think of it like this:
In the tab, we have 3 tables, with each row corresponding to an entry in the db of that data type (belonging to the model im currently working on.
The tab has buttons for adding,editing,deleting rows, where add and edit prompts a modal.

Before it was made weirdly, and each tab was Its own liveview, and because of that all modals are presented by a patch call.
The above feels wrong, as i dont Think a component should handle navigation, or be linked to the liveview it lives in.
On the other hand, the modals only work on the data, in the component, so the argument could be made that Its couple to that component.

(Im general i Think it should live in the liveview, but when i have 9 tabs, all with different events and modal to show, the liveview Will very quickly become a very Long file. And that might be the only reason im considering having the component display the modals…)

I’ve just hit this very same problem.

I’ve got a sidebar component for top level navigation in a layout. It’s inner content is made up of LiveViews that provide the views for each item in the sidebar. Except one I wish to be a tabbed view. I have 3 tabs, each provide CRUD like operations over entities within the app and it’s a mess. I can totally see how having 9 tabs in this scenario doing the same CRUD like things would get insane.

I feel like the obvious solution is leverage the router as much as possible and then use live_patch to navigate between tabs. But then you’re left with a trade off when it comes to the the live action:

  • Use it to dictate which tab is active and you can’t specify CRUD or other actions
  • Use it to specify the CRUD or other action and you can’t specify the tab

The only workarounds I’ve found are to parse the URI in handle_params to infer either the tab or action.

I’ve found another work around just now using the metadata field of the live macro in the router.

:metadata - a map to optional feed metadata used on telemetry events and route info, for example: %{route_name: :foo, access: :user}. This data can be retrieved by calling Phoenix.Router.route_info/4 with the uri from the handle_params callback. This can be used to customize a LiveView which may be invoked from different routes.

With a route of:

live "/tabs/a", TabsLive, :index, metadata: %{tab: :tab_a}
live "/tabs/a/:id/edit", TabsLive, :edit, metadata: %{tab: :tab_a}
live "/tabs/a/new", TabsLive, :new, metadata: %{tab: :tab_a}
live "/tabs/b", TabsLive, :index, metadata: %{tab: :tab_b}
live "/tabs/b/:id/edit", TabsLive, :edit, metadata: %{tab: :tab_b}
live "/tabs/b/new", TabsLive, :new, metadata: %{tab: :tab_b}

Then in TabsLive handle_param you can fetch the tab from the metadata specified above by doing:

# I don't think the host really matters here, but not 100% sure
%{:tab => tab} = Phoenix.Router.route_info(AppWeb.Router, "GET", URI.parse(uri).path, "127.0.0.1")

You can then plop the tab in the assign and use it to display/interact the tab component.

This makes it a little less cumbersome in the sense that you don’t need to parse anything specific out of the path by hand. Albeit, the computational work of parsing the URL is the same.

2 Likes