The standard way to create LiveView forms is to use the to_form/1 helper by passing it a changeset structure, for example:

to_form(Accounts.change_user(%User{})))

However, situations can arise where we have a form without having to back it up with a changeset.

Why skip the changeset?

Ecto.Changeset is a tool for working with information from various sources. It provides validations, castings, and error handling. Typically, we create forms based on an Ecto schema, meaning they have a table in our database. However, there are situations where generating a changeset can be excessive or unnecessary, either based on a schema or in its schemaless form.

If we simply want to capture data without the need for complex validations, you might want to avoid using a changeset.

Let’s use a contact form as an example (assuming you already have a Phoenix project running):

defmodule MyAppWeb.ContactLive do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    params = %{"name" => "", "subject" => "", "body" => "", "email" => ""}
    {:ok, assign(socket, :form, to_form(params))}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        <:subtitle>Contact</:subtitle>
      </.header>

      <.simple_form for={@form} id="contact_form" phx-submit="save">
        <.input field={@form[:name]} label="Name" />
        <.input field={@form[:subject]} label="Subject" />
        <.input field={@form[:body]} type="textarea" label="Body" />
        <.input field={@form[:email]} type="email" label="Email" />

        <:actions>
          <.button phx-disable-with="Saving...">Send</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

  @impl true
  def handle_event("save", params, socket) do
    {:noreply, socket}
  end
end

And in the router.ex:

 scope "/", MyAppWeb do
    pipe_through :browser

    get "/", PageController, :home

    live "/contact", ContactLive # <-- add this
  end

If we go to http://localhost:4000/contact, we’ll have our contact form:

Contact form withouth changeset in Liveview

As we can see, the only difference is that the helper to_form/1 will be passed a map instead of a changeset:

# with changeset
changeset = Accounts.change_user(%User{})
to_form(changeset)

# without changeset
params = %{"name" => "", "subject" => "", "body" => "", "email" => ""}
to_form(params)

If we test the form, we can see the following in the logs:

Logs handling form params

In this way we can continue taking advantage of the power of the helpers and the new components of the CoreComponents module.