Forms and Embedded Schemas

I’m attempting to wrap my head around JSONB columns, embedded schemas, and using them in forms. To that end, I’ve created a bit of a tester to try things out.

So far, here’s the module(s):

defmodule MapTypeTester.Users.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :name, :string
    embeds_one(:favorite_colors, MapTypeTester.FavoriteColors)

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email])
    |> cast_embed(:favorite_colors)
    |> validate_required([:name, :email])
  end
end

defmodule MapTypeTester.FavoriteColors do
  alias MapTypeTester.Users.User
  use Ecto.Schema

  embedded_schema do
    field(:color_one, :string)
    field(:color_two, :string)
    field(:color_three, :string)
    belongs_to(:users, User)
  end

  def changeset(favorite_colors, attrs) do
    favorite_colors
    |> Ecto.Changeset.cast(attrs, [:color_one, :color_two, :color_three])
  end
end

The controller (I’m in the middle of some changes right now, and the error I am hitting is what stopped me):

defmodule MapTypeTesterWeb.UserController do
  use MapTypeTesterWeb, :controller

  alias MapTypeTester.Users
  alias MapTypeTester.Users.User
  alias MapTypeTester.FavoriteColors

  def index(conn, _params) do
    users = Users.list_users()
    render(conn, "index.html", users: users)
  end

  def new(conn, _params) do
    changeset = Users.change_user(%User{})
    colors_changeset = Users.change_colors(%FavoriteColors{})
    render(conn, "new.html", changeset: changeset, colors_changeset: colors_changeset)
  end

  def create(conn, %{"user" => user_params}) do
    case Users.create_user(user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User created successfully.")
        |> redirect(to: Routes.user_path(conn, :show, user))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, %{"id" => id}) do
    user = Users.get_user!(id)
    render(conn, "show.html", user: user)
  end

  def edit(conn, %{"id" => id}) do
    user = Users.get_user!(id)
    changeset = Users.change_user(user)
    render(conn, "edit.html", user: user, changeset: changeset)
  end

  def update(conn, %{"id" => id, "user" => user_params}) do
    user = Users.get_user!(id)

    case Users.update_user(user, user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User updated successfully.")
        |> redirect(to: Routes.user_path(conn, :show, user))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "edit.html", user: user, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    user = Users.get_user!(id)
    {:ok, _user} = Users.delete_user(user)

    conn
    |> put_flash(:info, "User deleted successfully.")
    |> redirect(to: Routes.user_path(conn, :index))
  end
end

and the form:

<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

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

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

  <%= form_for @colors_changeset, @action, fn fcolor -> %>
    <%= label fcolor, :favorite_colors[:color_one] %>
    <%= text_input fcolor, :favorite_colors[:color_one] %>
    <%= error_tag fcolor, :favorite_colors[:color_one] %>

    <%= label fcolor, :favorite_colors[:color_two] %>
    <%= text_input fcolor, :favorite_colors[:color_two] %>
    <%= error_tag fcolor, :favorite_colors[:color_two] %>

    <%= label fcolor, :favorite_colors[:color_three] %>
    <%= text_input fcolor, :favorite_colors[:color_three] %>
    <%= error_tag fcolor, :favorite_colors[:color_three] %>
  <% end %>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

So far, when I attempt to go to localhost:4000/users, I am met with:

[error] #PID<0.478.0> running MapTypeTesterWeb.Endpoint (connection #PID<0.477.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /users
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Phoenix.HTML.Safe not implemented for %MapTypeTester.FavoriteColors{color_one: nil, color_three: nil, color_two: nil, id: nil, users: #Ecto.Association.NotLoaded<association :users is not loaded>, users_id: nil} of type MapTypeTester.FavoriteColors (a struct). This protocol is implemented for the following type(s): Decimal, Phoenix.LiveView.Rendered, Phoenix.LiveView.Comprehension, Phoenix.LiveView.Component, Phoenix.LiveComponent.CID, List, Tuple, Integer, Float, NaiveDateTime, BitString, DateTime, Atom, Phoenix.HTML.Form, Time, Date
        (phoenix_html) lib/phoenix_html/safe.ex:1: Phoenix.HTML.Safe.impl_for!/1
        (phoenix_html) lib/phoenix_html/safe.ex:15: Phoenix.HTML.Safe.to_iodata/1
        (map_type_tester) lib/map_type_tester_web/templates/user/index.html.eex:18: anonymous fn/3 in MapTypeTesterWeb.UserView."index.html"/1
        (elixir) lib/enum.ex:1948: Enum."-reduce/3-lists^foldl/2-0-"/3
        (map_type_tester) lib/map_type_tester_web/templates/user/index.html.eex:14: MapTypeTesterWeb.UserView."index.html"/1
        (phoenix) lib/phoenix/view.ex:310: Phoenix.View.render_within/3
        (phoenix) lib/phoenix/view.ex:472: Phoenix.View.render_to_iodata/3
        (phoenix) lib/phoenix/controller.ex:776: Phoenix.Controller.render_and_send/4
        (map_type_tester) lib/map_type_tester_web/controllers/user_controller.ex:1: MapTypeTesterWeb.UserController.action/2
        (map_type_tester) lib/map_type_tester_web/controllers/user_controller.ex:1: MapTypeTesterWeb.UserController.phoenix_controller_pipeline/2
        (phoenix) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2
        (map_type_tester) lib/map_type_tester_web/endpoint.ex:1: MapTypeTesterWeb.Endpoint.plug_builder_call/2
        (map_type_tester) lib/plug/debugger.ex:136: MapTypeTesterWeb.Endpoint."call (overridable 3)"/2
        (map_type_tester) lib/map_type_tester_web/endpoint.ex:1: MapTypeTesterWeb.Endpoint.call/2
        (phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4
        (cowboy) /home/cedric/tutorials/map_type_tester/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
        (cowboy) /home/cedric/tutorials/map_type_tester/deps/cowboy/src/cowboy_stream_h.erl:300: :cowboy_stream_h.execute/3
        (cowboy) /home/cedric/tutorials/map_type_tester/deps/cowboy/src/cowboy_stream_h.erl:291: :cowboy_stream_h.request_process/3
        (stdlib) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

I…cannot quite figure out what’s giving me this error, and would most greatly appreciate any assistance towards getting there.

I think what you are looking for is input_for. Try following this doc

5 Likes

Man this feels like the biggest waste of a post, and I do thank you for responding and, most especially, your assistance. I swear I’ve seen this before and just couldn’t pull it out of my memory hole.

2 Likes

Haha it happens man! enjoy!

Sure thing! I will admit that I’ve run into this error since, though I assume that it’s because of the attempt at parsing a map as a string:

Protocol.UndefinedError at GET /users/3
protocol Phoenix.HTML.Safe not implemented for %MapTypeTester.FavoriteColors{color_one: "yellow", color_three: "red", color_two: "blue", id: "73a49e45-93ea-4794-a9ab-b517eea823e4", users: #Ecto.Association.NotLoaded<association :users is not loaded>, users_id: nil} of type MapTypeTester.FavoriteColors (a struct). This protocol is implemented for the following type(s): Decimal, Phoenix.LiveView.Rendered, Phoenix.LiveView.Comprehension, Phoenix.LiveView.Component, Phoenix.LiveComponent.CID, List, Tuple, Integer, Float, NaiveDateTime, BitString, DateTime, Atom, Phoenix.HTML.Form, Time, Date

What does your form look like now?

Form looks like this:

<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

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

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

  <h2>Favorite Colors</h2>

  <%= inputs_for f, :favorite_colors, fn fcolor -> %>
    <%= label fcolor, :color_one %>
    <%= text_input fcolor, :color_one %>
    <%= error_tag fcolor, :color_one %>

    <%= label fcolor, :color_two %>
    <%= text_input fcolor, :color_two %>
    <%= error_tag fcolor, :color_two %>

    <%= label fcolor, :color_three %>
    <%= text_input fcolor, :color_three %>
    <%= error_tag fcolor, :color_three %>
  <% end %>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

which results in this:

So that part’s successful.

It’s the index page:

<h1>Listing Users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th>Favorite colors</th>

      <th></th>
    </tr>
  </thead>
  <tbody>
<%= for user <- @users do %>
    <tr>
      <td><%= user.name %></td>
      <td><%= user.email %></td>
      <%= for color <- user.favorite_colors do %>
        <td><%= color.key %>: <%= color.value %></td>
      <% end %>

      <td>
        <span><%= link "Show", to: Routes.user_path(@conn, :show, user) %></span>
        <span><%= link "Edit", to: Routes.user_path(@conn, :edit, user) %></span>
        <span><%= link "Delete", to: Routes.user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"] %></span>
      </td>
    </tr>
<% end %>
  </tbody>
</table>

<span><%= link "New User", to: Routes.user_path(@conn, :new) %></span>

Yeah the issue is you cant enumerate over a map.

Maybe you could make a function like this:

  defp favorite_colors(%User{favorite_colors: favorite_colors}) do
    favorite_colors
    |> Map.to_list()
    |> Enum.filter(fn {key, _} ->
      Atom.to_string(key) =~ "color_"
    end)
  end

It would look for all the color_ keys in your favorite_color struct, and return a list of those colors.

then you could use like this:

<%= for {key, color} <- favorite_colors(user) do %>
    <td><%= key %>: <%= color %></td>
<% end %>

OR

you could just add 3 <td> tags and grab it out manually.

3 Likes

And that’s what finally did the trick! Thank you so much! It’s been a long day of trying to get this sorted. May all the good things in the world happen to you.

2 Likes