Nested forms for `has_many` relations with `inputs_for` not working

I generated a Todo app with Phoenix generators.

Schema for todo list

defmodule MyApp.Todos.TodoList do
  use Ecto.Schema
  import Ecto.Changeset

  schema "todo_lists" do
    field :title, :string
    has_many :todo_items, MyApp.Todos.TodoItem

    timestamps()
  end

  @doc false
  def changeset(todo_list, attrs) do
    todo_list
    |> cast(attrs, [:title])
    |> validate_required([:title])
    |> cast_assoc(:todo_items)
  end
end

Schema for todo items

defmodule MyApp.Todos.TodoItem do
  use Ecto.Schema
  import Ecto.Changeset

  schema "todo_items" do
    field :description, :string
    belongs_to :todo_list, MyApp.Todos.TodoList

    timestamps()
  end

  @doc false
  def changeset(todo_item, attrs) do
    todo_item
    |> cast(attrs, [:description])
    |> validate_required([:description])
  end
end

And the Liveview form

<div>
  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="todo_list-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">
  
    <%= label f, :title %>
    <%= text_input f, :title %>
    <%= error_tag f, :title %>

    <%= inputs_for f, :todo_items, fn item_form -> %>
      <%= label item_form, :description %>
      <%= text_input item_form, :description %>
      <%= error_tag item_form, :description %>
    <% end %>
    <div>
      <%= submit "Save", phx_disable_with: "Saving..." %>
    </div>
  </.form>
</div>

The LiveView form is not rendering fields from the todo_items schema, it’s only rendering the fields in the todo_list schema.

Screenshot for todo list form:

However, if I change the relation from has_many to has_one, like this has_many :todo_items, MyApp.Todos.TodoItem to has_one :todo_items, MyApp.Todos.TodoItem the fields from the related todo_items schema are shown in the form. See the screenshot below:

Not sure what am doing wrong because I know that adding cast_assoc(:todo_items) to the todo_list changeset should take care of rendering relationships in the forms.

For reference look this example from ecto docs: Polymorphic associations with many to many — Ecto v3.8.4

That‘s not really correct. Changing the schema does nothing to your form - it never did. You‘d need to adjust the form manually or re-generate everything with the phoenix generators. Iirc there‘s a way to have it deal with relationships as well.

I did update the form to render fields from the todo_items schema using the inputs_for function like this:

<%= label f, :title %>
    <%= text_input f, :title %>
    <%= error_tag f, :title %>

    <%= inputs_for f, :todo_items, fn item_form -> %>
      <%= label item_form, :description %>
      <%= text_input item_form, :description %>
      <%= error_tag item_form, :description %>
    <% end %>

I’m not sure whether this worked for you or not, but I was facing the same issue and haven’t been able to solve it that way.

The solution is pretty simple, actually, just needed to read the docs. There is an example given there with what was required to make it work: list comprehension.
Docs

Code:

<%= f = form_for @changeset, Routes.user_path(@conn, :create), opts %>
  Name: <%= text_input f, :name %>

  <%= for friend_form <- inputs_for(f, :friends) do %>
    # for generating hidden inputs.
    <%= hidden_inputs_for(friend_form) %>
    <%= text_input friend_form, :name %>
  <% end %>
</form>