Form with Dynamically Named Field Groups

text_input accepts extra options which are passed down to the tag, so you can set your custom name that way, etc. I have some very old code that calls String.to_atom when generating a dynamic field but there is a memory pressure risk in doing that (atoms are never gc’d, in my case I am OK with this as there’s a fixed bound on what strings will be passed in – but they dont yet exist so to_existing_atoms doesn’t fit – but in your case where strings are free-input I would not do this.). I don’t believe new code should need this when using the newer form/field & components.

I think if you set your name to something like text_input(form, :not_used, name: "team[#{team_name}][wins]") you might get to where you want to go? I would look at see what field actually does when you’re providing your own name (& id?), you may find it has no impact just passing the same atom for each input.

Phoenix.HTML.Form is pretty relaxed and will create fields for anything you Access – even if it does not exist in the underlying changeset – which may be helpful. Eg form[Unstoppables_wins] should give you a %Field{} where name is something reasonable, but I think you probably want to use an embedded changeset and inputs_for.

  defmodule TournamentResult do
    use Ecto.Schema
    import Ecto.Changeset

    embedded_schema do
      field :tournament_name, :string

      embeds_many :teams, Team do
        field :name, :string
        field :wins, :integer

        # need this for cast embed
        def changeset(team, params) do
          team
          |> cast(params, [:name, :wins])
        end
      end
    end

    def changeset(tournament \\ %__MODULE__{}, params) do
      tournament
      |> cast(params, [:tournament_name])
      |> cast_embed(:teams, required: true)
    end
  end

    <.form
      for={@form}
      id="tournament_form"
      phx-change="validate"
      phx-submit="submit"
      phx-auto-recover="recover"
    >
      <.inputs_for :let={team} field={@form[:teams]}>
            <!-- the core components probably can just accept the field directly, this was
                  rough hewn from a legacy project that doesnt have them but has been updated to 1.7
                  which is why I set the name and value directly.
                 
                  You also probably want a hidden input that retains the team id (ecto should generate one)  -->
              <input
                type="text"
                placeholder="Team Name"
                autocomplete="off"
                class={[
                  "w-full",
                  team[:name].errors != [] && "placeholder-red-300 bg-red-50"
                ]}
                name={team[:name].name}
                value={team[:name].value}
              />
            <!-- still generates "valid" field even though does not exist in model -->
            <%= inspect team[:some_fake_field].name %>

Just be aware that when converted to “params” your teams change from an array to a map of numeric strings ("0" => team_1) so you lose intrinsic ordering. It seems that the newer to_form accounts for this better than it used to, but I have code there to re-index things they are added or removed, may not be needed now but just a heads up.

Not sure if all that helps you. Honestly I’m a bit unclear on what part is hard, Are you able to provide a basic (non-functioning) repo as an example? Are you using LV or regular controllers?