Is there an updated nested forms example for 1.7 and heex?
axdc:
I’m using this guide: https://ash-hq.org/docs/module/ash_phoenix/latest/ashphoenix-form It’s using a leex template and referencing functions that don’t exist for me. I’ve got a few related resources that should all be created together and I can do it with managed relationships in actions from iex, just trying to wrap it up with a nice setup liveview now.
zachdaniel:
Not that I’m aware of, unfortunately
zachdaniel:
but like
<.form ....
and friends should be relatively drop-in
axdc:
am I doing something wrong causing
inputs_for
to be unrecognized?
zachdaniel:
When you say “unrecognized” what do you mean? that you aren’t seeing a form where you’d expect to see one?
zachdaniel:
Is this for a to-one relationship when creating a record?
zachdaniel:
A common misunderstanding is that there isn’t a form automatically created for each relationship
zachdaniel:
its based on the existing related values, which on a create, is nothing
zachdaniel:
so you might want, in your setup,
AshPhoenix.Form.for_create(...) |> AshPhoenix.Form.add_form(:to_one, ...)
axdc:
okay, so perhaps my approach is fundamentally misinformed? this is what I’m working with right now, defining a create action to make the site and its accompanying related resources:
...
create :create_new_site do
argument :add_configuration, :map do
allow_nil? false
end
argument :add_domains, :map do
allow_nil? false
end
change manage_relationship(:add_configuration, :configuration, type: :create)
change manage_relationship(:add_domains, :domains, type: :create)
end
...
my index.ex:
...
@impl true
def mount(_params, _session, socket) do
form =
AshPhoenix.Form.for_create(Site, :create_new_site, api: Panacea.Sites, forms: [auto?: true])
|> to_form
IO.inspect(form)
{:ok, socket |> assign(form: form)}
end
...
index.html.heex:
...
<.simple_form for={@form} phx-change="validate">
<%= for domains_form <- inputs_for(@form, :add_domains) do %>
asdasd
<% end %>
</.simple_form>
...
This is currently telling me inputs_for is undefined. I was reading through the core_components file and phoenix form documentation to try and see what I was missing on that front. It’s a has_many relationship.
axdc:
I do have the
change manage_relationship(...)
mentioned in
https://ash-hq.org/docs/module/ash_phoenix/latest/ashphoenix-form-auto
zachdaniel:
ah, that looks correct 🙂
zachdaniel:
I think you just are missing an import somewhere
zachdaniel:
like
import Phoenix.HTML.Form
or something
zachdaniel:
or maybe the new form thing exposes it as a slot?
axdc:
okay oh goodness. okay. um in the MyAppWeb file where the :live_view use is defined, right? This is a fresh 1.7 phoenix generated project
axdc:
I seem to have jumped on mid-transition in the ecosystem 😅
zachdaniel:
a bit 😆 we’re not transitioning at all, but phoenix has made some changes
dblack1:
Not 100% sure, but don’t you need
:let={form}
in simple_form and use that in inputs_for?
dblack1:
...
<.simple_form :let={form} for={@form} phx-change="validate">
<%= for domains_form <- inputs_for(form, :add_domains) do %>
asdasd
<% end %>
</.simple_form>
...
dblack1:
there’s also the inputs_for component which looks after adding the hidden fields for you
zachdaniel:
ohhhhh thats right
zachdaniel:
is that defined in core components?
zachdaniel:
if so thats what you want 😄
axdc:
<.simple_form let={f} for={@form} phx-change="validate">
<%= for domains_form <- Phoenix.HTML.Form.inputs_for(f, :add_domains) do %>
<.input type="text" field={domains_form[:name]} />
<% end %>
</.simple_form>
this compiles, but I don’t see any inputs inside the form html, whether I use the
:let={f}
syntax or just reference
@form
directly. Do they not exist yet or something because it’s a create type action?
zachdaniel:
yeah, its that
zachdaniel:
we don’t assume there are forms there
zachdaniel:
if you’d like to add one by default, use
add_form
when first creating the form
axdc:
Of course, that makes sense. I’m not sure what the
path
should be, as in the docs the add_form button is getting the path from what would be
domains_form.name
, but IO.inspecting that gives me nothing. The path is like an address in the hierarchy of forms, right?
zachdaniel:
Yep!
zachdaniel:
it acepts it in the html form format, or atoms/integers
zachdaniel:
i.e
foo[bar][baz][0][bart]
or
[:foo, :bar, :baz, 0, :bart]
zachdaniel:
The integer is not necessary to add a form to a list
zachdaniel:
so just
[:foo]
for you I imagine
dblack1:
inputs_for
is a built in component,
https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#inputs_for/1
axdc:
got it, that’s the new componentized version. nice.
dblack1:
yep, you can easily forget about adding the hidden fields the other way
axdc:
it doesn’t seem happy with being
|> to_form
ed
@impl true
def mount(_params, _session, socket) do
form =
AshPhoenix.Form.for_create(Site, :create_new_site, api: Panacea.Sites, forms: [auto?: true])
|> AshPhoenix.Form.add_form(socket.assigns.form, [:add_domains])
|> to_form
{:ok, socket |> assign(form: form)}
end
key :form not found in: %{__changed__: %{}, flash: %{}, live_action: :index}
zachdaniel:
yeah, probably the number one most common mistake when working with nested forms
zachdaniel:
|> AshPhoenix.Form.add_form([:add_domains])
axdc:
<.simple_form :let={f} for={@form} phx-change="validate">
<.inputs_for :let={f_nested} field={f[:add_domains]}>
<.input type="text" label="Domain" field={f_nested[:name]} />
</.inputs_for>
<.inputs_for :let={f_nested} field={f[:add_configuration]}>
<.input type="text" label="Theme" field={f_nested[:theme]} />
</.inputs_for>
</.simple_form>
Everything looks wired up correctly, it’s showing up with the default theme from the Configuration resource, thank you 🙇
zachdaniel:
You can then hook up buttons for adding/removing forms and that kind of thing. Even though there is a fair amount of complexity there, I think overall it adds a lot of utility vs bare phoenix forms
dblack1:
Definitely much simpler than bare phoenix forms. I’d love to do a AshPhoenix version of this fantastic post: https://kobrakai.de/kolumne/one-to-many-liveview-form
dblack1:
Especially when you start getting into multiple layers of nesting
axdc:
That’s what I’m doing now, although the examples from the docs are giving me some trouble. It’ll remove the existing one fine after some changes but not add another? I feel like I might be misunderstanding some of the terminology. Do I want to add more inputs instead of another form? Does this system take the nature of the relationship into account?
axdc:
and yes, all the ash stuff is far more powerful than bare phoenix, and wonderful to use, once I wrap my head around it 😄
dblack1:
The docs could do with a bit of an update, but I think the add and remove buttons and handlers should be good. You’ve confirmed that add_form works when you call it when mounting the liveview but you can’t add a form after deleting one? Or is it just not working when hitting the button regardless of removing a form?
zachdaniel:
So “inputs” are just forms
zachdaniel:
AshPhoenix.Form
is recursive
zachdaniel:
So
inputs_for
is just returning nested forms
zachdaniel:
And therefore
add_form
means “add a nested form”, which would add something to the list you are iterating with
inputs_for
for example
axdc:
yes, they are added and load on mount but afterward no more appear. Removing removes the one that was added on mount
axdc:
<.inputs_for :let={f_nested} field={f[:add_domains]}>
<.input type="text" label="Domain" field={f_nested[:name]} />
<.button type="button" phx-click="remove_form" phx-value-path={f_nested.name}>
Remove domain
</.button>
<.button type="button" phx-click="add_form" phx-value-path={f_nested.name}>
Add Domain
</.button>
</.inputs_for>
zachdaniel:
ah
zachdaniel:
You don’t want the add/remove buttons inside the
inputs_for
axdc:
is it because they have the same path?
axdc:
ahh
zachdaniel:
And so you want something like
phx-value-path={f.name <> "[domains]"}
zachdaniel:
Sorry, but you
do
want the
remove
button to be inside of there
zachdaniel:
and to have
phx-value-path={f_nested.name}
as you have it
axdc:
because remove is referencing that specific existing one, but add should be creating a new one?
zachdaniel:
Yeah, exactly when you
add_form
you’d do something like
add_form([:domains])
But when you remove a form you’d do something like
remove_form([:domains,
where
n
is the index of the form you’re removing (handled by just passing the nested form name to
remove_form
)
axdc:
the form created in mount looks like its path is just form[add_domains], and so is the remove that’s taking the path from f_nested.name
axdc:
do I need to… add an index there, somehow? in the original form added in the mount callback?
zachdaniel:
🤔
zachdaniel:
You shouldn’t need to do that
zachdaniel:
the nested form should have a name like
form[add_domains][0]
axdc:
it does not, and if I specify
|> AshPhoenix.Form.add_form([:add_domains][0])
in mount I get an error
axdc:
hrm
zachdaniel:
Yeah,
add_form
shouldn’t end in an index (because we don’t expect you to know the index, you’re just adding a form to the end of the list)
zachdaniel:
how are you determining that the nested form doesn’t have the correct path?
axdc:
index.ex
defmodule PanaceaWeb.SetupLive.Index do
use PanaceaWeb, :live_view
alias Panacea.Sites.Site
@impl true
def mount(_params, _session, socket) do
form =
AshPhoenix.Form.for_create(Site, :create_new_site, api: Panacea.Sites, forms: [auto?: true])
|> AshPhoenix.Form.add_form([:add_domains])
|> AshPhoenix.Form.add_form([:add_profiles])
|> AshPhoenix.Form.add_form([:add_configuration])
|> to_form
{:ok, socket |> assign(form: form)}
end
@impl true
def handle_event("validate", %{"form" => form}, socket) do
form = socket.assigns.form |> AshPhoenix.Form.validate(form)
socket = socket |> assign(form: form)
{:noreply, socket}
end
def handle_event("add_form", %{"path" => path}, socket) do
IO.inspect(socket.assigns.form)
form = AshPhoenix.Form.add_form(socket.assigns.form, path) |> to_form
IO.inspect(form)
{:noreply, assign(socket, form: form)}
end
def handle_event("remove_form", %{"path" => path}, socket) do
form = AshPhoenix.Form.remove_form(socket.assigns.form, path) |> to_form
{:noreply, assign(socket, form: form)}
end
end
index.html.heex
<.simple_form :let={f} for={@form} phx-change="validate">
<.h2>Domains</.h2>
<.inputs_for :let={f_nested} field={f[:add_domains]}>
<.input type="text" label="Domain" field={f_nested[:name]} />
<.button type="button" phx-click="remove_form" phx-value-path={f_nested.name}>
Remove domain
</.button>
</.inputs_for>
<.button type="button" phx-click="add_form" phx-value-path="form[add_domains]">
Add Domain
</.button>
</.simple_form>
axdc:
I’m just looking at the html attributes in the browser inspector. Is there something else I should check?
zachdaniel:
🤔
zachdaniel:
That looks right to me. Although I would suggest deriving the
phx-value-path
in the
add_form
button
zachdaniel:
phx-value-path={"#{form.name}[add_domains]"}
zachdaniel:
Anyway
zachdaniel:
Remind me, the problem is that
add_form
isn’t doing what you think it should do? Its not adding a form when you click that button?
zachdaniel:
What happens instead?
axdc:
[error] GenServer #PID<0.4897.0> terminating
** (MatchError) no match of right hand side value: nil
(ash_phoenix 1.2.12) lib/ash_phoenix/form/form.ex:3488: AshPhoenix.Form.decoded_to_list/1
(ash_phoenix 1.2.12) lib/ash_phoenix/form/form.ex:3404: AshPhoenix.Form.parse_path!/2
(ash_phoenix 1.2.12) lib/ash_phoenix/form/form.ex:2545: AshPhoenix.Form.add_form/3
(ash_phoenix 1.2.12) lib/ash_phoenix/form/form.ex:2536: AshPhoenix.Form.add_form/3
(panacea 0.1.0) lib/panacea_web/live/setup_live/index.ex:29: PanaceaWeb.SetupLive.Index.handle_event/3
(phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:401: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
(phoenix_live_view 0.18.18) lib/phoenix_live_view/channel.ex:221: Phoenix.LiveView.Channel.handle_info/2
(stdlib 4.3) gen_server.erl:1123: :gen_server.try_dispatch/4
(stdlib 4.3) gen_server.erl:1200: :gen_server.handle_msg/6
(stdlib 4.3) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
axdc:
I’ve adopted this, good point, thanks 🙂
zachdaniel:
🤔
zachdaniel:
in your
add_form
handler
zachdaniel:
can you inspect
path
?
zachdaniel:
Also the
|> to_form()
calls should not be necessary
zachdaniel:
if we are given a
Phoenix.HTML.Form
we return a
Phoenix.HTML.Form
zachdaniel:
so you only need the first one in
mount
zachdaniel:
(or wherever you create a new form
axdc:
got it, removing those, Ash is smart and returns what it’s given after the first time
axdc:
“[add_domains]”
zachdaniel:
well, thats the problem 😆
zachdaniel:
that seems pretty strange
axdc:
<:thinkies:915154230078222336>
zachdaniel:
and if you remove the
"#{f.name}[add_domains]"
bit and just go back to
form[add_domains]
?
axdc:
no crash, the html in the inspector blinks like it’s been changed, any text entered in the field vanishes, it feels like it’s replacing the existing one with a fresh one that has the same name?
zachdaniel:
okay….and the html of the nested stuff doesn’t seem to be nested? Can you screenshot your inspector to show me that?
zachdaniel:
huh
axdc:

zachdaniel:
I think this may be a problem with the
<.inputs
component actually
zachdaniel:
potentially
zachdaniel:
Try adding
import Phoenix.HTML.Form
in the top of your module
zachdaniel:
and replace:
<.inputs_for :let={f_nested} field={f[:add_domains]}>
with
<%= for f_nested <- inputs_for(f, :add_domains) do %>
zachdaniel:
(and change the ending tag)
zachdaniel:
this will at least cut that out as the cause
axdc:
index.ex
...
import Phoenix.HTML.Form
...
index.html.heex
...
<.simple_form :let={f} for={@form} phx-change="validate">
<.h2>Domains</.h2>
<!-- <.inputs_for :let={f_nested} field={f[:add_domains]}> -->
<%= for f_nested <- inputs_for(f, :add_domains) do %>
<.input type="text" label="Domain" field={f_nested[:name]} />
<.button type="button" phx-click="remove_form" phx-value-path={f_nested.name}>
Remove domain
</.button>
<% end %>
<!-- </.inputs_for> -->
<.button type="button" phx-click="add_form" phx-value-path="form[add_domains]">
Add Domain
</.button>
...
</.simple_form>
axdc:
it appears to be the same?
zachdaniel:
I feel like this is going to be something right in front of our faces 😆
zachdaniel:
oh
zachdaniel:
OHHH
zachdaniel:
can I see the action 😆
zachdaniel:
nvm I see it above
zachdaniel:
and yeah, thats the issue
zachdaniel:
create :create_new_site do
argument :add_configuration, :map do
allow_nil? false
end
argument :add_domains, :map do
allow_nil? false
end
change manage_relationship(:add_configuration, :configuration, type: :create)
change manage_relationship(:add_domains, :domains, type: :create)
end
zachdaniel:
Should be
create :create_new_site do
argument :add_configuration, {:array, :map} do
allow_nil? false
end
argument :add_domains, {:array, :map} do
allow_nil? false
end
change manage_relationship(:add_configuration, :configuration, type: :create)
change manage_relationship(:add_domains, :domains, type: :create)
end
zachdaniel:
Or at least, if you want to accept multiple domains/configurations to add, then the type would need to be
{:array, :map}
axdc:
ahhhhh
zachdaniel:
So that is why
add_form
is just replacing the existing form. It sees that there is only one accepted value
axdc:
yes there’s one configuration and one or more domains
axdc:
omg
axdc:
🤦
axdc:
OK DUH
zachdaniel:
haha, don’t worry about it. Glad we got you there.
zachdaniel:
The link between action -> form is a lot to take in
axdc:
IMMEDIATELY just werks
axdc:
ok so I was just giving the poor thing conflicting instructions 😅
axdc:
Thank you!!!
zachdaniel:
👍 happy to help!
zachdaniel:
Hm…I just realized this post is in
announcements
😆
zachdaniel:
I should make it so that you need a special role to post here lol
axdc:
OH
axdc:
OH NO
zachdaniel:
not a big deal, I might just need to delete it
axdc:
can it be moved ;D;
zachdaniel:
but maybe we should copy the conversation somewhere?
axdc:
sorry
zachdaniel:
I don’t see an option to move it
zachdaniel:
really don’t worry about it 🙂
zachdaniel:
zachdaniel:
looks like its not a thing
zachdaniel:
meh, I’ll just leave it 🙂
zachdaniel:
We can close it, not a big deal.
zachdaniel:
I’ve fixed the permissions for this channel. Its not like this pings everyone in the server or anything, so its really no issue. I’m going to hit the sack but I’m glad we’ve got it sorted out.
zachdaniel:
💤
axdc:
just because the context is already in this thread, I figure I’ll mention for future travelers that right now when submitting a form that should error and put all the errors in the page, it sort of disappears? all the inputs vanish. I think I’m applying the form back to the socket incorrectly or ~something. i will tackle this
tomorrow
later today
def handle_event("create", form, socket) do
form = AshPhoenix.Form.validate(socket.assigns.form, form)
case AshPhoenix.Form.submit(form) do
{:ok, site} ->
{:noreply,
socket
|> put_flash(:info, "Site created successfully")
|> push_navigate(to: ~p"/sites/#{site}")}
{:error, form} ->
IO.inspect(form)
{:noreply, assign(socket, form: form)}
end
end
i will 💤 as well for now thank you again 🙇 i feel like i undesrstand more of the guts every day
dblack1:
This sounds like the LiveView is crashing and is being reloaded. I’ve made up an example repo which I’ll use to update the docs so they’re using heex and the new form components, hopefully it helps you track down your issue: https://github.com/totaltrash/form_example/blob/master/lib/my_app_web/live/grocery_live.ex
dblack1:
I reckon you might need to extract the form params in the handler:
def handle_event("create", %{"form" => form}, socket) do
zachdaniel:
You might not have anything set up in your form to show errors which is why you don’t see anything also
zachdaniel:
Either that or LV crashing 🙂