Understanding Forms and Changesets: Difference between revisions

From ElixirBlocks
Jump to: navigation, search
No edit summary
No edit summary
 
(19 intermediate revisions by the same user not shown)
Line 5: Line 5:
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.  
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.  


The setup for the tutorial requires the creation of a database table named '''Item''' and its Ecto "Context" data.  We walk through the entire process.


'''Glossary'''
* Changeset:
* Phoenix Template:


=Getting Started=
=Getting Started=
Line 18: Line 13:
[[How to Create an Empty Phoenix Application|How to Create an Empty Phoenix Application]]
[[How to Create an Empty Phoenix Application|How to Create an Empty Phoenix Application]]


When complete, run this command to create database tables and Ecto context code.
 
 
==Creating an HTML Form and Post Request==
 
In the [[router]] create these two routes:


<source>
<source>
  scope "/", AppWeb do
    pipe_through :browser


    get "/", PageController, :index      # First route
    post "/create", PageController, :new  # Second route


mix phx.gen.context Items Item items name:string


  end
</source>
</source>


Run the migrate command
 
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'''


<source>
<source>
mix ecto.migrate
 
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
 
</source>
</source>


==Seed Data==
 
In the file named app/priv/repo/seeds.ex type the following code to create "dummy data" for this exercise.
In the controllers directory create a file named '''page_html.ex'''
 
In this file, copy this code.


<source>
<source>
App.Items.create_item(%{name: "item-1"})
defmodule AppWeb.PageHTML do
App.Items.create_item(%{name: "item-2"})
  use AppWeb, :html
App.Items.create_item(%{name: "item-3"})


  embed_templates "page_html/*"
end


</source>
</source>


In the terminal type:


mix run priv/repo/seeds.exs
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'''


==Routes==
This directory will contain heex templates for the PageController.


In routes, add the the following routes.
In the page_html directory place a file named '''index.html.heex'''.
 
Inside the file copy the following code.


<source>
<source>
get "/items", ItemController, :index
 
<%=@data%>
 
 
 
<form method="POST" action="/create">
    <input type="hidden" name="_csrf_token" value={@csrf_token} />
    <input type="text" name="example" />
    <input type="submit">
  </form>
 
 
</source>
</source>




==Controller==
Run the server:  mix phx.server


In app/lib/app_web/controllers create a file controller named:
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.


item_controller.ex


Copy the following code into it.
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.
 


<source>
<source>
defmodule AppWeb.ItemController do
 
  <.form :let={f} action={~p"/create"}>
    <.input field={f[:x]} name="submitted-data" />
  </.form>
</source>
 
Delete or comment out the code in page_controller.ex and replace it with the code below.
<source>
defmodule AppWeb.PageController do
   use AppWeb, :controller
   use AppWeb, :controller


   def index(conn, _params) do
   def index(conn, params) do
     # The home page is often custom made,
     IO.inspect params
    # so skip the default app layout.
     render(conn, :index, data: "Hello World",form: %{})
     render(conn, :index, layout: false)
   end
   end


  def new(conn, params) do
      IO.inspect params
      redirect(conn, to: "/")
  end
end
end


</source>
When you run the server and visit the landing page, you should see the page render without error.


</source>


===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.




In the same directory create a file named:


'''item.html.ex'''
==Schema and Live View Form==


Open the file and type the following code:
===Schema===


<source>
<source>
defmodule AppWeb.ItemHTML do
defmodule App.Todo do
   use AppWeb, :html
   use Ecto.Schema
  import Ecto.Changeset
 
  schema "todos" do
    field :description, :string
    field :title, :string
 
    timestamps(type: :utc_datetime)
  end


   embed_templates "item_html/*"
   @doc false
  def changeset(todo, attrs) do
    todo
    |> cast(attrs, [:title, :description])
    |> validate_required([:title, :description])
  end
end
end


</source>
</source>


Create a folder named index_html in the same directory like this:
===Live View Form ===


app/lib/app_web/controllers/'''index_html'''
<source>
defmodule AppWeb.PageLive do
  use AppWeb, :live_view
  alias App.Todo


In '''index_html''' create a file named '''index.html.heex''' and copy type the following code into it.
  def mount(_params, _session, socket) do
    changeset = Todo.changeset(%Todo{}, %{})
   
    {:ok,
    socket
    |> assign(:changeset, changeset)
    |> assign(:todo, %Todo{})
    |> assign(:saved, false)}
  end


