lubien
Handling {:array, string} forms with phoenix for create/edit form component
tl;dr: I want to know the most phoenix way of letting users fill a {:array, :string} input via live view form component.
defp deps do
[
{:argon2_elixir, "~> 2.0"},
{:phoenix, "~> 1.6.0"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.6"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 3.0"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_view, "~> 0.16.0"},
]
end

I’m currently experimenting with the current schema and the relevant prop is :synonyms with {:array, :string} type. The user should be able to write any amount of items there.
defmodule MyApp.MyContext.Tag do
use Ecto.Schema
import Ecto.Changeset
schema "tags" do
field :synonyms, {:array, :string}
timestamps()
end
@doc false
def changeset(tag, attrs) do
tag
|> cast(attrs, [:synonyms])
end
end
I was able to make both edit and create work with some assignments called @synonyms and dynamically rendering inputs called name="tag[synonyms][]" using the code below:
defmodule MyAppWeb.TagLive.FormComponent do
use MyAppWeb, :live_component
alias MyApp.MyContext
@impl true
def update(%{tag: tag} = assigns, socket) do
changeset = MyContext.change_tag(tag)
{:ok,
socket
|> assign(assigns)
|> assign(:changeset, changeset)
|> assign(:synonyms, tag.synonyms)} # <---- relevant
end
@impl true
def handle_event("validate", %{"tag" => tag_params}, socket) do # <---- no changes needed
# tag_params => %{ "synonyms: ["foo", "bar"], rest... }, which is ok :)
changeset =
socket.assigns.tag
|> Jobs.change_tag(tag_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :changeset, changeset)}
end
# unrelated ...
end
<.form
let={f}
for={@changeset}
id="tag-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save">
<%= label f, :slug %>
<%= text_input f, :slug %>
<%= error_tag f, :slug %>
<%= label f, :type %>
<%= select f, :type, Ecto.Enum.values(MyApp.MyContext.Tag, :type), prompt: "Choose a value" %>
<%= error_tag f, :type %>
<%= for s <- @synonyms do %> <---------------- here
<%= label f, :slug %>
<input name="tag[synonyms][]" value={s}>
<%= error_tag f, :slug %>
<% end %>
<div>
<%= submit "Save", phx_disable_with: "Saving..." %>
</div>
</.form>
From there I should also make “add more” and “remove” inputs which is fine but I was left wondering if I’ve missed something obvious on the docs or somewhere else that I wasn’t able to find by my searches (as most results seem to deal with embedded schemas instead of an array on the very own schema)
Most Liked Responses
nilskj
Hello! I had the exact same requirement and a lot of troubles but I got it to work now with the new versions of stuff. Idk if idiomatic, I’m new here ![]()
So basically I have a similar Ecto schema with an array of strings as features that I want to edit in a form component:
schema "products" do
...
field :features, {:array, :string}, default: [""]
end
In my form live component I used the same trick as presented earlier here from couple years ago, with the product[features][] syntax in the name property for the input they get mapped correctly to this array.
<%= for {entry, index} <- Enum.with_index(Ecto.Changeset.get_field(@form.source, :features, [])) do %>
<div class="flex items-center gap-2">
<.input value={entry} name="product[features][]" type="text" label="Feature" />
<div phx-click="remove_feature" phx-value-index={index} phx-target={@myself}>
<.icon
name="hero-x-mark"
class="w-8 h-8 relative top-4 bg-red-500 hover:bg-red-700 hover:cursor-pointer"
/>
</div>
</div>
<% end %>
It was a bit tricky getting the index so I wrapped it into an Enum.with_index to make the deletion mutation easier to manage.
Here are my working but a bit ugly? click handlers as well:
def handle_event("add_feature", _, socket) do
existing_features = Ecto.Changeset.get_field(socket.assigns.form.source, :features, [])
append_feature =
Catalog.change_product(socket.assigns.product, %{
"features" => existing_features ++ [""]
})
{:noreply, assign(socket, form: to_form(append_feature))}
end
def handle_event("remove_feature", %{"index" => index}, socket) do
updated_features =
Ecto.Changeset.get_field(socket.assigns.form.source, :features, [])
|> List.delete_at(String.to_integer(index))
remove_feature =
Catalog.change_product(socket.assigns.product, %{
"features" => updated_features
})
{:noreply, assign(socket, form: to_form(remove_feature))}
end
Is this maybe better to do in an embedded schema? Then I guess I could use inputs_for or something. Maybe that is the way!
kuzyn
Very insightful thread for me ![]()
Is the name="map_name[list_name][]" behavior documented anywhere? It looks very strange to me since you could assume it’s a simple HTML name property without any template magic…
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #javascript
- #podcasts
- #code-sync
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








