Using Ecto (without Db) for validating Phoenix form
Author: Manh Vu
Published: 2024-06-08

I had a sharing session for Elixir community in Saigon about how to using Ecto without db for validating Phoenix form. Now I add this for people who just start to learn Elixir can see what can do with Ecto & Phoenix form.

For save time I just write an example for HTML form in LiveView.

As we known Ecto is very flexible library for Elixir application and people usually use Ecto like:

Ecto common case

Actually, we can use only 2 modules are enough for casting & validating form: Ecto Changeset & Schema (you can use only Changeset module but with Schema much more convenience).

For example: I declare Schema for Candidate module:

defmodule DemoEctoForm.Candidate do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :name, :string
    field :bio, :string
  end

  def changeset(candidate, attrs \\ %{}) do
    candidate
    |> cast(attrs, [:name, :bio])
    |> validate_required([:name])
    |> update_change(:name, &String.trim/1)
    |> validate_format(:name, ~r/^[a-zA-Z\s]+$/)
    |> validate_format(:name, ~r/^\w+(?:\s+\w+){1,5}$/)
    |> validate_format(:bio, ~r/^\w+(?:\s+\w+){2,25}$/)
  end
end

A embedded schema with only 2 fields & a changeset/2 function for casting & validating values from submitted form.

For validating use can see one required field (:name) and I add some regex for checking valid fields.

And at LiveView I defined a template:

    ~H"""
    <div class="bg-red-600 text-white rounded-md">
    <br>
    <.form
      id="candidate-form"
      :let={f} for={@changeset}
      phx-change="validate"
      phx-submit="submit"
      class="flex flex-col max-w-md mx-auto mt-8"
    >
     <h1 class="text-4xl font-bold text-center">New Candidate!</h1>
     <br>
      <.input field={f[:name]} placeholder="Nguyen Van Great" id="name" label="Full Name" />
      <br>
      <.input field={f[:bio]} placeholder="example: Elixir, Phoenix, Ecto, Hòzô" id="bio" label="Bio"/>
      <br>
      <.button type="submit">Add Candidate</.button>
      <br><br>
    </.form>
    </div>
    """

I passed default changeset from mount event then use directly in form. See .form and .input

My mount event:

  def mount(_params, _session, socket) do
    changeset = Candidate.changeset(%Candidate{})

    {:ok, assign(socket, changeset: changeset)}
  end

And add phx-submit event to handle submitted form from user:


  def handle_event("submit", %{"candidate" => candidate_params}, socket) do
    changeset =
      %Candidate{}
      |> Candidate.changeset(candidate_params)

    if changeset.valid? do
      data = apply_action!(changeset, :update)
      IO.puts "Candidate data: #{inspect(data, [pretty: true, struct: false])}"
      Ets.add_candidate(data)

      socket =
        socket
        |> put_flash(:info, "You added a candidate!")
        |> redirect(to: ~p"/")

      {:noreply, socket}
    else
      changeset =
        %Candidate{}
        |> Candidate.changeset(candidate_params)

      {:noreply, assign(socket, changeset: changeset)}
    end
  end

I cast data from submitted form to changeset then check if changeset is valid or not. If changeset is invalid I pass again socket for form show errors in browser then user can update data.

Now, I have a completed form!

Actually, I have added an other tips for user can continue fill to the form in another device/browser or in case if LiveView is crashed by save state of form to ets table in my project.

Check my repo for more.