<source>
  def handle_event("save", %{"todo" => todo_params}, socket) do
<div> Items go here</div>
  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
</source>
</source>


Start the app and make sure everything works.


'''mix phx.server'''


In your browser go to:
https://adopt-liveview.lubien.dev/guides/simple-forms-with-ecto/en
<source>
 
http://localhost:4000/items
</source>
You will see the phrase "Items go here" in the upper left corner of the screen.


==Capture Parameter Data of URL Link==
When a user clicks a hyper-link you should know how to capture the url parameters via controller. To do so follow these steps:


Open the router file  '''router.ex'''.
Create a new route that looks like this:


'''get "/items/:item", ItemController, :index'''
===A Working Form Without Changesets ===


<source>
<source>
defmodule AppWeb.SandLive do
  use AppWeb, :live_view


   scope "/", AppWeb do
   alias App.Todos
     get "/items/:data", ItemController, :index  #:data is a variable
  alias App.Todos.Todo
    get "/items", ItemController, :index
 
    get "/", PageController, :home
  def mount(params,session,socket) do
   
     {:ok, assign(socket, data: "some data")}
   end
   end
</source>


The :item is a variable and represents a value that is unknown. When you type a URL like
  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>


localhost:4000/item/some-data
          {@data}


The value "some-data" is assigned to the variable  :data as a string and the variable is captured in the controller. Once in the controller, the data can be manipulated.
        </div>


Open the '''item_controller.ex'''
      """


Place '''IO.inspect(_params)''' in the Item index controller body like this:
  end


<source>
 
   def index(conn, _params) do
   def handle_event("save",params, socket) do  
    IO.inspect _params
      IO.inspect params
     render(conn, :index, layout: false)
 
     {:noreply, socket}
   end
   end


end
</source>
</source>


Open the Item template in '''app_web/controllers/item_html/index.html.heex'''


Write a hyper link like this:
=== Same Example Without Changeset and With Flash Message ===


<source>
<source>
<a href ="items/some-data-goes-here>Click me</a>
defmodule AppWeb.SandLive do
</source>
  use AppWeb, :live_view
 
  alias App.Todos
  alias App.Todos.Todo


Start the server, go to localhost:4000/items/some-data-goes-here
  def mount(params,session,socket) do 
    {:ok, assign(socket, data: "some data")}
  end


Open the terminal. While it is open click the link in the website that says '''Click me'''.
  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>


In the terminal the the following text is presented:
      """
  end


<source>
  def handle_event("save", %{"name" => name} = params, socket) do
%{"data" => "some-data-goes-here"}
    IO.inspect params
</source>
   
    # 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


The key is the name of the variable you placed in the route (data) and the string is the data assigned to it in the hyper link (some-data-goes-here)
  end




== Submitting Data Through an HTML Form to a Controller==
end
You are now going to write code that lets you submit data through a form and inspect that data from within a controller. 


</source>


Elixir Phoenix has it's own syntax for writing forms. This syntax is named Embedded Elixir or Heex for short. It lets you write Elixir code in your HTML template files to work with data from the server, database and user interface.
===The Same Example with a Changeset ===


An example of a form with Heex used looks like this:


<source>
<source>


<.form for={@form} phx-change="validate" phx-submit="save">
  <.input type="text" field={@form[:username]} />
  <.input type="email" field={@form[:email]} />
  <button>Save</button>
</.form>


</source>
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


Prior to using Heex, you will create a working HTML form ''without'' Heex, and gradually integrate Heex into it.  
      case Todos.create_todo(todo_params) do
        {:ok, _todo} ->
          {:noreply, socket
          |> put_flash(:info, "User created successfully.")
          |> redirect(to: "/")}


To do this you need to create:
        {:error, %Ecto.Changeset{} = changeset} ->
          {:noreply, assign(socket, changeset: changeset)}
      end
    end
  end


* A POST route
* A Controller for the POST
* An Html form


===HTML Form in Template ===
</source>


Go to your item index template: '''app_web/controllers/item_html/index.html.heex'''
==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>


Create basic HTML form.
===Changeset with Modern to_form (follow this exmple)


<source>
<source>
<form method="POST" action="/submit">
 
  <input type="text" name="example" />
defmodule AppWeb.SandLive do
  <input type="submit">
    use AppWeb, :live_view
