** (Protocol.UndefinedError) protocol Phoenix.HTML.Safe not implemented for type Ecto.Changeset (a struct).
because the value of the field @form[:categories] contains a Category changeset, introduced by the change_article\2 function - which is taken from the Context Article
def change_article(%Article{} = article, attrs \\ %{}) do
categories = list_categories_by_id(attrs["categories"])
article
|> Repo.preload(:categories)
|> Article.changeset(attrs)
|> Ecto.Changeset.put_assoc(:categories, categories)
end
Phoenix tries to render the value in the select component but the changeset doesn’t help much here
attrs["categories"] there needs to be category_ids, too. list_categories_by_id and put_assoc are doing the work of wiring up the associated data. Read more about it here: Ecto.Changeset — Ecto v3.12.5
When you submit the form, part of the article params will be category_ids. When that goes into the create or update context functions, those will call change_article where it gets passed to list_categories_by_id and the result of that query gets shoved into the changeset by put_assoc.
I think the confusion is coming from the relationship between the Phoenix.HTML.Form struct that comes from the to_form function and what you pass to it(in this case %Article{})?
It is possible to “access” fields which do not exist in the source data structure. A Phoenix.HTML.FormField struct will be dynamically created with some attributes such as name and id populated.
Yes I noticed that it’s possible, but I didn’t understand when and how to make use of it.
The frameworks I used so far, expected to have a form data structure completely defined to render it properly and I treated the changeset as such in my mind.
In Phoenix, if you want to sketch out a form quickly, you can use to_form(%{}, as: "some_name"). You can then put whatever inputs to shape the params however you like.
I’d suggest reframing “article form setting categories” to “article form managing article_categories”, so moving from many to many to has many relationship. Then you can use inputs_for + select for category_id.
Yeah, there’s docs for that on the function documentation for <.inputs_for>, eventually you also want to avoid allowing duplicate category selection. But that doesn’t concern the form input anymore. It’s all just presentation logic and constraints.