Hello Everyone,
I am still quite new to the Phoenix framework and I am struggling with the has_many relationship using ecto and how to render those has_may to an <.simple_form> component.
I am trying to setup a User role page based on a sitemap.
My current structure is as follows:
- Sitemap
defmodule Plm.Sitemap do
use Ecto.Schema
import Ecto.Changeset
schema "sitemap" do
field :code, :string
field :displayname, :string
field :level, :integer
field :description, :string
field :parent, :string
end
def changeset(user, attrs) do
user
|> cast(attrs, [:code, :displayname, :level, :description])
end
end
- Role
defmodule Plm.Role do
use Ecto.Schema
import Ecto.Changeset
schema "role" do
field :name, :string
field :description, :string
has_many :role_permissions, Plm.RolePermission
end
def changeset(role, attrs) do
role
|> cast(attrs, [:name, :description])
|> validate_required(:name)
|> cast_assoc(:role_permissions)
end
end
- Role Permissions
defmodule Plm.RolePermission do
use Ecto.Schema
import Ecto.Changeset
schema "rolepermission" do
belongs_to :role, Role
field :permission, :integer
field :sitemap_code, :string
field :sitemap_name, :string
field :sitemap_level, :integer
end
def changeset(role_permission, attrs) do
role_permission
|> cast(attrs, [:permission, :sitemap_code, :sitemap_name, :sitemap_level])
|> validate_required([:permission, :sitemap_code, :sitemap_name, :sitemap_level])
end
end
Now I am trying to create the form to be able to create the Role and RolePermissions as follows:
- Controller
defmodule PlmWeb.RoleController do
use PlmWeb, :controller
alias Plm.SitemapService
alias Plm.Role
def new(conn, _params) do
# Fetch the sitemap data
sitemap_entries = SitemapService.list()
# Transform sitemap data into RolePermission structs
role_permissions = Enum.map(sitemap_entries, fn entry ->
%{
permission: 0, # Set your default permission here
sitemap_code: entry.code,
sitemap_name: entry.displayname,
sitemap_level: entry.level
}
end)
# Create a new role with associated role_permissions
changeset = %Role{}
|> Role.changeset(%{role_permissions: role_permissions})
render(conn, "new.html", changeset: changeset, page_title: "Create Role")
end
end
- role_html.heex
defmodule PlmWeb.RoleHTML do
use PlmWeb, :html
embed_templates "role_html/*"
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
def role_form(assigns)
end
- new.html.heex
<div class="mx-auto max-w-2xl">
<.role_form changeset={@changeset} action={~p"/roles"}/>
<.back navigate={~p"/"}>Home</.back>
</div>
- role_form.html.heex
<.simple_form :let={f} for={@changeset} action={@action}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
</.error>
<.input field={f[:name]} type="text" label="Name" />
<.input field={f[:description]} type="text" label="Description" />
<!-- Loop through each RolePermission changeset -->
<.inputs_for :let={item_f} field={f[:role_permissions]}>
<div class="mt-2 flex items-center justify-between gap-6">
<div class="w-[100%] flex">
<.label for="test" inner_block={item_f[:sitemap_name]}> <%= item_f[:sitemap_name] %></.label>
<.input class="w-[30%]" field={item_f[:permission]} type="select" value="0" options={[{"None", 0}, {"View", 1}, {"View & Edit", 2}, {"View, Edit & Delete", 4}, {"Full", 8}]} />
</div>
</div>
</.inputs_for>
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
All this according to different sources should allow me to render a label with the string sitemap_name and a dropdown.
However when i run this code i get the following error:
[error] ** (Protocol.UndefinedError) protocol Phoenix.HTML.Safe not implemented for %Phoenix.HTML.FormField{id: "role_role_permissions_0_sitemap_name", name: "role[role_permissions][0][sitemap_name]", errors: [], field: :sitemap_name, form: %Phoenix.HTML.Form{source: #Ecto.Changeset<action: nil, changes: %{permission: 0, sitemap_code: "H", sitemap_level: 0, sitemap_name: "Home"}, errors: [], data: #Plm.RolePermission<>, valid?: true>, impl: Phoenix.HTML.FormData.Ecto.Changeset, id: "role_role_permissions_0", name: "role[role_permissions][0]", data: %Plm.RolePermission{__meta__: #Ecto.Schema.Metadata<:built, "rolepermission">, id: nil, role_id: nil, role: #Ecto.Association.NotLoaded<association :role is not loaded>, permission: nil, sitemap_code: nil, sitemap_name: nil, sitemap_level: nil}, action: nil, hidden: [{"_persistent_id", "0"}], params: %{"_persistent_id" => "0", "permission" => 0, "sitemap_code" => "H", "sitemap_level" => 0, "sitemap_name" => "Home"}, errors: [], options: [multipart: false], index: 0}, value: "Home"} of type Phoenix.HTML.FormField (a struct). This protocol is implemented for the following type(s): Atom, BitString, Date, DateTime, Decimal, Float, Integer, List, NaiveDateTime, Phoenix.LiveComponent.CID, Phoenix.LiveView.Component, Phoenix.LiveView.Comprehension, Phoenix.LiveView.JS, Phoenix.LiveView.Rendered, Time, Tuple, URI
(phoenix_html 4.1.1) lib/phoenix_html/safe.ex:1: Phoenix.HTML.Safe.impl_for!/1
(phoenix_html 4.1.1) lib/phoenix_html/safe.ex:15: Phoenix.HTML.Safe.to_iodata/1
(plm 0.1.0) lib/plm_web/controllers/role/role_html/role_form.html.heex:13: anonymous fn/3 in PlmWeb.RoleHTML."role_form (overridable 1)"/1
(phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/engine.ex:149: Phoenix.HTML.Safe.Phoenix.LiveView.Rendered.to_iodata/1
(phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/engine.ex:165: Phoenix.HTML.Safe.Phoenix.LiveView.Rendered.to_iodata/3
(phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/engine.ex:105: Phoenix.HTML.Safe.Phoenix.LiveView.Comprehension.to_iodata/2
(phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/engine.ex:106: Phoenix.HTML.Safe.Phoenix.LiveView.Comprehension.to_iodata/2
(phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/engine.ex:101: anonymous fn/3 in Phoenix.HTML.Safe.Phoenix.LiveView.Comprehension.to_iodata/1
(elixir 1.14.0) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
(phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/engine.ex:101: Phoenix.HTML.Safe.Phoenix.LiveView.Comprehension.to_iodata/1
(phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/engine.ex:165: Phoenix.HTML.Safe.Phoenix.LiveView.Rendered.to_iodata/3
(phoenix 1.7.14) lib/phoenix/controller.ex:1008: anonymous fn/5 in Phoenix.Controller.template_render_to_iodata/4
(telemetry 1.2.1) /home/cornelis/Desktop/git/PLM/deps/telemetry/src/telemetry.erl:321: :telemetry.span/3
(phoenix 1.7.14) lib/phoenix/controller.ex:974: Phoenix.Controller.render_and_send/4
(plm 0.1.0) lib/plm_web/controllers/role/role_controller.ex:1: PlmWeb.RoleController.action/2
(plm 0.1.0) lib/plm_web/controllers/role/role_controller.ex:1: PlmWeb.RoleController.phoenix_controller_pipeline/2
(phoenix 1.7.14) lib/phoenix/router.ex:484: Phoenix.Router.__call__/5
(plm 0.1.0) lib/plm_web/endpoint.ex:1: PlmWeb.Endpoint.plug_builder_call/2
(plm 0.1.0) lib/plug/debugger.ex:136: PlmWeb.Endpoint."call (overridable 3)"/2
(plm 0.1.0) lib/plm_web/endpoint.ex:1: PlmWeb.Endpoint.call/2
And I am stuck at this. Nothing I do seems to change.
I have tried changing the controller code to:
role_permissions = Enum.map(sitemap_entries, fn entry ->
%Plm.RolePermission{
permission: 0, # Set your default permission here
sitemap_code: entry.code,
sitemap_name: entry.displayname,
sitemap_level: entry.level
}
end)
But then i get a different error:
[debug] ** (Ecto.CastError) expected params to be a :map, got: `%Plm.RolePermission{__meta__: #Ecto.Schema.Metadata<:built, "rolepermission">, id: nil, role_id: nil, role: #Ecto.Association.NotLoaded<association :role is not loaded>, permission: 0, sitemap_code: "H", sitemap_name: "Home", sitemap_level: 0}`
(ecto 3.11.2) lib/ecto/changeset.ex:722: Ecto.Changeset.cast/4
(plm 0.1.0) lib/plm/roles/role_permission/role_permission.ex:15: Plm.RolePermission.changeset/2
(ecto 3.11.2) lib/ecto/changeset.ex:1362: anonymous fn/4 in Ecto.Changeset.on_cast_default/2
(ecto 3.11.2) lib/ecto/changeset/relation.ex:131: Ecto.Changeset.Relation.do_cast/7
(ecto 3.11.2) lib/ecto/changeset/relation.ex:365: Ecto.Changeset.Relation.map_changes/11
(ecto 3.11.2) lib/ecto/changeset/relation.ex:112: Ecto.Changeset.Relation.cast/5
(ecto 3.11.2) lib/ecto/changeset.ex:1278: Ecto.Changeset.cast_relation/4
(plm 0.1.0) lib/plm_web/controllers/role/role_controller.ex:23: PlmWeb.RoleController.new/2
(plm 0.1.0) lib/plm_web/controllers/role/role_controller.ex:1: PlmWeb.RoleController.action/2
(plm 0.1.0) lib/plm_web/controllers/role/role_controller.ex:1: PlmWeb.RoleController.phoenix_controller_pipeline/2
(phoenix 1.7.14) lib/phoenix/router.ex:484: Phoenix.Router.__call__/5
(plm 0.1.0) lib/plm_web/endpoint.ex:1: PlmWeb.Endpoint.plug_builder_call/2
(plm 0.1.0) lib/plug/debugger.ex:136: PlmWeb.Endpoint."call (overridable 3)"/2
(plm 0.1.0) lib/plm_web/endpoint.ex:1: PlmWeb.Endpoint.call/2
(phoenix 1.7.14) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
(bandit 1.5.5) lib/bandit/pipeline.ex:124: Bandit.Pipeline.call_plug!/2
(bandit 1.5.5) lib/bandit/pipeline.ex:36: Bandit.Pipeline.run/4
(bandit 1.5.5) lib/bandit/http1/handler.ex:12: Bandit.HTTP1.Handler.handle_data/3
(bandit 1.5.5) lib/bandit/delegating_handler.ex:18: Bandit.DelegatingHandler.handle_data/3
(bandit 1.5.5) lib/thousand_island/handler.ex:411: Bandit.DelegatingHandler.handle_continue/2
After playing around with it for a while i noticed if i remove the following code from the role_form.html.heex
<%= item_f[:sitemap_name] %>
The error disappears.
I am most likely missing something very basic. But I am unable to find out what.
Any help would be greatly appreciated.
Cees