</form>
 
    alias App.Todos
    alias App.Todos.Todo
 
    def mount(_params, _session, socket) do
      changeset = Todos.change_todo(%Todo{})
      {:ok, assign(socket, form: to_form(changeset), data: "some data")}
    end
 
    def render(assigns) do
      ~H"""
      <div>
        <.form for={@form} phx-change="validate" phx-submit="save">
          <.input field={@form[:name]} type="text" label="Name" />
          <.button>SAVE</.button>
        </.form>
        {@data}
      </div>
      """
    end
   
   
    def handle_event("validate", %{"todo" => todo_params}, socket) do
      changeset =
        %Todo{}
        |> Todos.change_todo(todo_params)
        |> Map.put(:action, :validate)
       
      {:noreply, assign(socket, form: to_form(changeset))}
    end
 
    def handle_event("save", %{"todo" => todo_params}, socket) do
      case Todos.create_todo(todo_params) do
        {:ok, _todo} ->
          {:noreply,
          socket
          |> put_flash(:info, "Todo created successfully.")
          |> redirect(to: "/")}
 
        {:error, %Ecto.Changeset{} = changeset} ->
          {:noreply, assign(socket, form: to_form(changeset))}
      end
    end
  end


</source>
</source>




===Route===


In your router, create the following route. The endpoint must be "submit".


<source>
 
post "/submit", ItemController, :index
______________________________________________________
</source>




===Controller===
= Phoenix LiveView Forms & Changeset Tutorial =


Here's a concise tutorial to help you learn and recall the syntax for Phoenix LiveView forms with Ecto changesets.


<source>
== The Changeset Flow ==


  def submit(conn, _params) do
Think of the changeset as the "form manager" that follows this pattern:
    # The home page is often custom made,
    # so skip the default app layout.
    IO.inspect _params
    redirect(conn, to: "/items")
  end
 
</source>


Go to localhost:4000/items
# '''Initialize''' - Create an empty changeset for the form
# '''Validate''' - Update changeset when user types (phx-change)
# '''Submit''' - Process final changeset when user submits (phx-submit)


You will see a form. Click the submit button.
== Key Components ==


An error will appear:
=== 1. Initial Setup in Mount ===


<source>
<syntaxhighlight lang="elixir">
invalid CSRF (Cross Site Request Forgery) token, please make sure that:
def mount(_params, _session, socket) do
  changeset = Todos.change_todo(%Todo{})
  {:ok, assign(socket, form: to_form(changeset), data: "some data")}
end
</syntaxhighlight>


  * The session cookie is being sent and session is loaded
'''Memory Trick''': "Mount needs a blank changeset to start the form"
  * The request include a valid '_csrf_token' param or 'x-csrf-token' header
</source>


To fix the error add the following line to your form right above the index element.
=== 2. Rendering the Form ===


<source>
<syntaxhighlight lang="elixir">
<%# <input type="hidden" name="_csrf_token" value={@csrf_token} /> %>
<.form for={@form} phx-change="validate" phx-submit="save">
</source>
  <.input field={@form[:name]} type="text" label="Name" />
  <.button>SAVE</.button>
</.form>
</syntaxhighlight>


It will look like this:
'''Memory Trick''': "phx-change validates as you type, phx-submit saves when done"
<source>


<form method="POST" action="/submit">
=== 3. Validation Handler ===
  <input type="hidden" name="_csrf_token" value={@csrf_token} />
  <input type="text" name="example" />
  <input type="submit">
</form>


</source>
<syntaxhighlight lang="elixir">
def handle_event("validate", %{"todo" => todo_params}, socket) do
  changeset =
    %Todo{}
    |> Todos.change_todo(todo_params)
    |> Map.put(:action, :validate)
   
  {:noreply, assign(socket, form: to_form(changeset))}
end
</syntaxhighlight>


The controller now needs to be configured to render the CSRF token. Change your Item controller code to reflect the following example:  
'''Memory Tricks''':
* "Validate always starts with an empty struct (%Todo{})"
* "Map.put(:action, :validate) shows errors but doesn't save"


<source>
=== 4. Save Handler ===
  def index(conn, _params) do
    IO.inspect _params


