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)
First Post!
sukidhar
Most Liked
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
- #code-sync
- #javascript
- #podcasts
- #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
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex









