A conversation around Liveview about its design patterns

I want to start by saying first, nothing I’m about to say it to belittle or undermine the efforts of others.
I respect your free labor and I respect the work you have already done for my benefit. :pray: Thank you!.

Ok with some of the formalities out of the way.

Issue #1
I just wanted to say. I hate modals for web forms!

Modal are great for:

  • Alerts
  • Confirmations
  • Basic prompts and small feedback forms.

Modals are not so great for:

  • Large forms
  • Things that require large visual space
  • Things that have managed state (Think clicking away from form before saving and losing your work)

We should not be using modals as a default example since many people will just stick with it.

First more times than not the form being generated is not small enough for a modal.
Second, one misstep and clicking away from the modal and you lose all your work in the form.
Third, its just ugly and it makes the form look and feel like a second rate citizen when it often deserves its own page.

Maybe the phx.gen.auth could generate a modal for login that seems to make more sense :person_shrugging:

I’m sure I’m not the only one who feels this way about the generated modal. I’d like to see what others think about good solutions for this.

Issues #2
I think in addition to the choice of modal use the way we use Index vs Show is also something that bothers me about Liveview generators.

#2:1
The fact you have two separate Liveviews that can in short basically do the same thing but only have a small variation from each other makes it more confusing for new comers since this is not something you see in may other frameworks.

I don’t think the saving you get sending a little bit of html over the wire vs just having it go to another LV via push_patch or push_navigate is worth mixing in the new/edit forms into both the index and show LV.

#2:2
Beyond confusion for the New / Edit forms via modals in both Index and Show the routes get a little wonkey as result of both edit/new forms in both the index and show LV.

I’ve starting to create what I call the “upsert” Liveview that represents both the new and edit form but as its own dedicated LV vs just an embeded modal via a LV component. Not saying its the best choice but I still find myself picking it over the default way.

Issue #3
live_sessions vs Authorization logic mixed in.

I still find working in layouts to be a real pain. LiveSessions made it possible to make it easier to make a segregated Admin section vs Normal users sections. This pattern feels at odds consolidating layouts and trying to make a single form for both admin users and normal users where conditions show elements not layouts.

This has been the biggest of the struggles for me when it comes to trying to find a pattern that works well for me.

I would really like to hear from others on #3 about how they are working with Admin logic mixed in their layouts vs making a whole admin section.

Final thoughts:
I would lke to have a constructive convo here so please be mindful of others who have done hard work to get us to this point before complaining or making a statement here. That said I think we can possibly do better on the issues I’ve outlined and I would like to know others also think. Thanks.

4 Likes

This a pain all our technology faces, it’s not like there is a magical solution in other frontend frameworks, moreover I would say that this is very manageable with some custom abstractions (I think libraries in such cases are counterproductive in 90% of cases).

I’m gonna hold off on spamming in this post because I want to allow for other to have chance to talk before I go off on my tangent.

