phoenixframework / phoenix_live_view

Rich, real-time user experiences with server-rendered HTML
https://hex.pm/packages/phoenix_live_view
MIT License
6.29k stars 939 forks source link

Adding nested fields in forms without wrapping them into div produces weird html #159

Closed itSQualL closed 5 years ago

itSQualL commented 5 years ago

Environment

Actual behavior

I'm building a nested form with buttons for add or remove nested field.

form.html.leex:

= f = form_for @changeset, "#", [phx_change: :update, phx_submit: :save] %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>

    <%= error_tag f, :codes%>
    <%= error_tag f, :translations%>
  <% end %>

  <%= inputs_for f, :codes, fn c_f -> %>
    <div class="form-group">
      <%= label c_f, :code_name, class: "control-label" %>
      <%= text_input c_f, :code_name, class: "form-control", placeholder: "iso_xxx" %>
      <%= error_tag c_f, :code_name%>
    </div>

    <div class="form-group">
      <%= label c_f, :code, class: "control-label" %>
      <%= text_input c_f, :code, class: "form-control", placeholder: "32" %>
      <%= error_tag c_f, :code %>
    </div>

    <button ="remove_code" data-index=<%= c_f.index %>>Remove code</button>
  <% end %>

  <button type="button" phx-click="add_code">Add code</button>

  <%= inputs_for f, :translations, fn t_f -> %>
    <div class="form-goup">
      <%= label t_f, :language, "Idioma", class: "control-label" %>
      <%= select t_f, :language, language_options(), prompt: "Elige una lengua" %>
      <%= error_tag t_f, :language%>
    </div>

    <div class="form-group">
      <%= label t_f, :name, "traducción", class: "control-label" %>
      <%= text_input t_f, :name, class: "form-control", placeholder: "España, Spain, ..." %>
      <%= error_tag t_f, :name%>

    </div>

    <button phx_click="remove_translation" data-index=<%= t_f.index %>>Remove translation</button>
  <% end %>

  <button phx_click="add_translation">Add translation</button>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
</form>

live/new.ex:

defmodule BizlocWeb.Locations.CountryLive.New do
  use Phoenix.LiveView

  def mount(_session, socket) do
    countries_definition = Locations.get_countries_definition()

    params = %{
      "codes" => %{"0" => %{"code_name" => countries_definition.main_code}},
      "translations" => %{"0" => %{"language" =>  countries_definition.main_translation}}
    }

    changeset = Locations.change_region(%Region{}, params)

    {:ok, assign(socket, changeset: changeset, params: params)}
  end

  def render(assigns), do: CountryView.render("new.html", assigns)

  def handle_event("add_code", _, socket) do
    params = Map.update!(socket.assigns.params, "codes", fn codes ->
      cond do
        is_map(codes) -> Map.values(codes) ++ [%{}]
        is_list(codes) -> codes ++ [%{}]
      end
    end)

    changeset = Locations.change_region(%Region{}, params)

    {:noreply, assign(socket, %{changeset: changeset, params: params})}
  end

  def handle_event("update", %{"region" => params}, socket) do
    changeset = Locations.change_region(%Region{}, params)
    {:noreply, assign(socket, %{changeset: changeset, params: params})}
  end

end

Navigating to the view shows: imagen

but when I submit the add_code event the result html is: imagen

As you can see, "traducción" label and "add code" button dissapear

Expected behavior

I've found that if you wrap the nested fields into a div, it works perfect:

From:

  <%= inputs_for f, :codes, fn c_f -> %>
    <div class="form-group">
      <%= label c_f, :code_name, class: "control-label" %>
      <%= text_input c_f, :code_name, class: "form-control", placeholder: "iso_xxx" %>
      <%= error_tag c_f, :code_name%>
    </div>

    <div class="form-group">
      <%= label c_f, :code, class: "control-label" %>
      <%= text_input c_f, :code, class: "form-control", placeholder: "32" %>
      <%= error_tag c_f, :code %>
    </div>

    <button ="remove_code" data-index=<%= c_f.index %>>Remove code</button>
  <% end %>

To:

  <%= inputs_for f, :codes, fn c_f -> %>
    <div>
      <div class="form-group">
        <%= label c_f, :code_name, class: "control-label" %>
        <%= text_input c_f, :code_name, class: "form-control", placeholder: "iso_xxx" %>
        <%= error_tag c_f, :code_name%>
      </div>

      <div class="form-group">
        <%= label c_f, :code, class: "control-label" %>
        <%= text_input c_f, :code, class: "form-control", placeholder: "32" %>
        <%= error_tag c_f, :code %>
      </div>

      <button ="remove_code" data-index=<%= c_f.index %>>Remove code</button>
    </div>
  <% end %>

And then, it works perfect:

imagen

chrismccord commented 5 years ago

@snewcomer sounds like a possibly patching bug when we call morphdom, but I can't say why we'd diff any differently for the div container vs no container. Can you take a look? <3

snewcomer commented 5 years ago

@itSQualL I'd be curious what happens when you add data-index=<%= c_f.index %>> to each div with a form-group class (without the

container). Is that possible to try?

itSQualL commented 5 years ago

Ok I found the problem here. It was a mistake with that line:

<button ="remove_code" data-index=<%= c_f.index %>>Remove code</button>

I was testing the code and I forgot to append phx-click before ="remove_code", removing that or adding the event, it works.

I'm sorry for the noise here.

jrissler commented 5 years ago

@itSQualL Do you have the full solution documented anywhere? Interested in the same feature for a project.

itSQualL commented 5 years ago

@jrissler Hi, I don't have a repository but I'm doing the next:

First, on mount or validate events I'm saving the initial/updated params

def mount(params, socket) do
  ...
  {:ok, assign(socket, %{changeset: changeset, params: params})}
end

def handle_event("validate", %{"params" => params}, socket) do
  ...
  {:noreply, assign(socket, %{changeset: changeset, params: params})}
end

So I ever have the state of params.

Then, I have an add and remove actions triggered when button is pushed:

  def handle_event("add_code", _, socket) do
    {:noreply, LiveHelpers.add_nested_field(socket, "codes")}
  end

  def handle_event("remove_code", index, socket) do
    {:noreply, LiveHelpers.remove_nested_field(socket, "codes", index)}
  end
defmodule LiveHelpers do
  import Phoenix.LiveView
  alias Phoenix.LiveView.Socket

  def add_nested_field(socket, field) do
    params = Map.update!(socket.assigns.params, field, fn
      field when is_map(field) -> Map.values(field) ++ [%{}]
      field when is_list(field) -> field ++ [%{}]
    end)

    do_changes(socket, params)
  end

  def remove_nested_field(socket, field, index) do
    params = Map.update!(socket.assigns.params, field, fn
      field when is_map(field) -> Map.drop(field, [index])
      field when is_list(field) -> List.delete_at(field, String.to_integer(index))
    end)

    do_changes(socket, params)
  end

  defp do_changes(socket, params) do
    changeset =
      socket
      |> get_resource
      |> Locations.change_region(params) # It returns a changeset

    assign(socket, %{changeset: changeset, params: params})
  end

I hope it will help you. I've tried to find examples about this too but I find nothing.

jrissler commented 5 years ago

@itSQualL awesome thanks, that does help.

dsignr commented 5 years ago

@itSQualL I understand the logic of your code, but what I don't understand is how are you passing the index of the element to be deleted using liveview? I am doing the same thing as you, but even with data-index attribute set on the delete button, I don't receive anything on my handle_event("remove_code", index, socket) function as index is always "". Could you please share more detail, or am I missing something basic?

dsignr commented 5 years ago

Nevermind, I figured it out. You can pass parameters using phx-value attribute. This is documented here.

So, the correct version of your button should be:

<button phx-click="remove_code" phx-value=<%= c_f.index %>>Remove code</button>
andkar73 commented 5 years ago

@itSQualL Thanks for a good example! I’m trying to implement this example but have problems to understand what the function ”get_resource” does. I think it gets values from the form to ensure that date isn’t overwritten? But I can’t figur out how to do this from the handle_event.

itSQualL commented 5 years ago

@itSQualL Thanks for a good example! I’m trying to implement this example but have problems to understand what the function ”get_resource” does. I think it gets values from the form to ensure that date isn’t overwritten? But I can’t figur out how to do this from the handle_event.

Hi!, well, it was a private function that I omitted. It just return some info saved into the socket to use on the next function.

itSQualL commented 4 years ago

Since it is related, I will publish here a package to manage nested fields in forms:

https://github.com/MamesPalmero/dynamic_inputs_for

thojanssens commented 4 years ago

Hello, maybe this was too long ago for you, but I wonder why store the params separately from the changeset into the socket.

{:ok, assign(socket, changeset: changeset, params: params)}

Why not just store the changeset and use changeset.params?