Keynote: The Road To LiveView 1.0 by Chris McCord | ElixirConf EU 2023

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!

28 Likes

Just watching the intro now :lol:

If you haven’t seen it, go watch it!

1 Like

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?

8 Likes

Is there any transcript?

1 Like

May be not exactly what you want but there is a default YouTube transcript available:

2 Likes

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”.

1 Like

Enjoyed watching the whole thing. And made me want to get back to my desk asap :slight_smile: .

@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.

2 Likes

He says in the talk that he will :stuck_out_tongue:

3 Likes

Oh great! Looking forward.

1 Like

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:

  1. 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.

The “Add Status” btn is similar to lines 45-48

  1. When I deleted row indexed 1, phoenix re indexed all the rows down, giving me a new indexed 1 checkbox and for some reason proceded to check this new index 1 checkbox (the button is similar to lines 36-39 in the picture). Then when I click to delete row 0, both row 0 & 1 are deleted. I’m not sure if this is a browser thing (tested on firefox & safari), a phoenix js thing or a morph dom thing.

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? :stuck_out_tongue:

5 Likes

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? :upside_down_face: 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 f_nested[:id] instead of f_nested.id). You can avoid extra code and morphdom quirks. I forgot after a 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>

1 Like

I think that refers to https://github.com/phoenixframework/phoenix_live_view/commit/1e169b8c29ad108c5be61a69d0008e14c99bfa1c, which is merged into main.

1 Like

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">
1 Like

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.

3 Likes

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! :pray: :heart:

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.

Hi Everyone! Now that LiveView 0.19.1 is released, when is LiveView 1.0 expected?
I’m asking this, because I’m planning to invest a lot of time and energy in LiveView after 1.0 is released and all the learning material is updated.

When it’s ready :slight_smile: I gave our rundown of what we want to tackle for 1.0 in the talk, so follow the issues tracker to stay up to date. Now would be a great time to jump in!

9 Likes