Inicio / Elixir / Phoenix Framework: Web en Tiempo Real / Formularios

Formularios

Forms con changesets, validación, uploads y componentes.

Intermedio Web
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Formularios en Phoenix

Phoenix proporciona un sistema robusto para construir formularios HTML que se integran directamente con los changesets de Ecto. Esto permite validaciones en tiempo real, manejo de errores intuitivo y soporte nativo para carga de archivos.

El Componente form

En Phoenix 1.7+, los formularios se construyen usando el componente <.form> junto con function components para los campos:

# En la plantilla HEEx
<.form :let={f} for={@changeset} action={~p"/productos"}>
  <.input field={f[:nombre]} type="text" label="Nombre del producto" />
  <.input field={f[:descripcion]} type="textarea" label="Descripción" />
  <.input field={f[:precio]} type="number" label="Precio" step="0.01" />
  <.input field={f[:categoria_id]} type="select" label="Categoría"
    options={@categorias} />
  <.input field={f[:activo]} type="checkbox" label="Activo" />

  <:actions>
    <.button>Guardar Producto</.button>
  </:actions>
</.form>

La Función to_form

to_form convierte un changeset u otros datos en una estructura que el componente de formulario puede utilizar:

# En un controller tradicional
def new(conn, _params) do
  changeset = Catalogo.cambio_producto(%Producto{})
  render(conn, :new, changeset: changeset)
end

# En un LiveView
def mount(_params, _session, socket) do
  changeset = Catalogo.cambio_producto(%Producto{})
  form = to_form(changeset)
  {:ok, assign(socket, form: form)}
end

# to_form también acepta mapas simples
form = to_form(%{"nombre" => "", "email" => ""})

# O con opciones adicionales
form = to_form(changeset, as: :producto, id: "form-producto")

Changesets en Formularios

Los changesets alimentan los formularios proporcionando valores iniciales, validaciones y errores:

defmodule MiApp.Catalogo do
  alias MiApp.Catalogo.Producto
  alias MiApp.Repo

  def cambio_producto(%Producto{} = producto, attrs \\ %{}) do
    Producto.changeset(producto, attrs)
  end

  def crear_producto(attrs) do
    %Producto{}
    |> Producto.changeset(attrs)
    |> Repo.insert()
  end
end

# En el controller
def create(conn, %{"producto" => producto_params}) do
  case Catalogo.crear_producto(producto_params) do
    {:ok, producto} ->
      conn
      |> put_flash(:info, "Producto creado.")
      |> redirect(to: ~p"/productos/#{producto}")

    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, :new, changeset: changeset)
  end
end

Validaciones en Formularios

Las validaciones del changeset se reflejan automáticamente en el formulario mostrando errores:

defmodule MiApp.Catalogo.Producto do
  use Ecto.Schema
  import Ecto.Changeset

  schema "productos" do
    field :nombre, :string
    field :precio, :decimal
    field :stock, :integer
    timestamps()
  end

  def changeset(producto, attrs) do
    producto
    |> cast(attrs, [:nombre, :precio, :stock])
    |> validate_required([:nombre, :precio], message: "es obligatorio")
    |> validate_length(:nombre, min: 3, message: "debe tener al menos 3 caracteres")
    |> validate_number(:precio, greater_than: 0, message: "debe ser mayor a cero")
    |> unique_constraint(:nombre, message: "ya existe un producto con ese nombre")
  end
end

Manejo de Errores

Phoenix muestra los errores de validación automáticamente junto a cada campo del formulario:

# El componente input de CoreComponents maneja errores automáticamente
def input(assigns) do
  ~H"""
  <div phx-feedback-for={@name}>
    <label for={@id}><%= @label %></label>
    <input type={@type} name={@name} id={@id} value={@value}
      class={["input", @errors != [] && "input-error"]} />
    <.error :for={msg <- @errors}><%= msg %></.error>
  </div>
  """
end

def error(assigns) do
  ~H"""
  <p class="error-mensaje">
    <%= render_slot(@inner_block) %>
  </p>
  """
end

# Uso en la plantilla: los errores aparecen automáticamente
<.form :let={f} for={@changeset} action={~p"/productos"}>
  <.input field={f[:nombre]} type="text" label="Nombre" />
  <!-- Si hay error, aparece: "es obligatorio" debajo del campo -->
</.form>

Carga de Archivos con allow_upload

Phoenix LiveView ofrece soporte nativo para subir archivos con allow_upload:

defmodule MiAppWeb.ProductoLive.FormComponent do
  use MiAppWeb, :live_component

  def mount(socket) do
    {:ok,
     socket
     |> allow_upload(:imagen,
       accept: ~w(.jpg .jpeg .png .webp),
       max_entries: 3,
       max_file_size: 5_000_000
     )}
  end

  def render(assigns) do
    ~H"""
    <.form for={@form} phx-change="validate" phx-submit="save" phx-target={@myself}>
      <.input field={@form[:nombre]} type="text" label="Nombre" />
      <.live_file_input upload={@uploads.imagen} />

      <%= for entry <- @uploads.imagen.entries do %>
        <div class="preview">
          <.live_img_preview entry={entry} width="100" />
          <button phx-click="cancel-upload" phx-value-ref={entry.ref}
            phx-target={@myself}>&times;</button>
        </div>
      <% end %>

      <.button>Subir</.button>
    </.form>
    """
  end
end

Live Uploads

Los live uploads procesan los archivos en el servidor cuando se envía el formulario:

def handle_event("save", %{"producto" => params}, socket) do
  imagenes =
    consume_uploaded_entries(socket, :imagen, fn %{path: path}, entry ->
      dest = Path.join(["priv", "static", "uploads", entry.client_name])
      File.cp!(path, dest)
      {:ok, ~p"/uploads/#{entry.client_name}"}
    end)

  params = Map.put(params, "imagenes", imagenes)

  case Catalogo.crear_producto(params) do
    {:ok, producto} ->
      {:noreply,
       socket
       |> put_flash(:info, "Producto creado con imágenes.")
       |> push_navigate(to: ~p"/productos/#{producto}")}

    {:error, changeset} ->
      {:noreply, assign(socket, form: to_form(changeset))}
  end
end

def handle_event("cancel-upload", %{"ref" => ref}, socket) do
  {:noreply, cancel_upload(socket, :imagen, ref)}
end

Resumen

Phoenix integra formularios con changesets de Ecto de forma fluida. El componente <.form> con to_form genera formularios tipados, los changesets proveen validaciones que se reflejan automáticamente como errores en la interfaz. Para carga de archivos, allow_upload de LiveView ofrece una solución nativa con previsualizaciones, validación de tipo/tamaño y procesamiento en el servidor mediante consume_uploaded_entries.

🔒

Ejercicio práctico disponible

Formularios con Changesets

Desbloquear ejercicios
// Formularios con Changesets
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes