Phoenix live_live nested for add/remove inputs

Hey,

I have a nested form

<%= f =  form_for @config_changeset, "#", [phx_submit: :save_config, as: :config] %>
  <%= if @config_changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <%= Input.text f, :heading, label: "Content Heading" %>
  <hr/>
  <%= inputs_for f, :sections, fn fp -> %>
    <%= Input.text fp, :heading, label: "Section Heading" %>
    <%= Input.textarea fp, :body, label: "Section Body"%>
    <button class="btn btn-link" phx-click="project_information_remove_section"  ><i class="mdi mdi-trash-can-outline"></i> Remove Section</button>
    <hr/>
  <% end %>
  <div>
    <button class="btn btn-outline-secondary" phx-click="project_information_add_section" >
      <i class="mdi mdi-plus"></i> Add Section
    </button>
    <%= submit "Save", class: "btn btn-primary float-right" %>
  </div>
</form>

I have and add sections and a remove section button.
Add section should add a new empty section and remove should remove the selected one.
Here is the original mount:

  def mount(%{"project_id" => project_id, "project_widget_id" => project_widget_id}, socket) do
    project = Projects.get_project!(project_id)

    project_widget = Widgets.get_project_widget!(project_widget_id, preload: [:widget])

    title_changeset = Widgets.change_project_widget(project_widget)

    config_changeset = project_widget.config.__struct__.changeset(project_widget.config, %{})

    widget_descriptor = Widgets.widget_descriptor_from_project_widget(project_widget)

    {:ok,
     socket
     |> assign(
       project: project,
       project_widget: project_widget,
       title_changeset: title_changeset,
       config_changeset: config_changeset,
       form_name: project_widget.widget.config_type,
       widget_view_module: widget_descriptor.implementation.view_module
     )}
  end

I think I am missing something in the logic, here is my remove handler:
I know this would remove all now, that is not the problem

  def handle_event("project_information_remove_section", params, socket) do
    IO.puts("remove")
    project_widget = socket.assigns.project_widget

    config_changeset =
      project_widget.config.__struct__.changeset(project_widget.config, %{
        sections: []
      })

    {:noreply,
     assign(socket,
       config_changeset: config_changeset
     )}
  end

What I am trying to do now, is that I create a new changeset just without sections, so I would expect that once I pass that the view would update to having no sections at all, but nothing happens.
What I am doing wrong?
I tried to update the changeset from the socket as well directly, but no luck, although i think that is the way to go to track multiple changes

This is probably more like how it should be, still no effect though:

  def handle_event("project_information_remove_section", params, socket) do
    config_changeset = socket.assigns.config_changeset

    config_changeset =
      Ecto.Changeset.change(config_changeset, %{sections: []})
      |> Map.put(:action, :update)

    {:noreply,
     assign(socket,
       config_changeset: config_changeset
     )}
  end

Hi,

I ended up creating the parent first and then using a live_component to add and remove the child/children.

If the docs are not clear let me know and I will put in some code.

Andrew

1 Like

Thanks for the response! Oh please do make an example, I think it would be appreciated by many in the future as well as by me!

Hi,

Hope this helps. I will rename Config to Parent and Section to Child so that it is representative of the assoc.

I have a ParentLive.ex that handles creating the Parent as you expect:

def handle_event("add", %{"parent" => parent}, socket) do
    case Parents.create_parent(parent) do
      {:ok, parent} ->
        {:noreply, fetch(assign(socket, :parent, parent))}

      {:error, %Ecto.Changeset{} = _changeset} ->
        socket =
          socket
          |> put_flash(:danger, "Oh No! Something is going wrong")
        {:noreply, fetch(socket)}
    end
  end

I then update the page with the newly created Parent details and render a child form and list as a live_component.

<div>
    <%= Phoenix.LiveView.live_component(@socket, Child.ChildComponent, parent: @parent) %>
</div>

In the live_component file I handle the add and remove events (I have left out the html as it is just a form and a list of child records).

defmodule Child.ChildComponent do
  use Phoenix.LiveComponent
  use Phoenix.HTML

  def render(assigns) do
    ~L"""
      [CUSTOM HTML]
    """
  end

  def mount(_session, socket) do
    {:ok, socket}
  end

  def handle_event("add", %{"child" => child}, socket) do
    case Child.create_child(child}) do
      {:ok, child} ->
        send(self(), {:updated_child_list})
        {:noreply, socket}

      {:error, _error} ->
        send(self(), {:duplicate_child})
        {:noreply, socket}

      _ ->
        send(self(), {:unknown_error})
         {:noreply, socket}
    end
  end

  def handle_event("delete", %{"child-id" => child_id}, socket) do
    child = Child.get_child!(child_id)

    case Child.delete_child(child) do
      {:ok, child} ->
        send(self(), {:updated_child_list})
        {:noreply, socket}

      {:error, changeset} ->
        {:noreply, assign(socket, changeset: changeset)}

    end
  end
end

Back in the ParentLive.ex

def handle_info({:updated_diagnoses}, socket) do
    {:noreply, fetch(assign(socket, parent_id: socket.assigns.parent.id))}
end

def handle_info({:duplicate_child}, socket) do
    socket = socket
    |> put_flash(:danger, "Oh No! This is a duplicate Child")
    {:noreply, socket}
end

In the fetch() function I am updating the list of children for the identified parent. I have extracted this from a more complex set of transactions so hopefully I have not left anything out.

Let me know if you have any questions.

Andrew

1 Like

Hey Andrew,

Thank you so much for getting back to me!
I ended up figuring it out in a different way, but ultimately we decided to use a different design.

Have a good one!