# Rendering forms with Phoenix

`Zoi` works seamlessly with Phoenix forms through the `Phoenix.HTML.FormData` protocol. This guide walks through building a complete LiveView form step by step.

## 1. Define Your Schema

First, define your validation schema inline using `Zoi.Form.prepare/1`:

```elixir
defmodule MyAppWeb.UserLive.FormComponent do
  use MyAppWeb, :live_view

  @user_schema Zoi.map(%{
    name: Zoi.string() |> Zoi.min(3),
    email: Zoi.email(),
    age: Zoi.integer() |> Zoi.min(18) |> Zoi.optional()
  }) |> Zoi.Form.prepare()
end
```

`Zoi.Form.prepare/1` enables automatic coercion so form strings convert to the right types (integers, booleans, etc.).

## 2. Parse and Render

Parse params with `Zoi.Form.parse/2` and convert the context to a Phoenix form:

```elixir
def mount(_params, _session, socket) do
  params = %{}  # Start with empty form
  ctx = Zoi.Form.parse(@user_schema, params)

  {:ok, assign(socket, to_form(ctx, as: :user)}
end

def render(assigns) do
  ~H"""
  <.form for={@form} phx-change="validate" phx-submit="save">
    <.input field={@form[:name]} label="Name" />
    <.input field={@form[:email]} label="Email" />
    <.input field={@form[:age]} type="number" label="Age" />

    <div>
      <.button>Save</.button>
    </div>
  </.form>
  """
end
```

That's it! Phoenix's `<.input>` component automatically displays validation errors.

## 3. Handle Validation

Parse params on every change to show live validation:

```elixir
def handle_event("validate", %{"user" => params}, socket) do
  ctx = Zoi.Form.parse(@user_schema, params)

  {:noreply, assign(socket, form: to_form(ctx, as: :user))}
end
```

## 4. Handle Submit

Check `ctx.valid?` and use `ctx.parsed` for validated data:

```elixir
def handle_event("save", %{"user" => params}, socket) do
  ctx = Zoi.Form.parse(@user_schema, params)

  if ctx.valid? do
    # ctx.parsed is validated and type-coerced
    # Example: %{name: "John", email: "john@example.com", age: 30}
    case Accounts.create_user(ctx.parsed) do
      {:ok, user} ->
        {:noreply, push_navigate(socket, to: ~p"/users/#{user}")}

      {:error, _reason} ->
        # Handle submission errors - see "Adding External Errors" section below
        {:noreply, put_flash(socket, :error, "Failed to save")}
    end
  else
    # Show all errors immediately on submit
    form = to_form(ctx, as: :user, action: :validate)
    {:noreply, assign(socket, form: form)}
  end
end
```

## 5. Working with Nested data structures

Add nested addresses to your schema:

```elixir
@user_schema Zoi.map(%{
  name: Zoi.string() |> Zoi.min(3),
  email: Zoi.email(),
  addresses: Zoi.array(
    Zoi.map(%{
      street: Zoi.string() |> Zoi.min(5),
      city: Zoi.string(),
      zip: Zoi.string() |> Zoi.length(5)
    })
  )
}) |> Zoi.Form.prepare()
```

Render with `<.inputs_for>`:

```elixir
<.form for={@form} phx-change="validate" phx-submit="save">
  <.input field={@form[:name]} label="Name" />
  <.input field={@form[:email]} label="Email" />

  <.inputs_for :let={address} field={@form[:addresses]}>
    <.input field={address[:street]} label="Street" />
    <.input field={address[:city]} label="City" />
    <.input field={address[:zip]} label="ZIP" />

    <.button type="button" phx-click="remove_address" phx-value-index={address.index}>
      Remove
    </.button>
  </.inputs_for>

  <.button type="button" phx-click="add_address">Add Address</.button>

  <div>
    <.button>Save</.button>
  </div>
</.form>
```

## 6. Dynamic Add/Remove

To add or remove array items, work with `form.params` directly (arrays are always lists):

```elixir
def handle_event("add_address", _params, socket) do
  # Get current addresses from form.params (always a list)
  addresses = socket.assigns.form.params["addresses"] || []

  # Add a new empty address
  updated_params = Map.put(socket.assigns.form.params, "addresses", addresses ++ [%{}])

  # Re-parse and update form
  ctx = Zoi.Form.parse(@user_schema, updated_params)

  {:noreply, assign(socket, form: to_form(ctx, as: :user))}
end

def handle_event("remove_address", %{"index" => index}, socket) do
  addresses = socket.assigns.form.params["addresses"] || []
  idx = String.to_integer(index)

  # Remove the address at the index
  updated_params = Map.put(socket.assigns.form.params, "addresses", List.delete_at(addresses, idx))

  # Re-parse and update form
  ctx = Zoi.Form.parse(@user_schema, updated_params)

  {:noreply, assign(socket, form: to_form(ctx, as: :user))}
end
```

**Note:** `Zoi.Form.parse/2` automatically converts LiveView's map-based array format to lists, so `form.params` always contains clean data you can manipulate with standard list operations.

## 7. Handle Create and Edit

Use `handle_params` to handle both `:new` and `:edit` actions:

```elixir
def handle_params(params, _url, socket) do
  {:noreply, apply_action(socket, socket.assigns.live_action, params)}
end

defp apply_action(socket, :new, _params) do
  # Start with one empty address
  params = %{"addresses" => [%{}]}
  ctx = Zoi.Form.parse(@user_schema, params)

  socket
  |> assign(:page_title, "New User")
  |> assign(:user, nil)
  |> assign(:form, to_form(ctx, as: :user))
end

defp apply_action(socket, :edit, %{"id" => id}) do
  user = Accounts.get_user!(id)

  # Convert database record to form params (all strings)
  # You may also use Zoi to help with this conversion if needed
  # But in general, good to map manually for clarity
  params = %{
    "name" => user.name,
    "email" => user.email,
    "age" => user.age,
    "addresses" => Enum.map(user.addresses, fn addr ->
      %{
        "street" => addr.street,
        "city" => addr.city,
        "zip" => addr.zip
      }
    end)
  }

  ctx = Zoi.Form.parse(@user_schema, params)

  socket
  |> assign(:page_title, "Edit User")
  |> assign(:user, user)
  |> assign(:form, to_form(ctx, as: :user))
end
```

Update save to dispatch based on action:

```elixir
def handle_event("save", %{"user" => params}, socket) do
  ctx = Zoi.Form.parse(@user_schema, params)

  if ctx.valid? do
    save_user(socket, socket.assigns.live_action, ctx.parsed)
  else
    {:noreply, assign(socket, form: to_form(ctx, as: :user, action: :validate))}
  end
end

defp save_user(socket, :new, attrs) do
  case Accounts.create_user(attrs) do
    {:ok, user} ->
      {:noreply,
       socket
       |> put_flash(:info, "User created")
       |> push_navigate(to: ~p"/users/#{user}")}

    {:error, _changeset} ->
      {:noreply, put_flash(socket, :error, "Failed to create")}
  end
end

defp save_user(socket, :edit, attrs) do
  case Accounts.update_user(socket.assigns.user, attrs) do
    {:ok, user} ->
      {:noreply,
       socket
       |> put_flash(:info, "User updated")
       |> push_navigate(to: ~p"/users/#{user}")}

    {:error, _changeset} ->
      {:noreply, put_flash(socket, :error, "Failed to update")}
  end
end
```

## 8. Adding External Errors to Forms

When your backend returns errors (e.g., from Ecto changesets or business logic), you can add them to the Zoi context so they display on the form.

Since you already have the context from `Zoi.Form.parse/2`, use `Zoi.Context.add_error/2` to add errors before converting to a form:

```elixir
def handle_event("save", %{"user" => params}, socket) do
  ctx = Zoi.Form.parse(@user_schema, params)

  if ctx.valid? do
    case Accounts.create_user(ctx.parsed) do
      {:ok, user} ->
        {:noreply, push_navigate(socket, to: ~p"/users/#{user}")}

      {:error, :email_taken} ->
        error = Zoi.Error.custom_error(issue: {"has already been taken", []}, path: [:email])
        ctx = Zoi.Context.add_error(ctx, error)
        {:noreply, assign(socket, form: to_form(ctx, as: :user, action: :validate))}

      {:error, _reason} ->
        {:noreply, put_flash(socket, :error, "Failed to save")}
    end
  else
    {:noreply, assign(socket, form: to_form(ctx, as: :user, action: :validate))}
  end
end
```

### Adding Changeset Errors

Ecto changeset errors use the `{msg, opts}` tuple format. Convert them to Zoi errors preserving the format for translation support (see [Localizing errors with Gettext](localizing_errors_with_gettext.md)):

```elixir
defp save_user(socket, :new, attrs, ctx) do
  case Accounts.create_user(attrs) do
    {:ok, user} ->
      {:noreply,
       socket
       |> put_flash(:info, "User created")
       |> push_navigate(to: ~p"/users/#{user}")}

    {:error, changeset} ->
      ctx = add_changeset_errors(changeset, ctx)
      {:noreply, assign(socket, form: to_form(ctx, as: :user, action: :validate))}
  end
end

defp add_changeset_errors(changeset, ctx) do
  Enum.reduce(changeset.errors, ctx, fn {field, {msg, opts}}, acc ->
    error = Zoi.Error.custom_error(issue: {msg, opts}, path: [field])
    Zoi.Context.add_error(acc, error)
  end)
end
```

The Ecto schema might not always represent the form structure you created with Zoi, so depending on your use case, you need to map fields accordingly.
