Code Sync: Keynote: The Road To LiveView 1.0 by Chris McCord | ElixirConf EU 2023
Comments welcome! View the #code-sync and #elixirconf-eu tags for more Code Sync and ElixirConf EU talks!
Code Sync: Keynote: The Road To LiveView 1.0 by Chris McCord | ElixirConf EU 2023
Comments welcome! View the #code-sync and #elixirconf-eu tags for more Code Sync and ElixirConf EU talks!
Just watching the intro now
If you haven’t seen it, go watch it!
This was my favorite Phoenix talk in a long time. During the last 1/3 of the talk, it felt almost like Chris had seen my code and built one solution after another for each headache I’ve encountered with LiveView the past several weeks!
Is there a link anywhere to the form demo repo?
Is there any transcript?
OK, I haven’t listened to the talk yet…however, the introduction was definitely worth it.
As Chris put it, the intro was “unnecessary, but fantastic”.
Enjoyed watching the whole thing. And made me want to get back to my desk asap .
@chrismccord Were you planning to open source the todo_trek
repository? I was hoping to learn from studying it, just like I have been doing with LiveBeats.
He says in the talk that he will
Oh great! Looking forward.
I just spent a bunch of time going back and forward watching the video trying to implement the nested forms thing, and I got stuck trying to solve some issues. This post is about my findings.
Check out the video, it’s just ~15 sec. There are two bugs:
The “Add Status” btn is similar to lines 45-48
What I do know now, is that all this bugs are solved IF… drum rolls please
IDs. Maybe some other attribute will work to, but ids are fine. We need something that triggers an update when we need a fresh new element, that includes a marker that helps whoever is confusing these elements.
For the elements outside inputs_for
(add rows & cols), I’m using this:
n_rows = Phoenix.HTML.Form.inputs_for(form, :field_name) |> length()
...
~H"""
<input [..] id={"add-row-#{@n_rows}"} />
"""
For the inside ones, it’s simpler. LiveView’s inputs_for
(different from the Phoenix.HTML.Form
one), adds a very self explanatory field _persistent_id
to every nested form, which can be accessed like this:
<.inputs_for :let={f_status} field={@statuses_field}>
<input type="checkbox" [...] id={"row-delete-#{f_status.id}"} />
</.inputs_for>
So, how is your weekend going?
After the first click “Add status” (add row), the checkbox stays selected, so when we “Add a role” (column), both a row and a column are added since both were checked. This is actually quite natural behavior, why would phoenix send updates on a static element? Actually I’m quite baffled it even worked in the demo at all.
Try it in Safari 16.3? What version of Safari are you running? The demo behaviour, but not necessarily correct behaviour is exhibited for me in that, but not other browsers. I agree that the expected behaviour is to retain the check like any other input where liveview tries to not clobber user input it doesn’t know about.
Changing the id
is a good trick. I use that for an fuzzy search input where once the user clicks a value, the input needs value needs to change. I swap the input id from #search
to #search-selected
and then my value={@selection}
change takes hold. That input is pretty specifically behavioured though, the user has to “unlock” it to search again so its a bit easier to know when to swap the id back and clear the selection.
I built a version that replicates mccords demo with just LV.JS
and regular buttons. I would recommend if you’re able to just generate persistent uuids for the nested resources and pass them back and forth (so use I forgot after a f_nested[:id]
instead of f_nested.id
). You can avoid extra code and morphdom quirks.git reset
to re-add lv 0.19, so the current main with persistent_id
actually avoids this need or with 0.18 you can do the little value dance. You can also cut a lot of the cruft out by just dispatching your own custom event or hook that may just directly insert elements etc. This is definitely a “you can do it with just LiveView.JS”, not advocating that it’s the clearest or most maintainable way.
The checkbox version is certainly nicer. You could also probably make that work by generating a new id server side and pushing that down with each validate event, at least for the “add new” one, or again, another hook.
The real magic is in sort_param
and drop_param
, so it seems pretty flexible even if the checkbox idea isn’t as transportable as you might hope.
Chris did also mention some patches he had to do to the javascript side, and there is a branch with the stream limits and phx-viewport-top
stuff, so what he demoed isn’t quite into the main branch yet, perhaps things will smooth out a bit.
<.simple_form
for={@form}
id="todo-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<.input field={@form[:title]} type="text" label="Title" />
<h1 class="text-md font-semibold leading-8 text-zinc-800">
Invite Users
</h1>
<h1>
Note that setting the *attribute* value="x" has different behaviours
to editing an input, or setting element.value.
</h1>
<ul class="list-disc">
<li>Phantom delete bug (JS.remove_attribute("value") fixes this):</li>
<li>Add new</li>
<li>Enter 1 1</li>
<li>Add new</li>
<li>Enter 2 2</li>
<li>Remove first (1 1)</li>
<li>
Enter 1 1, element disappears because the "delete" input inherits the old
value from the removed "delete" input because they share the same id after
the remove event (and so they are likely the exact same element in the DOM)
and the second morph puts that attribute back. This is where just generating
actual uuids helps as you get actually unique elements for each and can just
set the value to the index and bang the input. The fix here *works* but probably
progressively degrades as forms get more complex and you have different effects
per checkbox.
</li>
</ul>
<div id="notifications" class="space-y-2">
<.inputs_for :let={f_nested} field={@form[:notifications]}>
<div class="flex space-x-2">
<input
id={"#{f_nested.id}_order"}
type="hidden"
name="todo[notifications_order][]"
value={f_nested.index}
/>
<input
id={"#{f_nested.id}_delete"}
type="hidden"
name="todo[notifications_delete][]"
data-js={
JS.set_attribute({"value", f_nested.index})
|> JS.dispatch("input")
|> JS.remove_attribute("value")
}
/>
<.input type="text" field={f_nested[:email]} placeholder="email" />
<.input type="text" field={f_nested[:name]} placeholder="name" />
<button
type="button"
class="border border-zinc-800"
phx-click={JS.exec("data-js", to: "##{f_nested.id}_delete")}
>
Remove
</button>
</div>
</.inputs_for>
</div>
<ul class="list-disc">
<li>
Need to toggle disabled value so we don't send up to the server on every input,
which would add a new child.
</li>
</ul>
<input
disabled=""
id="add-new-after"
type="hidden"
name="todo[notifications_order][]"
value="new"
data-js={
JS.remove_attribute("disabled")
|> JS.dispatch("input")
|> JS.set_attribute({"disabled", true})
}
/>
<button
type="button"
class="border border-zinc-800"
phx-click={JS.exec("data-js", to: "#add-new-after")}
>
Append
</button>
<:actions>
<.button phx-disable-with="Saving...">Save Todo</.button>
</:actions>
</.simple_form>
I think that refers to Support ordering within inputs_for (#2570) · phoenixframework/phoenix_live_view@1e169b8 · GitHub, which is merged into main.
Safari: 16.4 / Firefox 112
Yeah, I’ve been toying with the idea of just writting some JS hook and just code against ecto’s interface. In many ways seems easier and more reliable, but I do hope we reach that state where this is not necessary.
Thanks for the example! I hadn’t played yet with JS.exec
.
You say self-explanatory, but I’ve been struggling to figure out what is it used for? I have these in my generated output:
<input type="hidden" name="key[phrases][0][_persistent_id]" value="0">
<input type="hidden" name="key[phrases][0][id]" value="0187b3c0-908d-7185-8c58-3516afe797a7">
<input type="hidden" name="key[phrases_order][]" value="0">
I meant to say that the “contract” it offers is self explanatory: A persistent id that will be “attached” with the form no matter what mutation, sorting or deletion is performed (which is not the case of order, and you don’t always have an id like in your case).
I would need to digg deeper to understand how this id is used internally.
When Phoenix LiveView 1.0 releases, will the Optimistic UI be possible without using custom JavaScript?
yes an no. What you get currently optimistic UI wise is js commands and phx-disable-with, which allow you to provide immediate feedback when the user creates and deletes todos, but in a limited capacity. For example, check out the TodoTrek demo where I delete a todo, and it fades out immediately for 200-300ms, which would mask day to day latency. phx-disable-width also allows you to swap the content of a button to show loading states. You can also use JS.show|hide|toggle do to react-style loading placeholders. So you can do a reasonable amount of instant feedback on the client today, but it will be limited. For example, what you can’t do is dynamically render some server template on the client to for example make the todo look as if it’s inserted when it hasn’t yet made the round trip.
Thank you for your reply, and thank you for everything you are doing, all the good work, Phoenix, Phoenix LiveView and many more things! We can’t be grateful enough for all of this!
In my original post in 2019 I asked the following,
I had the Facebook/Messenger chat in my mind and I thought if that was possible with Phoenix LiveView.