<syntaxhighlight lang="elixir">
def handle_event("save", %{"todo" => todo_params}, socket) do
  case Todos.create_todo(todo_params) do
    {:ok, _todo} ->
      {:noreply,
      socket
      |> put_flash(:info, "Todo created successfully.")
      |> redirect(to: "/")}


     csrf_token = Plug.CSRFProtection.get_csrf_token()
     {:error, %Ecto.Changeset{} = changeset} ->
    render(conn, :index, [csrf_token: csrf_token])
      {:noreply, assign(socket, form: to_form(changeset))}
   end
   end
end
</syntaxhighlight>


'''Memory Trick''': "Save has two paths: success (redirect) or error (show form again)"


</source>
== Changeset Flow Mnemonic: "CIVC" ==


# '''C'''reate changeset in mount
# '''I'''nput through the form
# '''V'''alidate with phx-change
# '''C'''omplete with phx-submit


Start the server and go to localhost:4000/items
== Additional Tips ==


Submit data to the form.
* '''Pattern Matching''': Notice how form data comes in as <code>%{"todo" => todo_params}</code> - it's automatically namespaced by your schema.


In the terminal, the submitted data and CSRF content is viewable.
* '''The to_form Function''': Always convert changesets to forms with <code>to_form(changeset)</code> before passing to the template.


<source>
* '''Empty vs. Populated Structs''':
** Use empty <code>%Todo{}</code> for validation
** Use populated struct for editing existing records


%{
* '''Error Handling''': On save failure, you just re-assign the form with the error changeset, and Phoenix shows the errors.
  "_csrf_token" => "CAgbPih1d3QuUwIpW1t1byQvEAIUQz8beoJkRF4BxcNv38LYLHr6ZnLo",
  "example" => "Test"
}


</source>
This pattern is consistent across nearly all LiveView forms in Phoenix - master this flow and you'll be able to handle virtually any form scenario.




The key "example" is the name of the form, and has your form submission data.


==Example Using <.form>==
=== Same Code with Simple_Form ===
Example using the <pre> <.form> module with validate method


<source>
<source>
defmodule AppWeb.PageLive do
defmodule AppWeb.SandLive do
     use AppWeb, :live_view
     use AppWeb, :live_view
     def mount(_params, _session, socket) do
 
        {:ok, assign(socket, form: %{})}  
    alias App.Todos
    alias App.Todos.Todo
 
     def mount(_params, _session, socket) do
      changeset = Todos.change_todo(%Todo{})
      {:ok, assign(socket, form: to_form(changeset))}
     end
     end
 
    def render(assigns) do
      ~H"""
      <div>
        <.simple_form for={@form} phx-change="validate" phx-submit="save">
          <.input field={@form[:name]} type="text" label="Name" />
          <:actions>
            <.button>SAVE</.button>
          </:actions>
        </.simple_form>


     def handle_event("validate", data, socket) do
      </div>
      IO.inspect data
      """
         {:noreply, socket}
    end
   
     def handle_event("validate", %{"todo" => todo_params}, socket) do
      changeset =
        %Todo{}
        |> Todos.change_todo(todo_params)
        |> Map.put(:action, :validate)
          
      {:noreply, assign(socket, form: to_form(changeset))}
    end
 
    def handle_event("save", %{"todo" => todo_params}, socket) do
      case Todos.create_todo(todo_params) do
        {:ok, _todo} ->
          {:noreply,
          socket
          |> put_flash(:info, "Todo created successfully.")
          |> redirect(to: "/")}
        {:error, %Ecto.Changeset{} = changeset} ->
          {:noreply, assign(socket, form: to_form(changeset))}
       end
       end
     
    def render(assigns) do
        ~H"""
        <.form for={@form} phx-change="validate" phx-submit="save">
        <.input type="text" field={@form[:username]} name="first" value="xx"/>
        <.input type="email" field={@form[:email]} name="second" value="xx"/>
        <button>Save</button>
        </.form>
        """
     end
     end
end
  end
</source>
</source>
{{In_progress}}

Latest revision as of 21:42, 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

===Changeset with Modern to_form (follow this exmple)


defmodule AppWeb.SandLive 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, form: to_form(changeset), data: "some data")}
    end
  
    def render(assigns) do
      ~H"""
      <div>
        <.form for={@form} phx-change="validate" phx-submit="save">
          <.input field={@form[:name]} type="text" label="Name" />
          <.button>SAVE</.button>
        </.form>
        {@data}
      </div>
      """
    end
    
    
    def handle_event("validate", %{"todo" => todo_params}, socket) do
      changeset =
        %Todo{}
        |> Todos.change_todo(todo_params)
        |> Map.put(:action, :validate)
        
      {:noreply, assign(socket, form: to_form(changeset))}
    end
  
    def handle_event("save", %{"todo" => todo_params}, socket) do
      case Todos.create_todo(todo_params) do
        {:ok, _todo} ->
          {:noreply,
           socket
           |> put_flash(:info, "Todo created successfully.")
           |> redirect(to: "/")}
  
        {:error, %Ecto.Changeset{} = changeset} ->
          {:noreply, assign(socket, form: to_form(changeset))}
      end
    end
  end



______________________________________________________


Phoenix LiveView Forms & Changeset Tutorial

Here's a concise tutorial to help you learn and recall the syntax for Phoenix LiveView forms with Ecto changesets.

The Changeset Flow

Think of the changeset as the "form manager" that follows this pattern:

  1. Initialize - Create an empty changeset for the form
  2. Validate - Update changeset when user types (phx-change)
  3. Submit - Process final changeset when user submits (phx-submit)

Key Components

1. Initial Setup in Mount

def mount(_params, _session, socket) do
  changeset = Todos.change_todo(%Todo{})
  {:ok, assign(socket, form: to_form(changeset), data: "some data")}
end

Memory Trick: "Mount needs a blank changeset to start the form"

2. Rendering the Form

<.form for={@form} phx-change="validate" phx-submit="save">
  <.input field={@form[:name]} type="text" label="Name" />
  <.button>SAVE</.button>
</.form>

Memory Trick: "phx-change validates as you type, phx-submit saves when done"

3. Validation Handler

def handle_event("validate", %{"todo" => todo_params}, socket) do
  changeset =
    %Todo{}
    |> Todos.change_todo(todo_params)
    |> Map.put(:action, :validate)
    
  {:noreply, assign(socket, form: to_form(changeset))}
end

Memory Tricks:

  • "Validate always starts with an empty struct (%Todo{})"
  • "Map.put(:action, :validate) shows errors but doesn't save"

4. Save Handler

def handle_event("save", %{"todo" => todo_params}, socket) do
  case Todos.create_todo(todo_params) do
    {:ok, _todo} ->
      {:noreply,
       socket
       |> put_flash(:info, "Todo created successfully.")
       |> redirect(to: "/")}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, form: to_form(changeset))}
  end
end

Memory Trick: "Save has two paths: success (redirect) or error (show form again)"

Changeset Flow Mnemonic: "CIVC"

  1. Create changeset in mount
  2. Input through the form
  3. Validate with phx-change
  4. Complete with phx-submit

Additional Tips

  • Pattern Matching: Notice how form data comes in as %{"todo" => todo_params} - it's automatically namespaced by your schema.
  • The to_form Function: Always convert changesets to forms with to_form(changeset) before passing to the template.
  • Empty vs. Populated Structs:
    • Use empty %Todo{} for validation
    • Use populated struct for editing existing records
  • Error Handling: On save failure, you just re-assign the form with the error changeset, and Phoenix shows the errors.

This pattern is consistent across nearly all LiveView forms in Phoenix - master this flow and you'll be able to handle virtually any form scenario.


Same Code with Simple_Form

defmodule AppWeb.SandLive 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, form: to_form(changeset))}
    end
  
    def render(assigns) do
      ~H"""
      <div>
        <.simple_form for={@form} phx-change="validate" phx-submit="save">
          <.input field={@form[:name]} type="text" label="Name" />
          <:actions>
            <.button>SAVE</.button>
          </:actions>
        </.simple_form>

      </div>
      """
    end
    
    def handle_event("validate", %{"todo" => todo_params}, socket) do
      changeset =
        %Todo{}
        |> Todos.change_todo(todo_params)
        |> Map.put(:action, :validate)
        
      {:noreply, assign(socket, form: to_form(changeset))}
    end
  
    def handle_event("save", %{"todo" => todo_params}, socket) do
      case Todos.create_todo(todo_params) do
        {:ok, _todo} ->
          {:noreply,
           socket
           |> put_flash(:info, "Todo created successfully.")
           |> redirect(to: "/")}
        {:error, %Ecto.Changeset{} = changeset} ->
          {:noreply, assign(socket, form: to_form(changeset))}
      end
    end
  end