kccarter

kccarter

We’re trying to create a form using the .form component and a map but unable to figure out the right combination. We are not using Ecto. Our data is unstructured JSON stored in a NoSQL backend.

The documentation says to use to_form with a map, but we keep getting stuck with the error that the given map does not implement HTML.Safe.

<.form :let={f} for={@account} action={~p"/accounts/#{@tag["id"]}"}>
    # We know there's always a name, but in reality 
    # this is a for-loop over the account's keys.
    <.input field={f["name"]} />
</.form>
defmodule DemoWeb.AccountsController do
  def edit(conn, %{"id" => id}) do
    # This returns a Map of arbitrary key/value pairs.
    account = Accounts.find!(id) 
    
    # Is this what we're supposed to do?
    account_form = Phoenix.Component.to_form(account)
    
    render(conn, :edit, account: account_form)
  end
end

Our goal is to just render a form for a map, and then have the form fields posted back to the appropriate endpoint, where we’ll convert it back to a JSON blob and store it accordingly. Ecto, and Ecto changesets, are not used anywhere.

Showing Posts 1 to 10

sodapopcan

sodapopcan

Where is tag coming from? If account is a map, then this is the general idea:

account = %{"name" => "Account name"}
account_form = Phoenix.Component.to_form(account)
render(conn, :edit, account: account_form

then in the template:

<.form for={@account}>
  <.input field={@account[:name] />
</.form>
kccarter

kccarter OP

My bad. tag was meant to be account.

sodapopcan

sodapopcan

I’m assuming account is a struct since that would cause that error. You have to use either a bare map or a changeset with to_form. You can convert a struct to a map with Map.from_struct although you’ll get a warning that you should not be using atom keys with to_form. From there you could convert all the keys to strings but I would just use a changeset. Since you aren’t using changesets anywhere else, you won’t need to do anything else with them other than convert your struct to one just for use with the form:

account_form =
  account
  |> Ecto.Changeset.change()
  |> Phoenix.Component.to_form()

change just creates a changeset, no-questions-asked (no casting or anything) so it literally only needs to exist for the form.

kccarter

kccarter OP

Account was just a Map. The following still produces an error:

def edit(conn, _) do
  tag = Phoenix.Component.to_form(%{"name" => "Tim"})
  render(conn, :edit, tag: tag)
end

<.form :let={f} for={@tag}>
  <div>
    <label for="name">Name:</label>
    <.input field={f["name"]} />
  </div>
</.form>

Produces this error:

protocol Phoenix.HTML.Safe not implemented for %Phoenix.HTML.FormField{id: “id”, name: “id”, errors: , field: “id”, form: %Phoenix.HTML.Form{source: %{“name” => “Tim”}, impl: Phoenix.HTML.FormData.Map,…

That code above seems to be converting a normal Map using to_form as suggested by the documentation, but we’re obviously missing an important step.

sodapopcan

sodapopcan

Ohhhh crap, I’m sorry. It’s been a million years since I’ve used a controller with a form like that. AFAIR, you just use the bare map, no need for to_form. to_form is largely about change tracking which is a LiveView-specific thing. You can just do:

render(conn, :edit, account: %{"name" => "Name"})

<.form :let={f} for={@account} as="account">
  <.input field={f[:name]} />
</.form>

The as option will let you collect the params under an "account" key.

kccarter

kccarter OP

Awesome. Thank you.

We also noticed another issue that was causing a Safe HTML error but wasn’t related to the form, but rather a line further down in the heex file. Fridays..

sodapopcan

sodapopcan

That basically means you are trying to to_string some value into HTML. Most of the time it’s a struct, but it could be a tuple or fun or whatever doesn’t cast into a string (or HTML in this case, to be a bit more technical).

Sebb

Sebb

EDIT this anwser is OT, because this is about Controllers, not LV.

IF YOU’D BE USING LIVEVIEW you should not use :let here, see: Phoenix.Component — Phoenix LiveView v0.20.7

This works WITH LV:

def mount(_params, _session, socket) do
  form = to_form(%{"username" => "Kermit", "email" => "kermit@muppets.com"})
  {:ok, assign(socket, form: form)}
end
<.form for={@form} phx-change="validate" phx-submit="save">
  <.input type="text" field={@form[:username]} />
  <.input type="text" field={@form[:email]} />
  <.button>Save</.button>
</.form>
sodapopcan

sodapopcan

You’re making a similar mistake that I made—they are using controllers, not LiveViews. to_form just doesn’t work with controllers (from the bit of testing I did). And because of this it doesn’t really matter if they use :let or not (I also had the same thought, though).

kccarter

kccarter OP

Yeah, in a framework with relatively good documentation, we’ve found the narrative around forms to be a bit confusing and hard to follow. We’re not using LiveView, so the fact that .form is part of Phoneix.LiveView initially made us look past it.

There’s form_for in Phoenix.HTML but this blog post suggests that it’s now deprecated. And this older forum post has a post from Jose saying that .form is the correct way to go, but as noted above we initially thought that was just for LiveViews.

In core_components.ex there’s .simple_form which is used throughout the generated scaffolding code. In this case you can quickly see it just calls through to .form, but when you’re initially learning it’s an added layer of indirection.

In our case, where we’re just using a plain Map, the “secret” attribute to make it work was as:.

We’re editing JSON records with an arbitrary and unknown set of keys. All we really want a form component for is to have the proper _method and _csrf attributes injected along with proper field naming.

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
dillonoconnor
Is there any way to avoid the Hologram compiler running when using iex? It seems like the front-end code could potentially be disregarded...
New
thiagogsr
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews