LiveView: Stoic API
En este tutorial se realizará un formulario para ingresar más citas de filósofos estóicos para nuestra API creada en el tutorial anterior.
Paso 1: Agregar nuestra ruta de LiveView
Vamos al router y agregamos nuestra nueva página en lib/stoic_quotes_web/router.ex
.
Debemos modificar el scope
principal donde se muestra la página del navegador
y cambiar el PageController por un nuevo módulo que crearemos después.
Usamos la siguiente función live("/", Live.QuotesForm, :live)
.
scope "/", StoicQuotesWeb do
pipe_through(:browser)
get("/", PageController, :home)
end
A diferencia de las páginas normales que utilizan los mismos verbos HTTP como get
y post
.
LiveView solo utiliza la función live
para indicar que esto es una página de LiveView.
scope "/", StoicQuotesWeb do
pipe_through(:browser)
live("/", Live.QuotesForm, :live)
end
Paso 2: Crear nuestro controlador de LiveView
Creamos un nuevo directorio dentro de stoic_quotes_web
que se llame live
.
Este será el directorio que usaremos para almacenar todas nuestras páginas de LiveView.
Lo llamaremos live/quotes_form.ex
defmodule StoicQuotesWeb.Live.QuotesForm do
use StoicQuotesWeb, :live_view
def mount(_params, _session, socket) do
{:ok, socket}
end
end
Se recomienda una división adicional, con un nombre del directorio como la página
y el controlador y la vista llamados como |
live/
quotes_form/
page.ex
page.html.heex
La línea use StoicQuotesWeb, :live_view
nos indica que usaremos el macro asociado a un LiveView,
esto esta definido en el archivo lib/stoic_quotes_web/stoic_quotes_web.ex
.
def live_view do
quote do
use Phoenix.LiveView
unquote(html_helpers())
end
end
La línea def mount(params, session, socket)
nos indica que esta función
será la ejecutada al momento de montar
la página. Se nos dan tres argumentos,
el primero son los parámetros del request (definidos en la ruta dentro del router), el segundo son los elementos dentro de la sesión (cookies)
y finalmente la estructura socket
, la cual es usada a lo largo de todo el ciclo
de vida del LiveView para almacenar distintos datos que pueden ser compartidos
de padres a hijos.
La línea {:ok, socket}
es obligatoria para que el ciclo de vida pueda continuar,
estamos diciendo a Phoenix que todo ha resultado correctamente y puede seguir con
el ciclo de vida. Podemos modificar el socket en esta función para almacenar datos.
Luego crearemos nuestro archivo HTML que tendrá nuestra vista y formulario,
lo llamaremos de la misma forma que el archivo del controlador live, pero
utilizando el sufijo *.html.heex
, de esta forma: quotes_form.html.heex
.
Por el momento solo mostraremos un mensaje para validar que se haya configurado correctamente.
<p>Formulario Estóico</p>
Si ejecutamos el proyecto deberíamos ver el HTML en la dirección http://localhost:4000
.
$ mix phx.server

