Introducción
Este minúsculo proyecto esta hecho para ir copiando manualmente, y entender así cada detalle en los comentarios. Son dos archivos, así que se me hace justo
Creación del proyecto
Para facilitar las cosas, vamos a iniciar un proyecto sin ecto, por el momento una base de datos no es realmente necesaria
mix phx.new demo --no-ecto
Ya puestos a remover del proyecto elementos que por ahora no son necesarios, dejemosle sin internacionalización o envío de correo:
mix phx.new demo --no-ecto --no-gettext --no-mailer
Y para evitar el Y que nos preguntará luego para efectivamente instalar todo, se lo pasamos de una vez:
mix phx.new demo --no-ecto --no-gettext --no-mailer --install
Solo nos falta ingresar al proyecto ingresando al directorio creado, demo e iniciar el proyecto:
cd demo
mix phx.server
Primera fase: Rutas apenas funcionales
A ver: Phoenix separa nuestro modulo Demo en dos dominios: Demo propiamente, donde debe desarrollarse toda la lógica de negocios, y DemoWeb, dónde vamos a desarrollar todas las cuestiones sobre la presentación web.
Empezamos por la lógica web: Crearemos el directorio lib/demo_web/live/: Si bien el módulo LiveView ya se encuentra, por defecto, en el proyecto, el directorio asignado a guardar sus componentes no se crea por defecto
mkdir lib/demo_web/live
En él, creamos el fichero lib/demo_web/live/dominio_live.ex con el siguiente contenido:
defmodule DemoWeb.DominioLive do
use DemoWeb, :live_view
@doc"""
Se encarga de inicializar el componente cuando se accede a él por primera vez
"""
def mount(_params, _session, socket) do
{:ok, socket}
end
@doc"""
Se encarga de cambiar el componente cada vez que los paramétros de la URL cambian
Usamos apply_action/3 para manejar efectivamente cada ruta, según el comportamiento por defecto de Phoenix
"""
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
# Configuramos los assigns que el socket mantiene según se necesite. En esta caso, solo cambiamos el title html
defp apply_action(socket, :index, _params) do
socket |> assign(:page_title, "Lista de dominios")
end
# Obtenemos 'id' desestructurando 'params' de la url, lo configuramos mediante 'assign' ne el socket
defp apply_action(socket, :detail, %{"id" => id}) do
socket |> assign(:page_title, "Dominio #{id}") |> assign(:id, id)
end
@doc"""
Otra desestructuración especifica para asignar este render a :detail
Esta forma de acceder a un valor en assings mantiene la "reactividad" del sistema
"""
def render(%{live_action: :detail} = assigns) do
~H"<h2>Detalle de dominio #{@id}</h2>"
end
def render(%{live_action: :index} = assigns) do
~H"<h2>Lista de dominios</h2>"
end
end
Ahora registramos el controlador con su ruta correspondiente en lib/demo_web/router.ex:
# ...
pipeline :api do
plug :accepts, ["json"]
end
scope "/", DemoWeb do
pipe_through :browser
get "/", PageController, :home
# url - controlador - assign = %{live_action: :index}
live "/dominios", DominioLive, :index
# url - controlador - assign = %{live_action: :detail}
live "/dominios/:id", DominioLive, :detail
end
# Other scopes may use custom stacks.
# ...
Ahora, es posible acceder a http://localhost:4000/dominios/ y a http://localhost:4000/dominios/230 para ver nuestras dos sencillas rutas
Segunda fase: Agregamos el contexto
Para entender el contexto, existen muchas lecturas adecuadas, pero yo recomiendo:
Por nuestra parte, seguiremos de modelo el código que se crearía con el siguiente comando:
mix phx.gen.live Dominios Dominio dominios correo:string correo_administrador:string
Creamos el fichero lib/demo/dominios.ex con el siguiente contenido:
defmodule Demo.Dominios do
@moduledoc """
Contexto para Dominios
"""
@doc """
Retorna la lista de dominios
"""
def listar() do
[
%{id: 1, nombre: "dominio.com", correo: "cpena@dominio.com", correo_administrador: "admin@dominio.com"},
%{id: 1, nombre: "ejemplo.com", correo: "kpena@ejemplo.com", correo_administrador: "admin@ejemplo.com"},
]
end
@doc """
Retorna el dominio si existe, `nil` en caso contrario
"""
def obtener(id) when is_integer(id) do
listar() |> Enum.find(fn dominio -> dominio.id == id end)
end
def obtener(id) when is_binary(id) do
case Integer.parse(id) do
{id, ""} -> obtener(id)
_ -> nil
end
end
@doc """
Retorna el dominio si existe, emite un error en caso contrario
"""
def obtener!(id) do
case obtener(id) do
nil -> raise "Dominio con ID #{id} no encontrado"
dominio -> dominio
end
end
end
Una vez guardado, podemos probarlo en la REPL de Elixir de la siguiente forma:

