lib/live_admin/components/resource/form/map_input.ex

defmodule LiveAdmin.Components.Container.Form.MapInput do
  use Phoenix.LiveComponent
  import Phoenix.HTML.Form
  use PhoenixHTMLHelpers

  import LiveAdmin, only: [trans: 1]

  alias Phoenix.LiveView.JS

  @impl true
  def mount(socket) do
    {:ok, assign(socket, :values, %{})}
  end

  @impl true
  def update(assigns = %{form: form, field: field}, socket) do
    values =
      Map.get(form.params, to_string(field)) ||
        build_values_from_input_value(input_value(form, field)) ||
        %{}

    socket =
      socket
      |> assign(assigns)
      |> assign(:values, values)
      |> assign(:disabled, Enum.any?(values, fn {_, %{"value" => v}} -> is_map(v) end))

    {:ok, socket}
  end

  @impl true
  def update(assigns, socket) do
    {:ok, assign(socket, assigns)}
  end

  @impl true
  def render(assigns = %{disabled: true}) do
    ~H"""
    <div>
      <span class="resource__action--disabled">
        <pre><%= @form |> input_value(@field) |> inspect() %></pre>
      </span>
    </div>
    """
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div class="field__map--group" phx-hook="MapInput" id={input_id(@form, @field) <> "_map_input"}>
      <div>
        <%= for {idx, %{"key" => k, "value" => v}} <- Enum.sort(@values) do %>
          <div class="field__map--row">
            <a
              href="#"
              class="button__remove"
              phx-click={
                JS.push("remove",
                  value: %{idx: idx},
                  target: @myself,
                  page_loading: true
                )
              }
            />
            <textarea
              rows="1"
              name={input_name(@form, @field) <> "[#{idx}][key]"}
              phx-debounce={200}
              placeholder={trans("Key")}
            ><%= k %></textarea>
            <textarea
              rows="1"
              name={input_name(@form, @field) <> "[#{idx}][value]"}
              phx-debounce={200}
              placeholder={trans("Value")}
            ><%= v %></textarea>
          </div>
        <% end %>
        <a
          href="#"
          phx-click={
            JS.push("add",
              target: @myself,
              page_loading: true
            )
          }
          class="button__add"
        />
      </div>
    </div>
    """
  end

  @impl true
  def handle_event("add", _, socket) do
    socket =
      socket
      |> update(
        :values,
        &Map.put(&1, &1 |> map_size() |> to_string(), %{"key" => nil, "value" => nil})
      )
      |> push_event("change", %{})

    {:noreply, socket}
  end

  @impl true
  def handle_event("remove", %{"idx" => idx}, socket) do
    socket =
      socket
      |> update(:values, &remove_at(&1, idx))
      |> push_event("change", %{})

    {:noreply, socket}
  end

  defp remove_at(values, idx) do
    values
    |> Map.delete(idx)
    |> Enum.with_index()
    |> Map.new(fn {{_, value}, idx} ->
      {to_string(idx), value}
    end)
  end

  defp build_values_from_input_value(nil), do: nil

  defp build_values_from_input_value(value) do
    value
    |> Enum.with_index()
    |> Map.new(fn {{k, v}, idx} ->
      {to_string(idx), %{"key" => k, "value" => v}}
    end)
  end
end