To_form/2 errors aren't being rendered

I’m working on a form that is not connected to a Schame nor a Changeset so I created it using the to_form/2 function:

def mount(_params, _session, socket) do
    form = to_form(%{"keywords" => "", "extensions" => ""})
    {:ok, assign(socket, form: form)}
end

The problem is that when I added errors to the form using this method:

to_form(%{"keywords" => keywords, "extensions" => extensions}, errors: [keywords: "Keywords can only contain lowercase letters, uppercase letters, and numbers"])

They aren’t rendered in the UI:

Screenshot from 2024-07-22 17-43-50

This is the code that I used for the HTML:

<section>
  <.simple_form
    :let={f}
    id="generate-domains-form"
    for={@form}
    phx-change="validate_domains"
    phx-submit="generate_domains"
  >
    <.input field={f["keywords"]} type="text" label="Keywords (seperated by comma)" required />

    <.input
      field={f["extensions"]}
      type="text"
      label="Domain Extensions (seperated by comma)"
      required
    />

    <:actions>
      <.button phx-disable-with="Generating...">Generate Domains</.button>
    </:actions>
  </.simple_form>
</section>

LV recently changed their approach of detecting dirty inputs (ones edited by a user). -You’re not passing that, so core components assume those fields are untouched and errors to be hidden.

See Phoenix.Component — Phoenix LiveView v1.0.0-rc.6 for how this new approach works.

Edit: I think I got the order wrong, the details might still be useful.

I forgot to mention that this is the output of running IO.inspect(form):

%Phoenix.HTML.Form{
  source: %{"extensions" => "", "keywords" => "!"},
  impl: Phoenix.HTML.FormData.Map,
  id: nil,
  name: nil,
  data: %{},
  action: nil,
  hidden: [],
  params: %{"extensions" => "", "keywords" => "!"},
  errors: [
    keywords: "Keywords can only contain lowercase letters, uppercase letters, and numbers",
    extensions: ""
  ],
  options: [],
  index: nil
}

Seems like you need to provide errors with string keys as well.

I’m not a 100% sure what you mean but I did look at the docs Phoenix.Component — Phoenix LiveView v0.20.17 which state:

The underlying data may accept additional options when converted to forms. For example, a map accepts :errors to list errors, but such option is not accepted by changesets. :errors is a keyword of tuples in the shape of {error_message, options_list} . Here is an example:

to_form(%{"search" => nil}, errors: [search: {"Can't be blank", []}])

Unfortunately even if I change my error to:

to_form(%{"keywords" => keywords, "extensions" => extensions}, errors: [keywords: {"Keywords can only contain lowercase letters, uppercase letters, and numbers", []}])

I still don’t get any output in my html. The new output of IO.inspect(form):

%Phoenix.HTML.Form{
  source: %{"extensions" => "", "keywords" => "!"},
  impl: Phoenix.HTML.FormData.Map,
  id: nil,
  name: nil,
  data: %{},
  action: nil,
  hidden: [],
  params: %{"extensions" => "", "keywords" => "!"},
  errors: [
    keywords: {"Keywords can only contain lowercase letters, uppercase letters, and numbers",
     []},
    extensions: ""
  ],
  options: [],
  index: nil
}

You want errors: [{"keywords", "error msg"}] any at least based on the implementation those docs are wrong.

If I use errors: [{"keywords", "error msg"}] I end up with the following big error message in the console:

[error] GenServer #PID<0.959.0> terminating
** (FunctionClauseError) no function clause matching in BrandableDomainsWeb.CoreComponents.translate_error/1
    (brandable_domains 0.1.0) lib/brandable_domains_web/components/core_components.ex:652: BrandableDomainsWeb.CoreComponents.translate_error("Keywords can only contain lowercase letters, uppercase letters, and numbers")
    (elixir 1.17.1) lib/enum.ex:1703: Enum."-map/2-lists^map/1-1-"/2
    (brandable_domains 0.1.0) lib/brandable_domains_web/components/core_components.ex:299: BrandableDomainsWeb.CoreComponents."input (overridable 1)"/1
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/tag_engine.ex:92: Phoenix.LiveView.TagEngine.component/3
    (brandable_domains 0.1.0) lib/brandable_domains_web/live/generate_domains_live.html.heex:9: anonymous fn/3 in BrandableDomainsWeb.GenerateDomainsLive.render/1
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/diff.ex:368: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/diff.ex:532: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
    (elixir 1.17.1) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/diff.ex:366: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/diff.ex:532: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
    (elixir 1.17.1) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/diff.ex:366: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/diff.ex:532: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
    (elixir 1.17.1) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/diff.ex:366: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/diff.ex:532: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
    (elixir 1.17.1) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/diff.ex:366: Phoenix.LiveView.Diff.traverse/7
    (phoenix_live_view 1.0.0-rc.6) lib/phoenix_live_view/diff.ex:532: anonymous fn/4 in Phoenix.LiveView.Diff.traverse_dynamic/7
    (elixir 1.17.1) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
Last message: %Phoenix.Socket.Message{topic: "lv:phx-F-SY-Ni1nwaI7QND", event: "event", payload: %{"event" => "validate_domains", "type" => "form", "uploads" => %{}, "value" => "_unused_keywords=&keywords=%21&_unused_extensions=&extensions=&_target=keywords"}, ref: "14", join_ref: "13"}
State: %{socket: #Phoenix.LiveView.Socket<id: "phx-F-SY-Ni1nwaI7QND", endpoint: BrandableDomainsWeb.Endpoint, view: BrandableDomainsWeb.GenerateDomainsLive, parent_pid: nil, root_pid: #PID<0.959.0>, router: BrandableDomainsWeb.Router, assigns: %{form: %Phoenix.HTML.Form{source: %{"extensions" => "", "keywords" => ""}, impl: Phoenix.HTML.FormData.Map, id: nil, name: nil, data: %{}, action: nil, hidden: [], params: %{"extensions" => "", "keywords" => ""}, errors: [], options: [], index: nil}, __changed__: %{}, current_user: nil, flash: %{}, live_action: nil}, transport_pid: #PID<0.943.0>, ...>, components: {%{}, %{}, 1}, topic: "lv:phx-F-SY-Ni1nwaI7QND", serializer: Phoenix.Socket.V2.JSONSerializer, join_ref: "13", upload_names: %{}, upload_pids: %{}}

Just for the sake of brevity here is the output from IO.inspect(form):

%Phoenix.HTML.Form{
  source: %{"extensions" => "", "keywords" => "!"},
  impl: Phoenix.HTML.FormData.Map,
  id: nil,
  name: nil,
  data: %{},
  action: nil,
  hidden: [],
  params: %{"extensions" => "", "keywords" => "!"},
  errors: [
    {"keywords",
     "Keywords can only contain lowercase letters, uppercase letters, and numbers"}
  ],
  options: [],
  index: nil
}

Ah, that might be core_components expecting errors in the changset format of {msg, opts}. That’s independent of Phoenix.HTML.FormData though. You could customize translate_error to be able to handle plain strings as well. Phoenix might even do that if you generate it without ecto support.

Your earlier suggestion was really close to the answer. The answer is to use to_form/2 like this:

form = to_form(%{"keywords" => keywords, "extensions" => extensions}, errors: [{"keywords", {"error msg", []}}])

Screenshot from 2024-07-22 21-07-14

Thank you so much for your help!