That said this is a topic (#3) I feel I know the least about and struggle the most with.

For example right now I’ve been making a whole admin section and ripping out the edit/new logic from the forms where they are non admin, but honestly I would almost rather just have a single layout for both admins and normal users the more I keep pushing for this. I’m not really sure how that is meant to work in the context of live_sessions and how to keep from remounting all the time.

I guess its fair to say its a hard problem.

This a pain all our technology faces,

This is a very common issue, I’ve had this issue pretty much on 90% of projects I worked on across phoenix, angular, android, and the reality is that there is no silver bullet for this, splitting them separately is not an option (because you need to maintain 2 versions) and making a generic interface goes the same way (a lot of times requirements around what can and can’t an admin do can go down to very specific things.

Assuming we’re talking admin and public sections are separated (as opposed to “live editing” the public view), I prefer and even strongly advocate for separate forms. Sprinkling conditionals around a template bothers me far more than some duplication in another file. It gets especially hairy when you bring in authorization roles then you get into very hairy conditionals like “user is admin and has edit role”.

The worst thing that can happen if you make a mistake with separate forms is that you forget to show a field on one of them. The worst thing that can happen if you use a shared form is you show someone something they aren’t supposed to see.

To me this is one of example of striving too much for DRY. Assuming you’re using components for your form elements, this shouldn’t be a big deal but, as always, YMMV.

EDIT: Just to comment on the modals, they don’t really bother me as a bad pattern as they are done by generators because they are routable. The fact that they are modals just becomes a presentation detail. I can see your argument, though.

9 Likes

Yeah, I struggled with this too and I realized that I was “doing it wrong”. I was trying to find a way to compose/reuse the layouts and I either ended-up with something that made it hard for me to know which layout was being rendered or a layout full of conditionals. Then it dawned on me that I should keep the layouts fully separate and break the shared bits into components, that can now be reused between both admin and normal layout (or even other pages).

12 Likes

When an admin system is involved, I would also have a sanity check at the data level, because you can never trust frontend, but this varies a lot and cannot be categorized on a general level.

I think there should be some guidelines to follow in different cases and some design considerations, that you could leverage from the start, because a lot of times these requirements can be reformulated in a separate admin interface (but not always).

3 Likes

This is a really interesting take.
So is that to say your layout tend to be very sparse in terms of content?
I’d guess most of the meat of the html is in the components as a result.

Of course! I’m very, very against doing it any other way. To clarify, I’m talking about messaging and other UI-only things that might get sprinkled into the forms. But even then, it’s still possible to show a field that, even if can’t do any harm, you still might not want your users to see.

I was actually confused by your layout comment in your original post. Are you literally talking like app.html.heex or do you mean the heex in render/1?

Yeah sort of more about literal layouts, in short you use live_sessions typically to create an admin layout vs a normal user layout. Some mount checks a given user is or not an admin in say admin live_session and redirects non admins to a unauthed render.

If you are using totally different layouts, you could abstract this at the router level with some nice and concise macros, you don’t have to go down to the liveivew, but you would have to tell me more about the possible constrains you are facing.

Here’s my current learning project.

In my router I have some routes that is for my admin layout

This is for admin who can perform crud operations.

Then I have my normal user layout (public facing) show / index LV.

these routes are segergated by live_sessions which for my admin routes live_session :require_admin_user as you can assume require an admin user.

Alternatively I could have just used one route for the index / show respectively and had condition inside the LV to check if admin or not to allow for making edits ect.

Edit:
To @josevalim 's point I like the idea of breaking out the logic from the layout and moving it to the component and just loading the components where they are needed. I need to look into that more and I feel that is likely the best choice at the moment.

Oh ya, your layouts are huge! I always extract components for nav and whatnot. You can even eliminate conditionals with components, eg:

attr :current_user, MyApp.Accounts.User, default: nil
# other attrs
def nav(%{current_user: nil} = assigns) do
  ~H"""
  <div>logged out stuff</div>
  """
end

def nav(assigns) do
  ~H"""
  <div>logged in stuff</div>
  """
end
5 Likes

Does my layouts look big in this dress? :joy:

You are never suppose to tell me what you really think…

I know, I’m working as hard as I can to get this layout to fit in to a bikini by summer.

All jokes aside it is not well done in my example so take my example with a grain of salt as there are many
“what is he doing here” in that project. I agree though it makes more sense to break out a lot of what I have in my layouts.

1 Like

I’m sorry, it wasn’t my intent to body-shame your layouts :face_with_peeking_eye: :sweat_smile:

3 Likes

I should not make such kind of jokes. Sorry.
Also valid point given I shared that project.

I’m looking at this issue from a fresh project.

$ mix phx.new tmp_project
* creating tmp_project ....
$ mix phx.gen.auth Accounts User users
..
Do you want to create a LiveView based authentication system? Y
..
$ mix deps.get
$ mix ecto.migrate
...
$ mix phx.gen.live Blog Post posts title:string description:string body:text published_at:date draft:boolean
* creating lib/tmp_project_web/live/post_live ...
...
Add the live routes to your browser scope in lib/tmp_project_web/router.ex:

    live "/posts", PostLive.Index, :index
    live "/posts/new", PostLive.Index, :new
    live "/posts/:id/edit", PostLive.Index, :edit

    live "/posts/:id", PostLive.Show, :show
    live "/posts/:id/show/edit", PostLive.Show, :edit
...

At this point I have a choice as to where to define my routes.

Looking at our router we have 3 current live sessions we could use to define the posts routes above.
redirect_if_user_is_authenticated, require_authenticated_user and current_user.

We could just assume that logged in users for the moment are admins and non logged in are not admin.

So in short all users should have access to the PostLive.Show, :show and PostLive.Index, :index but not PostLive.Index, :new, PostLive.Index, :edit or PostLive.Show, :edit.

So lets start with how/where to put these routes.

Per the example in the docs:
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Router.html#live_session/3-examples

In the example above, we have two live sessions. Live navigation between live views in the different sessions is not possible and will always require a full page reload. This is important in the example above because the :admin live session has authentication requirements, defined by on_mount: MyAppWeb.AdminLiveAuth, that the other LiveViews do not have.

So with that in mind do we want to duplicate the index and show views for logged in users. Example.

Say we do something like this.

 scope "/admin", TmpProjectWeb do
    pipe_through [:browser, :require_authenticated_user]

    live_session :require_authenticated_user,
      on_mount: [{TmpProjectWeb.UserAuth, :ensure_authenticated}] do
...
      live "/posts/new", PostLive.Index, :new
      live "/posts/:id/edit", PostLive.Index, :edit
  
      live "/posts/:id/show/edit", PostLive.Show, :edit  
    end
  end

  scope "/", TmpProjectWeb do
    pipe_through [:browser]
...

    live_session :current_user,
      on_mount: [{TmpProjectWeb.UserAuth, :mount_current_user}] do
...      
      live "/posts", PostLive.Index, :index
      live "/posts/:id", PostLive.Show, :show
    end
  end

We lose the live navigation going between creating /editing and viewing. So whats the first right choice here?
Going back to @josevalim comment, It would make sense to duplicate these and abstract that condition to the components inside the liveviews.

scope "/admin", TmpProjectWeb do
    pipe_through [:browser, :require_authenticated_user]

    live_session :require_authenticated_user,
      on_mount: [{TmpProjectWeb.UserAuth, :ensure_authenticated}] do
...
      live "/posts", PostLive.Index, :index
      live "/posts/new", PostLive.Index, :new
      live "/posts/:id/show/edit", PostLive.Show, :edit  
      live "/posts/:id/edit", PostLive.Index, :edit
      live "/posts/:id", PostLive.Show, :show
    end
  end

  scope "/", TmpProjectWeb do
    pipe_through [:browser]
...

    live_session :current_user,
      on_mount: [{TmpProjectWeb.UserAuth, :mount_current_user}] do
...      
      live "/posts", PostLive.Index, :index
      live "/posts/:id", PostLive.Show, :show
    end
  end

Uhg, this is not the best example given I’m really talking about admins vs non authed users vs logged in but not admin users.

But in this case we could say the components inside PostLive.Index, :index and PostLive.Show, :show could have the conditions that care if current_user is present or not.

Or rather maybe all the routes are in the :current_user session and the condition dive deeper into if they are a admin or not ect.

I would bet a lot of new comers hit this point and are not sure of the right direction to go.

I don’t worry about this at all. I mean, many people still work in a way where they are re-building the entire world on every request! Hopping from an admin page to a non-admin page probably isn’t even happening all that often, it all depends on the app, though. That’s the worst thing about programming really: it always depends :face_with_spiral_eyes:

I currently have one very simple app (a contract) and one more complex app (for work) on the go. In the simple one I do it like in your first example. In my bigger app, the admin area has completely separate LiveViews and/controllers from the public facing area—there often isn’t even a 1:1 correlation between admin and public pages.

1 Like

and like it was said.

Then it dawned on me that I should keep the layouts fully separate and break the shared bits into components, that can now be reused between both admin and normal layout (or even other pages).

I think my mistake is that I don’t think I have done the part of creating the shared components.
I need to do better at creating components

I also don’t care for modals, and only use them for alerts and confirmations. In my opinion, they seem particularly ill suited for mobile devices.

4 Likes