{@thread.name}

axdc
2023-04-08

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

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:

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 🙂