Si analizamos el HTML generado en la página, descubriremos que Phoenix ha añadido código adicional. ¿Dónde están definidos estos HTML?.
<!-- <StoicQuotesWeb.Layouts.root> lib/stoic_quotes_web/components/layouts/root.html.heex:1 (stoic_quotes) --><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="HDofMiYGdwwcPRwgPiB8JDNsKiRRNAAbwHTdSeZnpI_BdMQBU-mg8_8Y">
<!-- @caller lib/stoic_quotes_web/components/layouts/root.html.heex:7 (stoic_quotes) --><!-- <Phoenix.Component.live_title> lib/phoenix_component.ex:2195 (phoenix_live_view) --><title data-default="StoicQuotes" data-suffix=" · Phoenix Framework">StoicQuotes · Phoenix Framework</title><!-- </Phoenix.Component.live_title> -->
<link phx-track-static rel="stylesheet" href="/assets/css/app.css">
<script defer phx-track-static type="text/javascript" src="/assets/js/app.js">
</script>
<script>
(() => {
const setTheme = (theme) => {
if (theme === "system") {
localStorage.removeItem("phx:theme");
document.documentElement.removeAttribute("data-theme");
} else {
localStorage.setItem("phx:theme", theme);
document.documentElement.setAttribute("data-theme", theme);
}
};
if (!document.documentElement.hasAttribute("data-theme")) {
setTheme(localStorage.getItem("phx:theme") || "system");
}
window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
window.addEventListener("phx:set-theme", ({ detail: { theme } }) => setTheme(theme));
})();
</script>
</head>
<body>
<div id="phx-GGl-vRc7pJr_fwWB" data-phx-main data-phx-session="SFMyNTY.g2gDaAJhBnQAAAAIdwJpZG0AAAAUcGh4LUdHbC12UmM3cEpyX2Z3V0J3B3Nlc3Npb250AAAAAHcGcm91dGVydxxFbGl4aXIuU3RvaWNRdW90ZXNXZWIuUm91dGVydwR2aWV3dyVFbGl4aXIuU3RvaWNRdW90ZXNXZWIuTGl2ZS5RdW90ZXNGb3JtdwpwYXJlbnRfcGlkdwNuaWx3CXJvb3Rfdmlld3clRWxpeGlyLlN0b2ljUXVvdGVzV2ViLkxpdmUuUXVvdGVzRm9ybXcRbGl2ZV9zZXNzaW9uX25hbWV3B2RlZmF1bHR3CHJvb3RfcGlkdwNuaWxuBgBE9CCRmQFiAAFRgA.ezDcn_NTue6_ZWfuSQeErOe4BX6hYh6GJ-P3XpwnoN8" data-phx-static="SFMyNTY.g2gDaAJhBnQAAAADdwJpZG0AAAAUcGh4LUdHbC12UmM3cEpyX2Z3V0J3BWZsYXNodAAAAAB3CmFzc2lnbl9uZXdqbgYARfQgkZkBYgABUYA.VDBvnUZgQNL8phJlkpF3O80LDbxKVcezEXZCpjF1SYQ"><!-- <StoicQuotesWeb.Live.QuotesForm.render> lib/stoic_quotes_web/live/quotes_form.html.heex:1 (stoic_quotes) --><p>Formulario Estóico</p><!-- </StoicQuotesWeb.Live.QuotesForm.render> --></div>
<iframe hidden height="0" width="0" src="/phoenix/live_reload/frame"></iframe></body>
</html><!-- </StoicQuotesWeb.Layouts.root> -->
Estos HTML adicionales están definidos en el directorio layouts/
. Son heredados desde el archivo root.html.heex
el cual se encuentra en el directorio lib/stoic_quotes_web/components/layouts/
.
Los contenidos de este archivo son transversales para todas las vistas.
Si se desea utilizar otro archivo se debe modificar el router.
plug(:put_root_layout, html: {StoicQuotesWeb.Layouts, :root})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.live_title default="StoicQuotes" suffix=" · Phoenix Framework">
{assigns[:page_title]}
</.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}>
</script>
<script>
(() => {
const setTheme = (theme) => {
if (theme === "system") {
localStorage.removeItem("phx:theme");
document.documentElement.removeAttribute("data-theme");
} else {
localStorage.setItem("phx:theme", theme);
document.documentElement.setAttribute("data-theme", theme);
}
};
if (!document.documentElement.hasAttribute("data-theme")) {
setTheme(localStorage.getItem("phx:theme") || "system");
}
window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
window.addEventListener("phx:set-theme", ({ detail: { theme } }) => setTheme(theme));
})();
</script>
</head>
<body>
{@inner_content}
</body>
</html>
-
{assigns[:page_title]}
: Imprime el contenido que puede ser modificado usando la estructurasockets
en la funciónmount
usandosocket = assign(socket, page_title: 'Mi Título')
. -
{@inner_content}
: Imprime un texto que puede ser reemplazado por una vista de un LiveView.
Paso 3: Implementar el formulario HTML
Editamos nuestro formulario (lib/stoic_quotes_web/quotes_form.html.heex
) con el HTML necesario.
<div class="min-h-full">
<header class="relative bg-gray-800 after:pointer-events-none after:absolute after:inset-x-0 after:inset-y-0 after:border-y after:border-white/10">
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold tracking-tight text-white">Stoic Quotes Form</h1>
</div>
</header>
<main>
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<form>
<div class="space-y-12">
<%# Alert Section %>
<div role="alert" class="alert">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>12 unread messages. Tap to see.</span>
</div>
<div class="border-b border-white/10 pb-12">
<h2 class="text-base/7 font-semibold text-white">Stoic Quote Information</h2>
<p class="mt-1 text-sm/6 text-gray-400">Use this form to add a new Stoic Quote</p>
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-3">
<label for="author" class="block text-sm/6 font-medium text-white">
Author
</label>
<div class="mt-2">
<input id="author" type="text" name="author" autofocus="true" class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6" />
</div>
</div>
<div class="sm:col-span-3">
<label for="source" class="block text-sm/6 font-medium text-white">
Source
</label>
<div class="mt-2">
<input id="source" type="text" name="source" class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6" />
</div>
</div>
<div class="col-span-full">
<label
for="quote"
class="block text-sm/6 font-medium text-white">
Quote
</label>
<div class="mt-2">
<textarea
id="quote"
type="text"
rows="5"
name="quote"
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"></textarea>
</div>
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-x-6">
<button type="reset" class="btn text-sm/6 font-semibold text-white">
Reset
</button>
<button type="submit" class="rounded-md btn btn-xl btn-wide bg-indigo-500 px-3 py-2 text-sm font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500">
Save
</button>
</div>
</form>
</div>
</main>
</div>
Lo que mostrará una página similar a lo siguiente:

Paso 4: Conectar el formulario al controlador
Para esto utilizaremos las herramientas proporcionadas por LiveView la cual permite enviar eventos y valores hacia el controlador.
Configuramos el valor de cada input para que sea enviado al controlador.
Para esto creamos una nueva estructura que almacenará los valores,
utilizamos una función llamada empty_form()
que utiliza la función de Phoenix to_form()
para entregar la estructura que usaremos en el formulario.
defp empty_form() do
to_form(%{"author" => "", "quote" => "", "source" => ""})
end
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(form: empty_form())}
end
También añadiremos dos eventos "validate" y "save" que por el momento solamente devuelven los valores del formulario. Luego serán mejorados.
def handle_event("validate", params, socket) do
IO.inspect(params, label: :validate)
form = to_form(params)
{:noreply,
socket
|> assign(form: form)
}
end
def handle_event("save", params, socket) do
IO.inspect(params, label: :save)
form = to_form(params)
{:noreply,
socket
|> assign(form: form)
}
end
Quedando el archivo de la siguiente forma:
defmodule StoicQuotesWeb.Live.QuotesForm do
use StoicQuotesWeb, :live_view
defp empty_form() do
to_form(%{"author" => "", "quote" => "", "source" => ""})
end
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(form: empty_form())}
end
def handle_event("validate", params, socket) do
IO.inspect(params, label: :validate)
form = to_form(params)
{:noreply,
socket
|> assign(form: form)
}
end
def handle_event("save", params, socket) do
IO.inspect(params, label: :save)
form = to_form(params)
{:noreply,
socket
|> assign(form: form)
}
end
end
También es necesario utilizar el elemento .form
para asociar el formulario al controlador.
Notar los eventos que se manejaran, phx-change
y phx-submit
.
<.form for={@form} phx-change="validate" phx-submit="save">
...
</.form>
Ahora es turno de asociar los elementos para que sean enviados en los eventos del formulario.
Para esto utilizamos los elementos .input
.
Author
<.input
autofocus="true"
placeholder="Marcus Aurelius"
phx-debounce="blur"
field={@form[:author]}
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
/>
Source
<.input
placeholder="Meditations"
phx-debounce="blur"
field={@form[:source]}
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
/>
Quote
<.input
type="textarea"
rows="5"
placeholder="Lorem Ipsum"
phx-debounce="blur"
field={@form[:quote]}
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
/>
¿Dónde están estos elementos de <.form>
e <.input>
?
Estos elementos están definidos en lib/stoic_quotes_web/components/core_components.ex
donde corresponden a una función que devuelve un html según los parámetros.
Estos son componentes que vienen predefinidos en Phoenix y son opcionales de utilizar,
pero recomendados.
...
def input(%{type: "textarea"} = assigns) do
~H"""
<fieldset class="fieldset mb-2">
<label>
<span :if={@label} class="label mb-1">{@label}</span>
<textarea
id={@id}
name={@name}
class={[
@class || "w-full textarea",
@errors != [] && (@error_class || "textarea-error")
]}
{@rest}
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
</fieldset>
"""
end
...
Alertas
Para generar alertas utilizaremos el componente <Layouts.flash_group>
el cual está dentro del archivo lib/stoic_quotes_web/components/layouts.ex
.
Este es un mensaje de alerta que cambia de color dependiendo del tipo de alerta (éxito o error).
Se utiliza la función put_flash()
en el socket
para enviar
mensajes.
<Layouts.flash_group flash={@flash} />
Quedando el formulario como lo siguiente:
<div class="min-h-full">
<header class="relative bg-gray-800 after:pointer-events-none after:absolute after:inset-x-0 after:inset-y-0 after:border-y after:border-white/10">
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold tracking-tight text-white">Stoic Quotes Form</h1>
</div>
</header>
<main>
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<.form for={@form} phx-change="validate" phx-submit="save">
<div class="space-y-12">
<Layouts.flash_group flash={@flash} />
<div class="border-b border-white/10 pb-12">
<h2 class="text-base/7 font-semibold text-white">Stoic Quote Information</h2>
<p class="mt-1 text-sm/6 text-gray-400">Use this form to add a new Stoic Quote</p>
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-3">
<label for="author" class="block text-sm/6 font-medium text-white">
Author
</label>
<div class="mt-2">
<.input
autofocus="true"
required="true"
placeholder="Marcus Aurelius"
phx-debounce="blur"
field={@form[:author]}
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
/>
</div>
</div>
<div class="sm:col-span-3">
<label for="source" class="block text-sm/6 font-medium text-white">
Source
</label>
<div class="mt-2">
<.input
placeholder="Meditations"
required="true"
phx-debounce="blur"
field={@form[:source]}
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
/>
</div>
</div>
<div class="col-span-full">
<label
for="quote"
class="block text-sm/6 font-medium text-white">
Quote
</label>
<div class="mt-2">
<.input
type="textarea"
required="true"
rows="5"
placeholder="Lorem Ipsum"
phx-debounce="blur"
field={@form[:quote]}
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
/>
</div>
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-x-6">
<button type="reset" class="btn text-sm/6 font-semibold text-white">
Reset
</button>
<.button type="submit" class="rounded-md btn btn-xl btn-wide bg-indigo-500 px-3 py-2 text-sm font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500">
Save
</.button>
</div>
</.form>
</div>
</main>
</div>
Paso 5: Implementar validación del formulario
Ahora se realizará la validación del formulario, para que muestre errores
si se envía un valor que no sea correcto. Para esto modificaremos la función
def handle_event("validate", params, socket)
, donde crearemos un nuevo changeset
,
el cual será la estructura usada para realizar todas las validaciones. Como ya tenemos
un esquema podemos reutilizarlo, sin embargo también existen los changeset
sin esquemas
(por ejemplo un formulario de contacto) que pemiten realizar validaciones a formularios
no asociados a una base de datos o también cuando sea necesario validar múltiples valores
no relacionados en la misma tabla.
Primero añadimos el módulo y el Logger.
defmodule StoicQuotesWeb.Live.QuotesForm do
use StoicQuotesWeb, :live_view
alias StoicQuotes.Quotes
alias StoicQuotes.Quotes.Quote
require Logger
...
Luego modificamos la función para usar el módulo. Notemos que añadimos
una nueva función llamada Quote.new
que inicia una validación con los parámetros
que le hemos dado. Para esto debemos añadir la función al esquema correspondiente.
def handle_event("validate", params, socket) do
changeset =
Quote.new(params)
form = to_form(params, errors: changeset.errors)
Logger.debug(form)
{:noreply,
socket
|> assign(form: form)}
end
Notar que separamos el changeset del formulario. Esto es por que un formulario puede tener distintos campos que no necesariamente tienen relación con el esquema y sus validaciones. Por lo que siempre es recomendable tener entidades separadas para mayor mantenibilidad y bajo acoplamiento. |
Ahora modicamos el esquema para que tenga la función new
.
@doc false
def new(attrs \\ %{"author" => "", "quote" => "", "source" => ""}) do
case changeset(%__MODULE__{}, attrs) do
{_, changeset} -> changeset
changeset -> changeset
end
end
defmodule StoicQuotes.Quotes.Quote do
use Ecto.Schema
import Ecto.Changeset
@optional_fields [:id, :inserted_at, :updated_at]
schema "quotes" do
field(:quote, :string)
field(:author, :string)
field(:source, :string)
timestamps(type: :utc_datetime)
end
def fields() do
__MODULE__.__schema__(:fields)
end
def required_fields() do
fields() -- @optional_fields
end
@doc false
def changeset(quote, attrs) do
quote
|> cast(attrs, fields())
|> validate_required(required_fields())
|> unsafe_validate_unique(:quote, StoicQuotes.Repo)
|> unique_constraint(:quote)
end
@doc false
def new(attrs \\ %{"author" => "", "quote" => "", "source" => ""}) do
case changeset(%__MODULE__{}, attrs) do
{_, changeset} -> changeset
changeset -> changeset
end
end
end
-
MODULE
: Este elemento permite utilizar el módulo dentro del mismo. Siempre apuntara al nombre del módulo, por lo que es buena práctica usarlo para reducir el acomplamiento. -
changeset(%MODULE{}, attrs)
: Llamamos a la función existente pasando los parámetros adecuados, como un nuevo struct del módulo. -
{_, changeset} → changeset
: La validaciónunique_constraint(:quote)
entrega una tupla{:error, changeset}
, por lo cual debemos estandarizar para simplificar el manejo de errores. -
changeset → changeset
: Si la validación entrega el formato estándar entonces la devolvemos tal cual es.
Paso 6: Implementar el guardado en la base de datos
Si las validaciones son exitosas, entonces podemos enviarlo para su almacenamiento
en la base de datos. Para esto modificamos la función def handle_event("save", params, socket)
.
Debemos evaluar los casos: las validaciones son correcta o no, se guardo exitosamente o no,
como también considerar un caso excepcional donde no se retornó el valor esperado al guardar (éxito o fracaso).
def handle_event("save", params, socket) do
changeset =
Quote.new(params)
form = to_form(params, errors: changeset.errors)
Logger.debug(form)
socket =
case changeset.valid? do
true ->
case Quotes.create_quote(params) do
{:ok, result} ->
Logger.debug("Insert completed")
Logger.debug(result)
socket
|> assign(form: empty_form())
|> put_flash(:info, "Created new Quote")
{:error, error} ->
Logger.debug("Insert failed")
Logger.debug(error)
socket
|> assign(form: form)
|> put_flash(:error, "There was an error saving the Quote")
unknown ->
Logger.debug("Insert operation with unknown state")
Logger.debug(unknown)
socket
|> assign(form: form)
|> put_flash(:error, "There was an error saving the Quote")
end
false ->
Logger.debug("Changeset with errors can not be saved")
Logger.debug(changeset.errors)
socket
|> assign(form: form)
|> put_flash(:error, "There was an error saving the Quote")
end
{:noreply, socket}
end
Paso 7: Botón Guardar
Ahora que tenemos las validaciones listas se modificará un poco el botón guardar para que solo esté activo si el formulario tiene valores válidos.
Para esto le añadimos la propiedad disabled
que estará en verdadero si no se puede guardar.
disabled={@can_save? == false}
.
<button
type="submit"
class="rounded-md btn btn-xl btn-wide bg-indigo-500 px-3 py-2 text-sm font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
disabled={@can_save? == false}
>
Save
</button>
Ahora debemos añadir esta nueva variable en nuestro socket
y función de validación.
Para esto creamos la variable con su valor inicial en mount
.
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(can_save?: false)
|> assign(form: empty_form())}
end
Y modificamos tanto la función de validación, como la función de guardado. En la función de validación debemos obtener el valor del changeset de validaciones para determinar si el botón puede ser habilitado.
def handle_event("validate", params, socket) do
changeset =
Quote.new(params)
form = to_form(params, errors: changeset.errors)
Logger.debug(form)
{:noreply,
socket
|> assign(can_save?: changeset.valid?)
|> assign(form: form)}
end
def handle_event("save", params, socket) do
...
case Quotes.create_quote(params) do
{:ok, result} ->
Logger.debug("Insert completed")
Logger.debug(result)
socket
|> assign(can_save?: false)
|> assign(form: empty_form())
|> put_flash(:info, "Created new Quote")
...
Se mostrará el código final de cada archivo.
defmodule StoicQuotesWeb.Live.QuotesForm do
use StoicQuotesWeb, :live_view
alias StoicQuotes.Quotes.Quote
alias StoicQuotes.Quotes
require Logger
defp empty_form() do
to_form(%{"author" => "", "quote" => "", "source" => ""})
end
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(can_save?: false)
|> assign(form: empty_form())}
end
def handle_event("validate", params, socket) do
changeset =
Quote.new(params)
form = to_form(params, errors: changeset.errors)
Logger.debug(form)
{:noreply,
socket
|> assign(can_save?: changeset.valid?)
|> assign(form: form)}
end
def handle_event("save", params, socket) do
changeset =
Quote.new(params)
form = to_form(params, errors: changeset.errors)
Logger.debug(form)
socket =
case changeset.valid? do
true ->
case Quotes.create_quote(params) do
{:ok, result} ->
Logger.debug("Insert completed")
Logger.debug(result)
socket
|> assign(can_save?: false)
|> assign(form: empty_form())
|> put_flash(:info, "Created new Quote")
{:error, error} ->
Logger.debug("Insert failed")
Logger.debug(error)
socket
|> assign(form: form)
|> put_flash(:error, "There was an error saving the Quote")
unknown ->
Logger.debug("Insert operation with unknown state")
Logger.debug(unknown)
socket
|> assign(form: form)
|> put_flash(:error, "There was an error saving the Quote")
end
false ->
Logger.debug("Changeset with errors can not be saved")
Logger.debug(changeset.errors)
socket
|> assign(form: form)
|> put_flash(:error, "There was an error saving the Quote")
end
{:noreply, socket}
end
end
<div class="min-h-full">
<header class="relative bg-gray-800 after:pointer-events-none after:absolute after:inset-x-0 after:inset-y-0 after:border-y after:border-white/10">
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold tracking-tight text-white">Stoic Quotes Form</h1>
</div>
</header>
<main>
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<.form for={@form} phx-change="validate" phx-submit="save">
<div class="space-y-12">
<Layouts.flash_group flash={@flash} />
<div class="border-b border-white/10 pb-12">
<h2 class="text-base/7 font-semibold text-white">Stoic Quote Information</h2>
<p class="mt-1 text-sm/6 text-gray-400">Use this form to add a new Stoic Quote</p>
<div class="mt-10 grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-3">
<label for="author" class="block text-sm/6 font-medium text-white">
Author
</label>
<div class="mt-2">
<.input
autofocus="true"
required="true"
placeholder="Marcus Aurelius"
phx-debounce="blur"
field={@form[:author]}
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
/>
</div>
</div>
<div class="sm:col-span-3">
<label for="source" class="block text-sm/6 font-medium text-white">
Source
</label>
<div class="mt-2">
<.input
placeholder="Meditations"
required="true"
phx-debounce="blur"
field={@form[:source]}
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
/>
</div>
</div>
<div class="col-span-full">
<label
for="quote"
class="block text-sm/6 font-medium text-white">
Quote
</label>
<div class="mt-2">
<.input
type="textarea"
required="true"
rows="5"
placeholder="Lorem Ipsum"
phx-debounce="blur"
field={@form[:quote]}
class="block w-full rounded-md bg-white/5 px-3 py-1.5 text-base text-white outline-1 -outline-offset-1 outline-white/10 placeholder:text-gray-500 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-500 sm:text-sm/6"
/>
</div>
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-x-6">
<button type="reset" class="btn text-sm/6 font-semibold text-white">
Reset
</button>
<button
type="submit"
class="rounded-md btn btn-xl btn-wide bg-indigo-500 px-3 py-2 text-sm font-semibold text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500"
disabled={@can_save? == false}
>
Save
</button>
</div>
</.form>
</div>
</main>
</div>