(Sé que no es el primer lenguaje con una REPL, pero si uno de los que más conveniente puede llegar a ser)
Tercera fase: Mejores templates
Vamos a unir nuestra breve lógica de negocios con nuestras sencillas rutas. Pero antes, vamos a mejorar el template (Que por ahora esta ‘inline’ en el código) y de una vez lo colocamos en su propio archivo.
Empezamos creando un directorio en nuestro dominio web (DemoWeb) para DominioLive
mkdir lib/demo_web/live/dominio_live/
Y creamos el template lib/demo_web/live/dominio_live/index.html.heex:
<div class="mx-auto max-w-4xl px-4 py-8">
<div class="mb-6">
<.link navigate={~p"/dominios"} class="text-indigo-600 hover:text-indigo-500 text-sm font-medium">
← Volver a la lista
</.link>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h1 class="text-3xl font-bold text-gray-900">
<%= @dominio |> Map.get(:nombre, "") %>
</h1>
</div>
<div class="border-t border-gray-200">
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">ID</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<%= @dominio |> Map.get(:id, "") %>
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Correo</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<%= @dominio |> Map.get(:correo, "") %>
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Correo Administrador</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<%= @dominio |> Map.get(:correo_administrador, "") %>
</dd> </div> </dl>
</div>
</div>
</div>
Y también a lib/demo_web/live/dominio_live/detail.html.heex:
<div class="mx-auto max-w-4xl px-4 py-8">
<div class="mb-6">
<.link navigate={~p"/dominios"} class="text-indigo-600 hover:text-indigo-500 text-sm font-medium">
← Volver a la lista
</.link>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h1 class="text-3xl font-bold text-gray-900">
<%= @dominio |> Map.get(:nombre, "") %>
</h1>
</div>
<div class="border-t border-gray-200">
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">ID</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<%= @dominio |> Map.get(:id, "") %>
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Correo</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<%= @dominio |> Map.get(:correo, "") %>
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Correo Administrador</dt>
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
<%= @dominio |> Map.get(:correo_administrador, "") %>
</dd> </div> </dl>
</div>
</div>
</div>
Modificamos nuestro controlador (Esa esa la función de lib/demo_web/live/dominio_live.ex) en tres partes:
- Incluimos los templates como funciones gracias a
embed_templates: (1) - Cambiamos los apply:
- Para
:index, usamosassignpara agregar un valor adominios: (2) - Para
:detail, usamosassignpara agregar un valor adominio: (3)
- Para
- Cambiamos los render:
- Para
%{live_action: :index}, usamosindex: El template enlib/demo_web/live/dominio_live/index.html.heexconvertido a función en el paso 1: (4) - Para
%{live_action: :detail}, usamosdetail: El template enlib/demo_web/live/dominio_live/detail.html.heexconvertido a función en el paso 1: (5)
- Para
defmodule DemoWeb.DominioLive do
use DemoWeb, :live_view
embed_templates "dominio_live/*" # (1)
@doc"""
Se encarga de inicializar el componente cuando accedemos a eĺ por primera vez
"""
def mount(_params, _session, socket) do
{:ok, socket}
end
@doc"""
Se encarga de cambiar el componente cada vez que los parametros de la URL cambian
Usamos apply_action/3 para manejar efectivamente cada ruta, según el comportamiento por defecto de Phoenix
"""
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
# Obtenemos 'id' desestructurando 'params' de la url, lo configuramos mediante 'assign' en el socket
defp apply_action(socket, :detail, %{"id" => id}) do
dominio = %{} # (3)
socket |> assign(:page_title, "Dominio #{id})") |> assign(:dominio, dominio)
end
# Configuramos los assigns que el socket manitne según se necesite. Es este caso, solo cambiamos el title HTML
defp apply_action(socket, :index, _params) do
dominios = [] # (2)
socket |> assign(:page_title, "Lista de dominios") |> assign(:dominios, dominios)
end
@doc"""
Otra desestructuración especifica para asignar este render a :detail
Esta forma de acceder a un valor en assings mantiene la "reactividad" del sistema
"""
def render(%{live_action: :detail} = assigns) do
detail(assigns) # (4)
end
def render(%{live_action: :index} = assigns) do
index(assigns) # (5)
end
end
Accedemos de nuevo a http://localhost:4000/archivos/ y a http://localhost:4000/archivos/230, y vemos como el maquetado ha mejorado
Cuarta fase: Unimos el dominio web con el dominio de nuesta lógica de negocios
Para esto, solo hay dos cosas que hacer:
- Agregamos un alias a
Demo.Dominios, el punto de partida para nuestra lógica de negocios. Esto es opcional (Podríamos ir escribiendo la ruta completa en donde corresponda), pero es una buena práctica: (1) - Agregamos nuestra lógica de negocios en los
apply_action:- Para
:index, usamosDominios.listar/0para asignarle valor adominios: (2) - Para
:detail, usamosDominios.obtener/1para asignarle valor adominio: (3)
- Para
defmodule DemoWeb.DominioLive do
alias Demo.Dominios # (1)
use DemoWeb, :live_view
embed_templates "dominio_live/*"
@doc"""
Se encarga de inicializar el componente cuando accedemos a eĺ por primera vez
"""
def mount(_params, _session, socket) do
{:ok, socket}
end
@doc"""
Se encarga de cambiar el componente cada vez que los parametros de la URL cambian
Usamos apply_action/3 para manejar efectivamente cada ruta, según el comportamiento por defecto de Phoenix
"""
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
# Configuramos los assigns que el socket manitne según se necesite. Es este caso, solo cambiamos el title HTML
defp apply_action(socket, :index, _params) do
dominios = Dominios.listar() # (2)
socket |> assign(:page_title, "Lista de dominios") |> assign(:dominios, dominios)
end
# Obtenemos 'id' desestructurando 'params' de la url, lo configuramos mediante 'assign' en el socket
defp apply_action(socket, :detail, %{"id" => id}) do
dominio = Dominios.obtener(id) # (3)
socket |> assign(:page_title, "Dominio #{id})") |> assign(:dominio, dominio)
end
@doc"""
Renders
"""
def render(%{live_action: :detail} = assigns) do
detail(assigns)
end
def render(%{live_action: :index} = assigns) do
index(assigns)
end
end
Posibles conclusiones
Esto es más o menos Phoenix LiveView. Es posible que sea un ejemplo demasiado básico para mostrar todas sus caracteristicas, sin embargo, lo cierto es que con lo que esta acá se puede empezar a experimentar bastante sobre el tema