Live View Streams with Index returns error

Does anyone know how to use index when using Phoenix Live View stream/4 ?

for example when I do this:
<%= for {index, f} <- Enum.with_index(@streams.changeset) %>

I get:

[error] GenServer #PID<0.26435.0> terminating
** (ArgumentError) streams can only be consumed directly by a for comprehension.
If you are attempting to consume the stream ahead of time, such as with
`Enum.with_index(@stream.changeset)`, you need to place the relevant information
within the stream items instead.

I need to use the index inside my form for loop

Can you expand on why you need to use the index?

The entire point of streams is to not keep the entire data set in memory, and you’d need that to have an index. If you can expand on what you’re doing with the index, perhaps someone here can suggest an alternative solution.

Thanks @zachallaun for the fast reply.
Sure I can expand. So, for example, right below the for loop with streams I’ll have something like:

<div id={"some-id-name-#{index}"} class="form-row form-row-indent-sm">

And so on, for example another use I’ll have to make with index is to show options on a select given the index
Like this:

<div class="flex-1">
  <%= select f, :type,
    get_options(index),
    disabled: @disabled
  %>
</div>

Is this enough? I know that usually people use IDs from changesets but in my case I wont have a changeset, at least not when the form renders for the first time, I’ll have to show empty inputs in this case.
Please tell me if you need more info.
Thanks so far!

Have you tried the suggestion in the error message?
How do you currently assign the stream and could you “decorate” the items with an index before streaming them?

Have you tried the suggestion in the error message?

I did after you mention but it was logic not to work since theres no :stream inside the assigns.

How do you currently assign the stream and could you “decorate” the items with an index before streaming them?

This is how I’m assigning:

def update(assigns, socket) do
    socket =
      socket
      |> assign(assigns)
      |> stream(:changeset, [to_form(User.changeset(%User{}, %{}))])
    {:ok, socket}
  end

As you can see I’m assigning an empty changeset to the stream in the update funciton for the first render, I’ll update the changeset in some event later on.
Actually I’d rather even just send an empty list to the stream, but then I wouldnt have a Phoenix.HTML.Form.

Hmm, is it really worth streaming a single form struct?

Streams were designed with collections in mind and the design pattern from form components created via the generators store the form struct as an assign which also makes it easy to update in the form validation callback like so:

  defp assign_form(socket, %Ecto.Changeset{} = changeset) do
    assign(socket, :form, to_form(changeset))
  end

  def update(%{list: list} = assigns, socket) do
    changeset = Todos.change_list(list)

    {:ok,
     socket
     |> assign(assigns)
     |> assign_form(changeset)}
  end

  def handle_event("validate", %{"list" => list_params}, socket) do
    changeset =
      socket.assigns.list
      |> Todos.change_list(list_params)
      |> Map.put(:action, :validate)

    {:noreply, assign_form(socket, changeset)}
  end

I understand, but see this case. I have a simple form with 2 inputs, a select and a text, this is a component, and below this form will be a button to add form that when clicked the same form will duplicate one above the other. So we can have multiple forms in there with no changesets, they can all be empty but I’ll have to use stream since these forms will be a list of forms. So this is why I need to use stream in here.

I still think this is is sort of questionable. Are there going to be > 1000 forms? I can’t imagine how that would be good UX. If you have N forms I’d still just have N form components and probably make each of them a live component so that they can each manage their own events.

That’s plausible.
But then would you have just a simple for loop through the list of forms without using the stream?

Yeah or depending on how your page is set up iterate through a list of users and then pass that user to a live component, and have each component generate their own form.

1 Like

With the latest version of LiveView, you can use the new sort/drop_param and options for cast_assoc/embed in conjunction with inputs_for for Dynamically adding and removing inputs like so:

<label class="block cursor-pointer">
  <input type="checkbox" name="list[emails_sort][]" class="hidden" />
  add more
</label>

<input type="hidden" name="list[emails_drop][]" />
1 Like

Thanks guys @benwilson512 and @codeanpeace , looks like elixirforum was off so we ended up coming up with our own solution which was implementing the streams although we know it’s not gonna be of great improvement on performance but still will be good for further improvements and good practices, so this is kinda of what we did:

<%= for {id, f} <- @changesets do %>
  <.form for={f} id={id}...>
    <div .../>
    ...
  <./form>
<% end %>

This worked for me, so if you guys have any thoguhts, cheers and thanks again!

While this specific use-case didn’t end up needing the stream, my use-case is a little more reasonable in that I need to simply know which position a given element is for tracking purposes; i.e “send me which index the item was at to GA”. I suspect I’ll end up with a virtual attr on my schema or some such thing like the docs suggest.