How to wire up a Phoenix form for a field {:array, :string}?

My schema is very simple:

  schema "partnerships" do
    field :email, :string
    field :name, :string
    field :social_media_urls, {:array, :string}

    timestamps()
  end

image

I want to be able to click on Add Another to add another input to the UI.

Here’s what I’ve tried so far:

def mount(_params, _session, socket) do
  roles = Partnerships.list_partnership_roles()
  changeset = make_changeset()

  socket =
    socket
    |> assign(%{
      page_title: "Partnerships",
      roles: Enum.map(roles, &{&1.name, &1.id}),
      changeset: changeset,
      social_media_urls: changeset.data.social_media_urls
    })

  {:ok, socket}
end

@impl true
def handle_event(
      "validate",
      %{"partnership" => attrs},
      socket
    ) do
  changeset =
    %Partnership{}
    |> Partnerships.change_partnership(attrs)
    |> Map.put(:action, :validate)

  {:noreply,
    assign(socket,
      changeset: changeset,
      social_media_urls: changeset.data.social_media_urls || [""]
    )}
end

def handle_event("add-new-social-media-urls", attrs, socket) do
  social_media_urls = ["" | socket.assigns.social_media_urls]

  changeset =
    socket.assigns.changeset
    |> Ecto.Changeset.change(social_media_urls: social_media_urls)

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

defp make_changeset(attrs \\ %{}) do
    %Partnership{
      social_media_urls: [""]
    }
    |> Partnerships.change_partnership(attrs)
  end

The problem is that as soon as validate runs it wipes out my social media url fields.

How would you wire up this form?

So here’s how you do it properly.

Create your changeset for the form as usual.

  def mount(_params, _session, socket) do
    changeset = make_changeset()

    socket =
      socket
      |> assign(%{
        changeset: changeset
      })

    {:ok, socket}
  end

Then in your form, you need to use get_field to retrieve that array of values to tie it in properly to the schema.

<div class="form-group">
  <.form_label
    form={f}
    field={:social_media_urls}
    label="Social Media Profiles"
    class="mb-0 text-sm text-white"
  />
  <%= for social_media_url <- Ecto.Changeset.get_field(@changeset, :social_media_urls, [""]) do %>
    <%= text_input(f, :favorite_games,
      name: "partnership[social_media_urls][]",
      value: social_media_url,
      class: "form-input mb-2",
      placeholder: "e.g. twitter.com/grilla"
    ) %>
  <% end %>
  <.form_feedback form={f} field={:social_media_urls} />
</div>

Take special note of the name value. That’s what allows phoenix to do it’s binding magic to the right schema fields

You end up with a proper filled in Elixir map ready to persist to your database.

One more big detail is in your schema cast, you should use empty_values: [].

  def changeset(partnership, attrs) do
    partnership
    |> cast(attrs, [:name, :email, :about, :partnership_role_id])
    |> cast(
      attrs,
      [:social_media_urls],
      empty_values: []
    )
    |> validate_required([:name, :email, :about, :partnership_role_id, :social_media_urls])
    |> Emails.Validation.validate_email_format()
  end

You could try using existing changeset from assigns instead of building a new one in validate handler.