---
title: "Dynamic forms with LiveView Streams"
description: "Learn how to use to_form/1 and LiveView Streams to create a dynamic list of forms."
---

# Dynamic forms with LiveView Streams

[Annie Ruygt](https://annieruygtillustration.com)

In this post, we’ll develop a dynamic list component using the new LiveView Streams and the enhanced form features introduced in LiveView 0.18/0.19. Get ready to discover how to create a responsive list component with interactive capabilities like adding, editing, and deleting items. Fly.io is a great place to run your Phoenix LiveView applications! Check out how to [get started](https://docs.fly.io/elixir)!

You developed a component that lets you add and remove items in a “has_many” relationship. Take, for example, a shopping list where it can *have many* items. You’ve got a cool form that lets you send data for new items, and it works really well:

However, you’re still not entirely happy with the result. When you make a mistake while adding an item to the list, there’s no way to edit it. How can you make the list items editable right in the list component, without the need for a modal?

Well, what if we use individual forms for each list item instead of a single form to add and associate them with the list? That’s a simple task, we can render as many forms as we want. We’re going to use the [to_form/2](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#to_form/2) function to create form structs from changesets, and then we’re going to use those structs to render our form components.

And another thing: once we’ve fixed up our form component, we’re going to see how to use [LiveView Streams](https://fly.io/phoenix-files/phoenix-dev-blog-streams/) to manage and manipulate our list items without the need to store them all in memory. Adding, editing, deleting, and resetting items in a collection has never been easier!

## Defining our component’s markup

Let’s begin by *refactoring* [our component](https://fly.io/phoenix-files/liveview-drag-and-drop/) and keeping only the header:

```
defmodule ComponentsExamplesWeb.ListComponent do
  use ComponentsExamplesWeb, :live_component

  def render(assigns) do
    ~H"""
    <div class="bg-gray-100 py-4 rounded-lg">
      <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
        <.header>
          <%= @list_name %>
        </.header>
      </div>
    </div>
    """
  end

  def update(%{list: list} = assigns, socket) do
    socket =
      socket
      |> assign(
        list_id: list.id,
        list_name: list.title
      )
    {:ok, socket}
  end
end  
```

In the assigns, we’re passing a struct `list`. It contains an id, a title, and a list of items, each of which has multiple attributes.

To generate these structs, their corresponding changesets, and the context functions that we’ll be using later, you can use the [phx.gen.context](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Context.html) generator:

```
mix phx.gen.context SortableList List lists title:string
mix phx.gen.context SortableList Item items name:string position:integer list_id:integer
```

Here’s an example list, containing a single item:

```
%ComponentsExamples.SortableList.List{
  __meta__: #Ecto.Schema.Metadata<:loaded, "lists">,
  id: 1,
  title: "Shopping list 1",
  items: [
    %ComponentsExamples.SortableList.Item{
      __meta__: #Ecto.Schema.Metadata<:loaded, "items">,
      id: 161,
      name: "chocolate",
      position: 0,
      status: :started,
      list_id: 1,
      list: #Ecto.Association.NotLoaded<association :list is not loaded>,
      inserted_at: ~N[2023-05-16 20:29:12],
      updated_at: ~N[2023-05-16 20:29:12]
    }
  ],
  inserted_at: ~N[2023-04-25 19:35:09],
  updated_at: ~N[2023-04-25 19:35:09]
}
```

Now, we need to iterate over the items of the list and render the forms. But before we dive into that, let’s update our assigns to include a form per item:

```
def update(%{list: list} = assigns, socket) do
+ item_forms = Enum.map(list.items, &build_item_form(&1, %{list_id: list.id}))

  socket =
    socket
    |> assign(
      list_id: list.id,
      list_name: list.title,
+     items: item_forms
    )
  {:ok, socket}
end
```

Let’s see the content of the `build_item_form/1` function:

```
defp build_item_form(item_or_changeset, params) do
  changeset =
    item_or_changeset
    |> SortableList.change_item(params)

  to_form(changeset, id: "form-#{changeset.data.list_id}-#{changeset.data.id}")
end
```

We receive an `%Item{}` and create a changeset from it. Then, we use the [Phoenix.Component.html.to_form/2](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#to_form/2) function. This handy function converts a data structure into a [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/3.3.0/Phoenix.HTML.Form.html), which we can use to render our [form/1](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1) components. Note that the `id` of the form has the `list_id` and the `id` of the item interpolated.

Check out how [to_form/2](https://www.phoenixframework.org/blog/phoenix-1.7-final-released#:~:text=New%20Form%20field%20datastructure) allows change tracking on individual inputs in our forms!

As a result, we have an assign called `:items` that holds the `Phoenix.HTML.Form` structs containing our element data. We iterate through each item in our assign and render a `simple_form/1` for each of them:

You can find the `simple_form/1` component—and many others—in `core_component.ex`

```
def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>
        <%= @list_name %>
      </.header>
+     <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
+      <div :for={form <- @items} id={"list#{@list_id}-item#{item.id}"}>
+       <.simple_form
+           for={form}
+           phx-change="validate"
+           phx-submit="save"
+           phx-target={@myself}
+           class="min-w-0 flex-1 drag-ghost:opacity-0"
+           phx-value-id={form.data.id}
+         >
+          <div class="flex">
+          </div>
+         </.simple_form>
        </div>
      </div>
    </div>
  </div>
  """
end
```

We define the events triggered on change or data submission, and we pass the item ID to these events using the `phx-value-*` bindings.

**Tip:** Make sure you’re using `LiveView 0.19` or a newer version, as it was not possible to send `phx-value-*` values with forms before this update.

Now, let’s add elements to the body of our item form. We start by adding a button to change the status of our element:

![This is a screenshot of the shopping list component. In the screenshot, there is a button highlighted by a red square. This button can be used to toggle the status of an item in the list.](https://fly.io/phoenix-files/dynamic-forms-with-streams/assets/form_1.png?card?1/3?center)

```
def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>...</.header>
      <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
        <div :for={form <- @items} id={"list#{@list_id}-item#{item.id}"}>
          <.simple_form ...>
            <div class="flex">
+.            <button
+               :if={form.data.id}
+               type="button"
+               class="w-10"
+.              phx-click={JS.push("toggle_complete", target: @myself, value: %{id: form.data.id})}
+             >
+               <.icon
+                 name="hero-check-circle"
+                 class={[
+                   "w-7 h-7",
+.                  if(form[:status].value == :completed, 
+                     do: "bg-green-600", 
+                     else: "bg-gray-300")
+                 ]}
+               />
+.            </button>
            </div>
          </.simple_form>
        </div>
      </div>
    </div>
  </div>
  """
end
```

This button is displayed conditionally when the list element has an `id`, indicating that it already exists in the database and its status can be edited. Additionally, we apply conditional classes to the `<.icon>` based on the item’s status, to change its color.

Now, let’s add the most important part, the text input to to edit each list item:

![This is a screenshot of the shopping list component. In the screenshot, there is a text input field highlighted. This text input field is used to create new items or update existing ones in the shopping list.](https://fly.io/phoenix-files/dynamic-forms-with-streams/assets/form_2.png?card?1/3?center)

We add two text inputs to send the parameters of our item: the `:name` and the `:list_id` to which it belongs:

```
def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>...</.header>
      <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
        <div :for={form <- @items} id={"list#{@list_id}-item#{item.id}"}>
          <.simple_form ...>
            <div class="flex">
              ...
+             <div class="flex-auto block text-sm leading-6 text-zinc-900">
+               <input type="hidden" name={form[:list_id].name} value={form[:list_id].value} />
+               <.input
+                 field={form[:name]}
+                 type="text"
+                 phx-target={@myself}
+                 phx-key="escape"
+                 phx-keydown={
+                   !form.data.id &&
+                     JS.push("discard", target: @myself, value: %{list_id: @list_id})
+                 }
+                 phx-blur={form.data.id && JS.dispatch("submit", to: "##{form.id}")}
+               />
+             </div>
            </div>
          </.simple_form>
        </div>
      </div>
    </div>
  </div>
  """
end
```

Let’s take a closer look at the elements we added. First, we include a hidden input field to store the id of the list to which the element belongs. This allows us to send it as part of the parameters for the `validate` and `save` events by setting the `value={form[:list_id].value}` attribute.

Next, we introduce a slightly more complex `<.input>` component with event options. By using the `phx-key` and `phx-keydown` attributes, we specify that pressing the escape key triggers the `discard` event sent to the server.

We have two ways to save changes: pressing Enter to trigger the `submit` event or allowing changes to be automatically saved when the input loses focus.

For elements that already exist in the database and are modified, the `phx-blur` binding comes into play. It automatically submits the form when the input loses focus, ensuring that changes are saved seamlessly.

Lastly, we add a button with an icon to delete existing elements from the database:

![This is a screenshot of the shopping list component. In the screenshot, there is a button highlighted in a red square. This button is used to delete the corresponding item from the shopping list.](https://fly.io/phoenix-files/dynamic-forms-with-streams/assets/form_3.png?card?1/3?center)

```
def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>...</.header>
      <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
        <div :for={form <- @items} id={"list#{@list_id}-item#{item.id}"}>
          <.simple_form ...>
            <div class="flex">
              ...
+             <button
+               :if={form.data.id}
+               type="button"
+               class="w-10 -mt-1 flex-none"
+               phx-click={
+                 JS.push("delete", target: @myself, value: %{id: form.data.id})
+                 |> hide("#list#{@list_id}-item#{form.data.id}")
+               }
+             >
+               <.icon name="hero-x-mark" />
+             </button>
            </div>
          </.simple_form>
        </div>
      </div>
    </div>
  </div>
  """
end
```

We trigger the `delete` event while simultaneously hiding the form of the item we wish to remove.

We now add the final section to our component. We include two buttons: one to add a new item to the list, and another to delete all items from the list.

![This is a screenshot of the shopping list component. Below the list items, there is a highlighted section containing two buttons. The first button is used to add a new item form to the list, while the second button is used to reset the list items.](https://fly.io/phoenix-files/dynamic-forms-with-streams/assets/form_4.png?card?1/3?center)

```
def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>...</.header>
      <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
        <div :for={form <- @items} id={"list#{@list_id}-item#{item.id}"}>
          ...
        </div>
      </div>
+     <.button phx-click={JS.push("new", target: @myself, value: %{list_id: @list_id})} class="mt-4">
+       Add item
+     </.button>
+     <.button
+       phx-click={JS.push("reset", target: @myself, value: %{list_id: @list_id})}
+       class="mt-4"
+     >
+       Reset
+     </.button>
    </div>
  </div>
  """
end
```

We sent events to the server using [JS.push](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.JS.html#push/1). Both the `reset` and `new` events receive the `list_id` as a parameter.

Awesome! We’ve finished the markup for our component!

We can easily render it by passing a list struct like the one we saw above:

```
def render(assigns) do
  ~H"""
  <div id="lists" class="grid sm:grid-cols-1 md:grid-cols-3 gap-2">
    <.live_component
      :for={list <- @lists}
      module={ComponentsExamplesWeb.ListComponent}
      id={list.id}
      list={list}
    />
  </div>
  """
end
```

But before we tackle handling all the events we defined, how about we optimize memory usage by using `Streams`? With just a few tweaks, we can implement this feature and ensure we’re not storing all the list elements in memory.

## Converting to Streams

To optimize memory usage, let’s start by making a small change to our assigns. We assign the stream items using the [stream/4](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream/4) function:

```
def update(%{list: list} = assigns, socket) do
  item_forms = Enum.map(list.items, &build_item_form(&1, %{list_id: assigns.id}))

  socket =
    socket
    |> assign(
      list_id: list.id,
      list_name: list.title,
-     items: item_forms
    )
+   |> stream(:items, item_forms)

  {:ok, socket}
end
```

Next, we define the required `phx-update` DOM attribute on the parent container where the item collection is rendered. The items are now accessed via the new assign `@streams`, and we consume the stream using a comprehension:

```
def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      ...
-      <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
+      <div
+        id={"#{@list_id}-items"}
+        class="grid grid-cols-1 gap-2"
+        phx-update="stream"
+      >
-        <div :for={form <- @items} id={"list#{@list_id}-item#{item.id}"}>
+        <div :for={{id, form} <- @streams.items} id={id}>         
        ...
        </div>
      </div>
      ...
    </div>
  </div>
  """
end
```

Now we’re all set to handle the events we defined earlier!

## Fly.io ❤️ Elixir

Fly.io is a great way to run your Phoenix LiveView app close to your users. It’s really easy to get started. You can be running in minutes.

[Deploy a Phoenix app today! →](https://docs.fly.io/elixir)

## Add, delete, update, reset Streams

We’ll be defining five events (new, validate, save, delete, reset). Our main focus is explain how we can use streams to reflect changes in our list. We won’t be diving into functions that interact with the database.

To get started, it will be helpful to define a helper function that creates forms already associated with the `list_id`:

```
defp build_empty_form(list_id) do
  build_item_form(%Item{list_id: list_id}, %{})
end
```

Keep in mind this function, as well as the one we defined earlier. We’ll be using them in our upcoming steps!

### New

To add a new element to the list, we can use the function we just defined to create an empty form. Then, we can insert this form into the stream using the [stream_insert/4](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream_insert/4) function:

```
def handle_event("new", %{"list_id" => list_id}, socket) do
  {:noreply, stream_insert(socket, :items, build_empty_form(list_id), at: -1)}
end
```

To insert the new form at the end of the list, we use the `:at` option provided by the `stream_insert/4` function.

### Validate

To display the errors of our item, we need to modify the already rendered form in the client and insert a new form that includes the errors from the changeset.

One important detail to note is that in order for our component to recognize that it should display the changeset errors, we need to add an `:action` to it. To achieve this, we make a slight modification to the `build_item_form/4` function that we defined earlier:

```
-defp build_item_form(item_or_changeset, params) do
+defp build_item_form(item_or_changeset, params, action \\ nil) do
  changeset =
    item_or_changeset
    |> SortableList.change_item(params)
+   |> Map.put(:action, action)

  to_form(changeset, id: "form-#{changeset.data.list_id}-#{changeset.data.id}")
end
```

The action can be any random atom, but hey, let’s keep things clear and name them sensibly.

Now let’s see how to handle the event and use this new `:action` parameter:

```
def handle_event("validate", %{"item" => item_params} = params, socket) do
  item = %Item{id: params["id"] || nil, list_id: item_params["list_id"]}
  item_form = build_item_form(item, item_params, :validate))
  {:noreply, stream_insert(socket, :items, item_form}
end 
```

First, we generate a new `%Item{}` that includes the item `id` sent from the text input and the `list_id` sent using the `phx-value-id={form.data.id}` parameter. Next, we call our `build_item_form/4` function with the `:validate` action and insert the item into the stream using `stream_insert/4`.

Like magic, we didn’t have to specify that this is an update of a form that already exists! This is because `to_form` created a DOM ID that allows to identify the elements of the stream that already exists in the client.

### Save

When attempting to save a new item, we may receive one of two possible responses. First, when an item is successfully inserted, we clear the new item form and replace it with a fresh empty form. We also insert a new form containing the persisted data. Second, if an error occurs, we display the relevant errors to the user.

Let’s now delve into the implementation details of these actions.

```
def handle_event("save", %{"item" => item_params}, socket) do
  case SortableList.create_item(item_params) do
    {:ok, new_item} ->
      empty_form = build_empty_form(item_params["list_id"])


      {:noreply,
       socket
       |> stream_insert(:items, build_item_form(new_item, %{}))
       |> stream_delete(:items, empty_form)
       |> stream_insert(:items, empty_form)}

    {:error, changeset} ->
      {:noreply, assign(socket, :form, build_item_form(changeset, %{}, :insert)))}
  end
end
```

Great! We can chain together the different functions of the stream using the pipeline operator.

On the other hand, the `save` event may be triggered by one of the forms that have already been saved in the database, indicating an update of the item. We can identify this by pattern matching, receiving a non-null `item_id`:

```
def handle_event("save", %{"id" => item_id, "item" => params}, socket) do
  todo = SortableList.get_item!(item_id)

  case SortableList.update_item(todo, params) do
    {:ok, updated_item} ->
      {:noreply, stream_insert(socket, :items, build_item_form(updated_item, %{}))}

    {:error, changeset} ->
      {:noreply, stream_insert(socket, :items, build_item_form(changeset, %{}, :update))}
  end
end
```

It’s almost like magic, isn’t it? We just need to use `stream_insert`, and the stream takes care of updating itself. And if we want to display the errors from the changeset, we simply add an `:action` to it.

### Delete

Deleting items from a stream is easy too!

For this we have [stream_delete/3](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream_delete/3) which also receives changesets as a parameter:

```
def handle_event("delete", %{"id" => item_id}, socket) do
  item = SortableList.get_item!(item_id)
  {:ok, _} = SortableList.delete_item(item)
  {:noreply, stream_delete(socket, :items, build_item_form(item, %{}))}
end
```

You can also use the `stream_delete/3` function to `discard` the form of the new element when needed:

```
def handle_event("discard", params, socket) do
  {:noreply, stream_delete(socket, :items, build_empty_form(params["list_id"]))}
end
```

### Reset

We have the option to remove all items from a stream, which is perfect for our reset button:

```
def handle_event("reset", params, socket) do
  empty_form = build_empty_form(params["list_id"])
  {:noreply, stream(socket, :items, [empty_form], reset: true)}
end
```

We simply need to reconfigure our stream by passing the option `reset: true`. Additionally, we can include a list of new elements to be inserted, which in this case would be at least one empty form.

[LiveView 0.19](https://github.com/phoenixframework/phoenix_live_view/blob/main/CHANGELOG.md#019) added support for stream resets with bulk insert operations!

Hooray! We’ve done it! Our component is now complete and ready to shine!

## Closing

The features we explored today unlock a world of exciting new possibilities for apps development with LiveView. `LiveView Streams` revolutionize collection handling and memory optimization, simplifying tasks such as adding, editing, and deleting items. Furthermore, the optimizations brought by `to_form/1` enable efficient manipulation of individual inputs without the need for full form re-rendering. This simple yet immensely powerful function opens up new avenues for form usage, expanding the potential of your applications.

Check out [this repo](https://github.com/bemesa21/components_examples) to see these game-changing features in action. We used [our previous learnings](https://fly.io/phoenix-files/liveview-drag-and-drop/) to create an even more impressive component!

## Credits

A big shoutout to Chris McCord for sharing [the incredible example](https://github.com/chrismccord/todo_trek) that inspired these posts and for patiently answering any questions about the exciting new concepts in Phoenix and LiveView.
