Hi,
I’d like to create a form in Phoenix 1.7 with one text field that has no underlying changeset. Previously I’d use form_for
along with text_input
. Now, I guess, I should use .simple_form
but I don’t know what should be there in the for
attribute. When I try to use @conn
, as in the 1.6, I get the error: function Plug.Conn.fetch/2 is undefined (Plug.Conn does not implement the Access behaviour. If you are using get_in/put_in/update_in, you can specify the field to be accessed using Access.key!/1)
.
What is the proper way to create an unbound form in Phoenix 1.7?
Thanks
Hello, from the Phoenix.HTML.Form — Phoenix.HTML v3.3.1 docs, it looks like you can use it with a Map:
With map data
form_for/4 expects as first argument any data structure that implements the Phoenix.HTML.FormData protocol. By default, Phoenix.HTML implements this protocol for Map.
This is useful when you are creating forms that are not backed by any kind of data layer. Let’s assume that we’re submitting a form to the :new action in the FooController:
<%= form_for @conn.params, Routes.foo_path(@conn, :new), fn f -> %>
<%= text_input f, :contents %>
<%= submit "Search" %>
<% end %>
hth
Hi,
thanks for the quick reply. This is exactly what I had before I started migrating to 1.7. Is this still the way that it should be done?
Sorry, I didn’t write any form since 1.5.7, 
I’d just give @conn.params a try
.
.simple_form
is a component defined within the core_components.ex
generated via mix phx.gen.new
and is a simple wrapper around the Phoenix.Component.form/1
function provided by LiveView.
Using the for
attribute
The for
attribute can also be a map or an Ecto.Changeset. In such cases, a form will be created on the fly, and you can capture it using :let
:
<.form
:let={form}
for={@changeset}
phx-change="change_user"
>
However, such approach is discouraged in LiveView for two reasons:
- LiveView can better optimize your code if you access the form fields using
@form[:field]
rather than through the let-variable form
- Ecto changesets are meant to be single use. By never storing the changeset in the assign, you will be less tempted to use it across operations
source: Phoenix.Component.form/1
2 Likes
I see I’m mixing a couple of things here… To clarify more: I have a ‘standard’ (not LiveView) page and I wonder what is the proper way to create such form in Phoenix 1.7. Sorry for confusion.
A bit further down in the docs, there’s an example noting that an action
attribute is required for standard forms.
Example: outside LiveView (regular HTTP requests)
The form
component can still be used to submit forms outside of LiveView. In such cases, the action
attribute MUST be given. Without said attribute, the form
method and csrf token are discarded.
<.form :let={f} for={@changeset} action={Routes.comment_path(:create, @comment)}>
<.input field={f[:body]} />
</.form>
In the example above, we passed a changeset to for
and captured the value using :let={f}
. This approach is ok outside of LiveViews, as there are no change tracking optimizations to consider.
source: Example: outside LiveView (regular HTTP requests)
1 Like
I’m curious on this as well. Currently I just cheat and make a form component with no changeset. For example for creating a form to sort content based on when it was created I can use the below.
Heex:
<.form
let={f}
id="time-sort-form"
phx-target={@myself}
phx-change="update">
<.input
field={f[:time]}
type="select"
options=
{
[ {"Today", "today"},
{"Week", "week"},
{"Month", "month"},
{"Year", "year"},
{"All Time", "all time"}
]
} />
</.form>
</div>
The ex:
defmodule APP.SortLive.FormComponent do
use APP, :live_component
@impl true
def handle_event("update", %{"time" => time}, socket) do
username = socket.assigns.user.username
time = time
sort = "popular"
{:noreply, socket |> push_patch(to: ~p"/user/#{username}/?#{[sort: sort, time: time]}")}
end
end
I pretty much create a form without a changeset, then use a handle_event to deal with the input. I do similar for things like search inputs as well, but my handle_event passes the query value to a function for the redirect.
I’d also like a nice neat example of a changeset free form as I’m pretty sure I’m doing this either wrong or ineffeciently. It works, but feels off.
2 Likes
I think it looks like this…
def mount(_params, _session, socket) do
{
:ok,
socket
|> assign_form(%{})
}
end
You use an empty map as a changeset. Then in the form…
<.simple_form for={@form} as={:form} id="form-graph" phx-submit="save">
As You use normal controllers, it might look a little bit different, especially phx-submit won’t be available. But it should be the way to have a form without changeset.
BTW it’s how phx.gen.auth does when You select non live…
<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/users/log_in"}>
2 Likes
Seems reasonable to me, .form
automatically sets the :for
assign to %{}
by default when it’s not explicitly given.
If you wanted to show which option is currently selected, you could then explicitly set a form assign so that the input component can pull the field value off of the form when generating the options via <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
.
<.form
let={f}
for={@form}
id="time-sort-form"
phx-target={@myself}
phx-change="update">
<.input
type="select"
field={f[:time]}
options={@time_options}
/>
</.form>
</div>
defmodule MyApp.SortLive.FormComponent do
use MyApp, :live_component
def mount(socket) do
time_form = to_form(%{"time" => "today"}
time_options = [{"Today", "today"}, ..., {"All Time", "all time"}]
{:ok,
socket
|> assign(:sort, "popular")
|> assign(:time_form, time_form)
|> assign(:time_options, time_options)}
end
def handle_event("update", %{"time" => time}, socket) do
username = socket.assigns.user.username
sort = socket.assigns.sort
time_form = to_form(%{"time" => time})
{:noreply,
socket
|> assign(:time_form, time_form)
|> push_patch(to: ~p"/user/#{username}/?#{[sort: sort, time: time]}")}
end
end
1 Like