Understanding Forms and Changesets: Difference between revisions
No edit summary |
No edit summary |
||
Line 380: | Line 380: | ||
</source> | |||
==Troubleshooting ChangeSets Example == | |||
<source> | |||
defmodule AppWeb.SandLive do | |||
use AppWeb, :live_view | |||
alias App.Todos | |||
alias App.Todos.Todo | |||
def mount(_params, _session, socket) do | |||
# Create an empty changeset for the form | |||
changeset = Todos.change_todo(%Todo{}) | |||
# Initialize debug_info to an empty map | |||
{:ok, assign(socket, data: "some data", changeset: changeset, debug_info: %{})} | |||
end | |||
def render(assigns) do | |||
~H""" | |||
<div> | |||
<.form :let={f} for={@changeset} phx-submit="save"> | |||
<.input field={f[:name]} type="text" label="Name" /> | |||
<.button>SAVE</.button> | |||
</.form> | |||
<p>{@data}</p> | |||
<%= if map_size(@debug_info) > 0 do %> | |||
<div class="mt-4"> | |||
<h3 class="text-lg font-bold">Debug Info:</h3> | |||
<pre class="bg-gray-100 p-2 text-sm overflow-auto"><%= inspect(@debug_info, pretty: true) %></pre> | |||
</div> | |||
<% end %> | |||
</div> | |||
""" | |||
end | |||
def handle_event("save", %{"todo" => todo_params} = params, socket) do | |||
# Log the raw params first | |||
IO.inspect(params, label: "Raw form parameters") | |||
# Create a changeset without saving (for inspection) | |||
inspection_changeset = Todo.changeset(%Todo{}, todo_params) | |||
IO.inspect(inspection_changeset, label: "Changeset before save") | |||
# Note the cast fields | |||
IO.inspect(inspection_changeset.changes, label: "Fields that will change") | |||
# Note any errors that would occur | |||
IO.inspect(inspection_changeset.errors, label: "Validation errors") | |||
# Now proceed with the actual save | |||
result = Todos.create_todo(todo_params) | |||
case result do | |||
{:ok, todo} -> | |||
# Log the created record | |||
IO.inspect(todo, label: "Created todo") | |||
{:noreply, | |||
socket | |||
|> assign(debug_info: %{ | |||
raw_params: params, | |||
changeset: inspection_changeset, | |||
changes: inspection_changeset.changes, | |||
errors: inspection_changeset.errors, | |||
created_record: todo | |||
}) | |||
|> put_flash(:info, "User created successfully.") | |||
|> redirect(to: "/sandbox")} | |||
{:error, changeset} -> | |||
# Log the error changeset | |||
IO.inspect(changeset, label: "Error changeset") | |||
{:noreply, | |||
socket | |||
|> assign(changeset: changeset, debug_info: %{ | |||
raw_params: params, | |||
error_changeset: changeset, | |||
errors: changeset.errors | |||
}) | |||
|> put_flash(:error, "Failed to create user.")} | |||
end | |||
end | |||
end | |||
</source> | </source> |
Revision as of 03:34, 23 March 2025
This page is in progress
This tutorial assumes you have a basic understanding of Elixir and that you have explored Phoenix. It also assumes that you understand HTML forms.
The methodology of this tutorial includes converting a conventional HTML form into a Phoenix template that uses Elixir to communicate with back end code. You also learn how changesets integrate with controllers.
Getting Started
To begin, create a new empty Phoenix app named app. If you do not know how to do that follow this tutorial:
How to Create an Empty Phoenix Application
Creating an HTML Form and Post Request
In the router create these two routes:
scope "/", AppWeb do pipe_through :browser get "/", PageController, :index # First route post "/create", PageController, :new # Second route end
Place the code below in a file named page_controller.ex. Place the file in the controller directory.
app/lib/app_web/controllers/page_controller.ex
defmodule AppWeb.PageController do use AppWeb, :controller def index(conn, params) do IO.inspect params csrf_token = Plug.CSRFProtection.get_csrf_token() render(conn, :index, data: "Hello World",form: %{},csrf_token: csrf_token) end def new(conn, params) do IO.inspect params redirect(conn, to: "/") end end
In the controllers directory create a file named page_html.ex
In this file, copy this code.
defmodule AppWeb.PageHTML do use AppWeb, :html embed_templates "page_html/*" end
In the controllers directory create another directory named page_html (technically you can place this directory outside the controllers and it will still work).
app/lib/app_web/controllers/page_html
This directory will contain heex templates for the PageController.
In the page_html directory place a file named index.html.heex.
Inside the file copy the following code.
<%=@data%> <form method="POST" action="/create"> <input type="hidden" name="_csrf_token" value={@csrf_token} /> <input type="text" name="example" /> <input type="submit"> </form>
Run the server: mix phx.server
You should see a form field and a submit button with the text "Hello World" above them. Type into the form and submit it, the terminal will display your input.
If you replace the form with the built in form helper, you can remove the csrf code. CSRF protection comes built in with the form helper. Delete or comment out the code in index.html.heex and replace it with the code below.
<.form :let={f} action={~p"/create"}> <.input field={f[:x]} name="submitted-data" /> </.form>
Delete or comment out the code in page_controller.ex and replace it with the code below.
defmodule AppWeb.PageController do use AppWeb, :controller def index(conn, params) do IO.inspect params render(conn, :index, data: "Hello World",form: %{}) end def new(conn, params) do IO.inspect params redirect(conn, to: "/") end end
When you run the server and visit the landing page, you should see the page render without error.
Form Changesets
Without Changesets we have these two problems:
- We lose what we typed.
- The app doesn’t tell us what our errors were.
Changesets solve both of these problems.
Schema and Live View Form
Schema
defmodule App.Todo do use Ecto.Schema import Ecto.Changeset schema "todos" do field :description, :string field :title, :string timestamps(type: :utc_datetime) end @doc false def changeset(todo, attrs) do todo |> cast(attrs, [:title, :description]) |> validate_required([:title, :description]) end end
Live View Form
defmodule AppWeb.PageLive do use AppWeb, :live_view alias App.Todo def mount(_params, _session, socket) do changeset = Todo.changeset(%Todo{}, %{}) {:ok, socket |> assign(:changeset, changeset) |> assign(:todo, %Todo{}) |> assign(:saved, false)} end def handle_event("save", %{"todo" => todo_params}, socket) do case save_todo(todo_params) do {:ok, _todo} -> {:noreply, socket |> assign(:changeset, Todo.changeset(%Todo{}, %{})) |> assign(:saved, true)} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, changeset: changeset, saved: false)} end end defp save_todo(todo_params) do %Todo{} |> Todo.changeset(todo_params) |> App.Repo.insert() end def render(assigns) do ~H""" <.form :let={f} for={@changeset} phx-submit="save"> <div> <.input field={f[:title]} type="text" label="Title" /> </div> <div> <.input field={f[:description]} type="textarea" label="Description" /> </div> <div> <.button>Save</.button> </div> </.form> <%= if @saved do %> <div class="alert alert-info"> Todo saved successfully! </div> <% end %> """ end end
https://adopt-liveview.lubien.dev/guides/simple-forms-with-ecto/en
A Working Form Without Changesets
defmodule AppWeb.SandLive do use AppWeb, :live_view alias App.Todos alias App.Todos.Todo def mount(params,session,socket) do {:ok, assign(socket, data: "some data")} end def render(assigns) do ~H""" <div> <.form :let={f} phx-submit="save"> <.input field={f[:name]} type="text" label="Name" value="" /> <.button>SAVE </.button> </.form> {@data} </div> """ end def handle_event("save",params, socket) do IO.inspect params {:noreply, socket} end end
Same Example Without Changeset and With Flash Message
defmodule AppWeb.SandLive do use AppWeb, :live_view alias App.Todos alias App.Todos.Todo def mount(params,session,socket) do {:ok, assign(socket, data: "some data")} end def render(assigns) do ~H""" <div> <.form :let={f} phx-submit="save"> <.input field={f[:name]} type="text" label="Name" value="" /> <.button>SAVE </.button> </.form> {@data} </div> """ end def handle_event("save", %{"name" => name} = params, socket) do IO.inspect params # Create todo without using a changeset directly result = Todos.create_todo(%{"name" => name}) case result do {:ok, _todo} -> {:noreply, socket |> put_flash(:info, "User created successfully.") |> redirect(to: "/")} {:error, _reason} -> {:noreply, socket |> put_flash(:error, "Failed to create user.")} end end end
The Same Example with a Changeset
defmodule AppWeb.PageLive do use AppWeb, :live_view alias App.Todos alias App.Todos.Todo def mount(_params, _session, socket) do changeset = Todos.change_todo(%Todo{}) {:ok, assign(socket, changeset: changeset)} end def render(assigns) do ~H""" <h1>Create User</h1> <.form :let={f} for={@changeset} phx-submit="save"> <div> <.input field={f[:name]} type="text" label="Name" /> </div> <div> <.button>Save</.button> </div> </.form> """ end def handle_event("save", %{"todo" => todo_params}, socket) do result = Todos.create_todo(todo_params) IO.inspect result case Todos.create_todo(todo_params) do {:ok, _todo} -> {:noreply, socket |> put_flash(:info, "User created successfully.") |> redirect(to: "/")} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, changeset: changeset)} end end end
Troubleshooting ChangeSets Example
defmodule AppWeb.SandLive do use AppWeb, :live_view alias App.Todos alias App.Todos.Todo def mount(_params, _session, socket) do # Create an empty changeset for the form changeset = Todos.change_todo(%Todo{}) # Initialize debug_info to an empty map {:ok, assign(socket, data: "some data", changeset: changeset, debug_info: %{})} end def render(assigns) do ~H""" <div> <.form :let={f} for={@changeset} phx-submit="save"> <.input field={f[:name]} type="text" label="Name" /> <.button>SAVE</.button> </.form> <p>{@data}</p> <%= if map_size(@debug_info) > 0 do %> <div class="mt-4"> <h3 class="text-lg font-bold">Debug Info:</h3> <pre class="bg-gray-100 p-2 text-sm overflow-auto"><%= inspect(@debug_info, pretty: true) %></pre> </div> <% end %> </div> """ end def handle_event("save", %{"todo" => todo_params} = params, socket) do # Log the raw params first IO.inspect(params, label: "Raw form parameters") # Create a changeset without saving (for inspection) inspection_changeset = Todo.changeset(%Todo{}, todo_params) IO.inspect(inspection_changeset, label: "Changeset before save") # Note the cast fields IO.inspect(inspection_changeset.changes, label: "Fields that will change") # Note any errors that would occur IO.inspect(inspection_changeset.errors, label: "Validation errors") # Now proceed with the actual save result = Todos.create_todo(todo_params) case result do {:ok, todo} -> # Log the created record IO.inspect(todo, label: "Created todo") {:noreply, socket |> assign(debug_info: %{ raw_params: params, changeset: inspection_changeset, changes: inspection_changeset.changes, errors: inspection_changeset.errors, created_record: todo }) |> put_flash(:info, "User created successfully.") |> redirect(to: "/sandbox")} {:error, changeset} -> # Log the error changeset IO.inspect(changeset, label: "Error changeset") {:noreply, socket |> assign(changeset: changeset, debug_info: %{ raw_params: params, error_changeset: changeset, errors: changeset.errors }) |> put_flash(:error, "Failed to create user.")} end end end