Is it possible to make a live-view form with infinitely-nested repeated components?

In my attempt to understand how Phoenix LiveView works, I’m trying to do a somewhat todo-list app, but with a simple twist - each todo item could have an sublist of same-kind todo’s and it goes as far as user needs, similarly to how things are organised in Emacs orgmode.
I store stuff in database like that:

  • Checklists.Checklist - ecto schema that has a jsonb field with entries list, root entity for each checklist, has a list of Checklist.Entry
  • Checklist.Entry - ecto embedded schema that has an optional list of another Checklist.Entry sub-entities

LiveView structure organized in more or less same way:

  • ChecklistLive.Edit - live_view, root one
  • ChecklistLive.EntriesListComponent - live component, receives an entity and renders it’s list of entries using ChecklisterWeb.ChecklistLive.EntryEdit
  • ChecklisterWeb.ChecklistLive.EntryEdit - a form for an Entry and if needed, to list entries sub-entries.

Because of jsonb structure of all entries, when subentity is added/updated, I need to re-save whole checklist and while it’s not optimal, it works for me and way easier than get such tree structure from postgres and/or store it properly with traditional DB structure. Because of it, I update whole checklist on a root level of live view, inside ChecklistLive.Edit process.

It looks and works pretty poorly now, but I achieved adding new entries of checklist and updating them on blur of input. If you don’t go further than first level, it works as expected.
But when I add an sub-entry to any of first level entries, backend part of it works fine - entries are added/updated, whole checklist is saved and when I reload page, I got the structure that I expect.
However, on live view side, not everything is so bright:

  • When I add subentry to existing first level entry, I receive no visual confirmation of it. Subentry looks unsaved until I reload a page. It’s like code that compares old checklist and new checklist doesn’t see this change in sub-entries. If I update/add first level entry, it will work as expected, again.
  • For other fields (timestamps) change happens! So, when I add subentry, I see timestamp of checklist changed, but no subentry added
  • Again, if I reload the page from scratch, added subentries are shown and ready to create their own subentries with similar behaviour

If you’re interested, you can check the code here: GitHub - maximkuzmin/checklister: My approach to build infinitely-nested todo-list (Emacs-orgmode-like) with Phoenix & LiveView, however, since I’m not very profound un Ecto/Phoenix/LiveView part of elixir, you can find it not very best-practicey (and maybe this is a real reason, but not sure where to look) :smiley:

Because of the very same reason, my question is - is this behaviour a result of some live-view limitation on nested components or I’ve missed something in documentation?

Have you got ids on the html elements? Maybe the li needs one.

You can certainly nest until your heart is content. I have a SQL query builder that nests and I had some fun issues when I was missing ids.

1 Like

Hey, thanks for reply!
From reading documentation I understood that only root element of live component need ids and added it, but will play a bit more with you suggestion.

I had some funky behaviour sometimes as well, e.g. when deleting an item of a list, a nearby button would disappear as well, or I would see an element twice, or an added element would replace a button etc.

Backend state was always ok, but I think the DOM patch on the frontend got confused.

Solution for me was to give the DOM elements an ID, as @cmo mentioned as well.

So instead of

<div :for={user <- @users} class="...">
  <%= %>

I’d do

<div :for={user <- @users} class="..." id={}>
  <%= %>

If you’re using bigint ids, you need to prefix otherwise you’re going to run into conflicting ids:


LiveView also use to complain when using bare ints as ids but I just noticed it no longer seems to!

When it comes to nested forms where you can add and remove elements that may not be saved yet, I make ids with the form’s indices:

<inputs_for :let={ff} for={@form[:child]}>
  <input ... id={"child-#{@form.index}-#{ff.index}"}>

This is gives you a predictable id scheme that is shared between persisted and unpersisted records.