How to create a form where some of its fields are added dynamically, without losing progress?

Hi all,

how can I create a form where some of its fields are added dynamically, without losing the data when a user adds fields to the form?

I’m trying to create a cv builder. A cv can have many entries (i.e. the name of the company as well as the duration).

Here are the schemas that I have (not backed by a DB yet):

defmodule Cvtex.Cv do
  use Ecto.Schema
  import Ecto.Changeset

  defmodule Entry do
    import Ecto.Changeset
    use Ecto.Schema

    embedded_schema do
      field :company_name, :string
      field :work_period, :string
    end

    def new, do: %Entry{}
  end

  embedded_schema do
    field :first_name, :string
    field :last_name, :string
    embeds_many :entries, Entry
  end

  def changeset(params) do
    cast(%__MODULE__{entries: [Entry.new()]}, params, [:first_name, :last_name])
  end
end

The form looks like this in the template:


<.form let={f} for={@changeset}  phx-submit="submit-cv">
  <%= label f, :first_name %>
  <%= text_input f, :first_name %>
  
  <%= label f, :last_name %>
  <%= text_input f, :last_name %>
  
  <%= inputs_for f, :entries, fn entry -> %>
      <%= label entry, :company_name %>
      <%= text_input entry, :company_name %>

      <%= label entry, :work_period %>
      <%= text_input entry, :work_period %>
  <% end %>

  <%= button "Add a work entry", to: "#", "phx-click": "add-entry" %>
  <%= submit "Submit" %>

</.form>

And here’s the LiveView:

defmodule CvtexWeb.CvLive do
  use CvtexWeb, :live_view

  alias Cvtex.Cv

  @impl true
  def mount(_params, _session, socket) do
    changeset = Cv.changeset(%{})

    socket =
      socket
      |> assign(changeset: changeset)

    {:ok, socket}
  end

  @impl true
  def handle_event("submit-cv", %{"cv" => params}, socket) do
    IO.inspect(params)

    {:noreply, socket}
  end

  @impl true
  def handle_event("add-entry", params, socket) do
    current_entries =
      Map.get(socket.assigns.changeset.changes, :entries, socket.assigns.changeset.data.entries)

    changeset =
      Ecto.Changeset.put_embed(
        socket.assigns.changeset,
        :entries,
        [Cv.Entry.new() | current_entries]
      )

    socket =
      socket
      |> assign(changeset: changeset)

    {:noreply, socket}
  end
end

The problem is, everytime I click on “Add a work entry”, the current data that has been entered in the form simply dissapears. I would like to be able to add new entries/fields to the form without losing the current progress.

You can find an example of that here: Phoenix LiveView form with nested embeds and add/delete buttons · GitHub

2 Likes

Thank you @LostKobrakai.
After playing around with your code, I have noticed that I could reproduce the same error that I have currently when I delete the phx-change="validate" attribute from the HTML.

After adding the phx-change, my code worked as expected; though I’m not sure why I must validate the data while the user is typing in order to not to lose it.

Phoenix rerenders your form on updates. The only changes it doesn’t overwrite are for the input currently in focus. Everything else is replaced if unknown to the server.

1 Like

Thank you :pray:

Note that if you don’t need a change event for every keystroke you can put phx-debounce=blur on the input. This significantly reduces the amount of events over-the-wire because it only sends the change when the user input loses focus.

2 Likes

I just found this via Hacker News, which may be of interest:

https://weakty.com/blog/2022-05-03-dynamic-live-view-forms.html

2 Likes