LiveView 
Realmente queríamos un ejemplo de mundo real gratuito y de código abierto con código completo , pruebas y autenticación.
Escribimos esto para poder señalar a las personas en nuestro equipo/comunidad aprendiendo Phoenix LiveView .
Este ejemplo/tutorial LiveView lo lleva de cero a una aplicación que funcione completamente en 20 minutos .
Aquí está la tabla de contenido de lo que puede esperar cubrir en este ejemplo/tutorial:
LiveViewPhoenixlive , controlador y plantilla LiveViewrouter.exmount/3mount/3handle_event/3handle_info/2AUTH_API_KEYauth_plugrouter.exAuthControlleron_mount/4 Cualquiera que aprenda Phoenix LiveView desee un tutorial autónomo que incluya: Setup , Testing , Authentication , Presence ,
Se recomienda , aunque no sea necesario , que siga el tutorial de contador LiveView , ya que este es más avanzado. ¡Al menos, consulte la lista de requisitos previos para que sepa lo que necesita haber instalado en su computadora antes de comenzar esta aventura!
Siempre que tenga instalado Elixir , Phoenix y Postgres , ¡listo!
Phoenix Comience creando la nueva aplicación liveview_chat Phoenix :
mix phx.new liveview_chat --no-mailer --no-dashboard No necesitamos características email o dashboard , por lo que las excluyamos de nuestra aplicación. Puede obtener más información sobre cómo crear nuevas aplicaciones de Phoenix ejecutando: mix help phx.new
Ejecute mix deps.get para recuperar las dependencias. Luego cree la base de datos liveview_chat_dev Postgres ejecutando el comando:
mix ecto.setupDebería ver la salida similar a la siguiente:
The database for LiveviewChat.Repo has been created
14:20:19.71 [info] Migrations already upUna vez que ese comando tenga éxito, ahora debería poder iniciar la aplicación ejecutando el comando:
mix phx.serverVerá la salida terminal similar a la siguiente:
[info] Running LiveviewChatWeb.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)
[debug] Downloading esbuild from https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.29.tgz
[info] Access LiveviewChatWeb.Endpoint at http://localhost:4000
[watch] build finished, watching for changes... Cuando abre la URL: http://localhost:4000 en su navegador web, debería ver algo similar a:

live , controlador y plantilla LiveView Cree la carpeta lib/liveview_chat_web/live y el controlador en lib/liveview_chat_web/live/message_live.ex :
defmodule LiveviewChatWeb.MessageLive do
use LiveviewChatWeb , :live_view
def mount ( _params , _session , socket ) do
{ :ok , socket }
end
def render ( assigns ) do
LiveviewChatWeb.MessageView . render ( "messages.html" , assigns )
end
endNota : Ni el nombre del archivo ni el código tienen la palabra " controlador " en ninguna parte. Ojalá no sea confuso. Es un "controlador" en el sentido de que controla lo que sucede en la aplicación.
Un controlador LiveView requiere que las funciones mount/3 y render/1 para definir.
Para mantener el controlador simple, el mount/3 simplemente devuelve la tupla {:ok, socket} sin ningún cambio. El render/1 invoca LiveviewChatWeb.MessageView.render/2 (incluido con Phoenix ) que hace la template messages.html.heex que definiremos a continuación.
Cree el archivo lib/liveview_chat_web/views/message_view.ex :
defmodule LiveviewChatWeb.MessageView do
use LiveviewChatWeb , :view
end Esto es similar a view regular Phoenix ; Nada especial/interesante aquí.
A continuación, cree el directorio lib/liveview_chat_web/templates/message , luego cree
lib/liveview_chat_web/templates/message/messages.html.heex archivo y agregue la siguiente línea de HTML :
< h1 > LiveView Message Page </ h1 > Finalmente, para simplificar el diseño de la raíz , abra el archivo lib/liveview_chat_web/templates/layout/root.html.heex y actualice el contenido del <body> a:
< body >
< header >
< section class =" container " >
< h1 > LiveView Chat Example </ h1 >
</ section >
</ header >
< %= @inner_content % >
</ body > router.ex Ahora que ha creado los archivos necesarios, abra el enrutador lib/liveview_chat_web/router.ex reemplace el controlador PageController de ruta predeterminado:
get "/" , PageController , :index con controlador MessageLive :
scope "/" , LiveviewChatWeb do
pipe_through :browser
live "/" , MessageLive
endAhora, si actualiza la página, debería ver lo siguiente:

En este punto, hemos realizado algunos cambios que significan que nuestra suite de prueba automatizada ya no pasará ... Ejecute las pruebas en su línea de comando con el siguiente comando:
mix testVerá la salida similar a la siguiente:
Generated liveview_chat app
..
1) test GET / (LiveviewChatWeb.PageControllerTest)
test/liveview_chat_web/controllers/page_controller_test.exs:4
Assertion with = ~ failed
code: assert html_response(conn, 200) = ~ " Welcome to Phoenix! "
left: " <!DOCTYPE html><html lang= " en " > <head> <meta charset= " utf-8 " > <meta http-equiv= " X-UA-Compatible " content= " IE=edge " >
<title data-suffix= " · Phoenix Framework " >LiveviewChat · Phoenix Framework</title> <link phx-track-static rel= " stylesheet " href= " /assets/app.css " > <script defer phx-track-static type= " text/javascript " src= " /assets/app.js " ></script> </head>
<body> <header> <section class= " container " >
<h1>LiveView Chat Example</h1></section> </header>
<h1>LiveView Message Page</h1></main></div> </body></html> "
right: " Welcome to Phoenix! "
stacktrace:
test/liveview_chat_web/controllers/page_controller_test.exs:6: (test)
Finished in 0.03 seconds (0.02s async, 0.01s sync)
3 tests, 1 failure Esto se debe a que el page_controller_test.exs todavía espera que la página de inicio contenga el "Welcome to Phoenix!" texto.
¡Actualicemos las pruebas! Cree la carpeta test/liveview_chat_web/live y el archivo message_live_test.exs dentro de él: test/liveview_chat_web/live/message_live_test.exs
Agregue el siguiente código de prueba:
defmodule LiveviewChatWeb.MessageLiveTest do
use LiveviewChatWeb.ConnCase
import Phoenix.LiveViewTest
test "disconnected and connected mount" , % { conn: conn } do
conn = get ( conn , "/" )
assert html_response ( conn , 200 ) =~ "LiveView Message Page"
{ :ok , _view , _html } = live ( conn )
end
end Estamos probando que se puede acceder al / punto final y tiene el texto "LiveView Message Page" en la página.
Consulte también el módulo LiveViewTest para obtener más información sobre las pruebas y LiveView.
Finalmente, puede eliminar todo el código generado predeterminado vinculado al PageController :
rm test/liveview_chat_web/controllers/page_controller_test.exsrm lib/liveview_chat_web/controllers/page_controller.exrm test/liveview_chat_web/views/page_view_test.exsrm lib/liveview_chat_web/views/page_view.exrm -r lib/liveview_chat_web/templates/page Ahora puede ejecutar la prueba nuevamente con el comando mix test . Debería ver lo siguiente (Pasando las pruebas):
Generated liveview_chat app
...
Finished in 0.1 seconds (0.06s async, 0.1s sync)
3 tests, 0 failures
Randomized with seed 841084 Con la estructura LiveView definida, podemos centrarnos en crear mensajes. La base de datos guardará el mensaje y el nombre del remitente. Creemos un nuevo esquema y migración:
mix phx.gen.schema Message messages name:string message:stringNota : No olvide ejecutar
mix ecto.migratepara crear la nueva tabla demessagesen la base de datos.
Ahora podemos actualizar el esquema Message para agregar funciones para crear nuevos mensajes y enumerar los mensajes existentes. También actualizaremos los cambios para agregar requisitos y validaciones en el texto del mensaje. Abra el archivo lib/liveview_chat/message.ex y actualice el código con el siguiente:
defmodule LiveviewChat.Message do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias LiveviewChat.Repo
alias __MODULE__
schema "messages" do
field :message , :string
field :name , :string
timestamps ( )
end
@ doc false
def changeset ( message , attrs ) do
message
|> cast ( attrs , [ :name , :message ] )
|> validate_required ( [ :name , :message ] )
|> validate_length ( :message , min: 2 )
end
def create_message ( attrs ) do
% Message { }
|> changeset ( attrs )
|> Repo . insert ( )
end
def list_messages do
Message
|> limit ( 20 )
|> order_by ( desc: :inserted_at )
|> Repo . all ( )
end
end Hemos agregado la función validate_length en la entrada del mensaje para garantizar que los mensajes tengan al menos 2 caracteres . Este es solo un ejemplo para mostrar cómo funciona la validación changeset con el formulario en la página LiveView .
Luego creamos las funciones create_message/1 y list_messages/0 . Similar al ejemplo de Phoenix-Chat, limit el número de mensajes devueltos a los últimos 20 .
mount/3 Abra el archivo lib/liveview_chat_web/live/message_live.ex y agregue la siguiente línea en la línea 3:
alias LiveviewChat.Message Luego actualice la función mount/3 en el archivo lib/liveview_chat_web/live/message_live.ex para usar la función list_messages :
def mount ( _params , _session , socket ) do
messages = Message . list_messages ( ) |> Enum . reverse ( )
changeset = Message . changeset ( % Message { } , % { } )
{ :ok , assign ( socket , changeset: changeset , messages: messages ) }
end mount/3 ahora obtendrá la lista de messages y creará un changeset que se utilizará para el formulario de mensaje. Luego asignamos el changeset y los messages al socket que los mostrarán en la página LiveView.
Actualice la plantilla messages.html.heex al siguiente código:
< ul id =' msg-list ' phx-update =" append " >
< %= for message < - @messages do % >
< li id = { "msg-#{message.id}"} >
< b > < %= message.name % > : </ b >
< %= message.message % >
</ li >
< % end % >
</ ul >
< .form let={f} for={@changeset} id="form" phx-submit="new_message" phx-hook="Form" >
< %= text_input f, :name, id: "name", placeholder: "Your name", autofocus: "true" % >
< %= error_tag f, :name % >
< %= text_input f, :message, id: "msg", placeholder: "Your message" % >
< %= error_tag f, :message % >
< %= submit "Send"% >
</ .form > Primero muestra los nuevos mensajes y luego proporciona un formulario para que las personas create un nuevo mensaje.
Si actualiza la página, debería ver lo siguiente:

La sintaxis <.form></.form> es cómo usar el componente de función de formulario.
Un componente de funciones es cualquier función que reciba un mapa
assignscomo argumento y devuelve unastructrenderizada construida con el~HSigil.
Finalmente, asegurémonos de que la prueba siga pasando actualizando el archivo assert en la test/liveview_chat_web/live/message_live_test.exs a:
assert html_response ( conn , 200 ) =~ "LiveView Chat" Como hemos eliminado el título LiveView Message Page , en su lugar podemos probar el título en el diseño de la raíz y asegurarnos de que la página aún se muestre correctamente.
Por el momento, si ejecutamos la aplicación Phoenix mix phx.server y enviamos el formulario en el navegador, no sucederá nada. Si miramos el registro del servidor, vemos lo siguiente:
** (UndefinedFunctionError) function LiveviewChatWeb.MessageLive.handle_event/3
is undefined or private
(liveview_chat 0.1.0) LiveviewChatWeb.MessageLive.handle_event("new_message",
%{"_csrf_token" => "fyVPIls_XRBuGwlkMhxsFAciRRkpAVUOLW5k4UoR7JF1uZ5z2Dundigv",
"message" => %{"message" => "", "name" => ""}}, #Phoenix.LiveView.Socket
Al enviar el formulario está creando un nuevo evento definido con phx-submit :
< . form let = { f } for = { @ changeset } id = "form" phx - submit = "new_message" >
Sin embargo, este evento aún no se administra en el servidor, podemos solucionar esto agregando la función handle_event/3 en lib/liveview_chat_web/live/message_live.ex :
def handle_event ( "new_message" , % { "message" => params } , socket ) do
case Message . create_message ( params ) do
{ :error , changeset } ->
{ :noreply , assign ( socket , changeset: changeset ) }
{ :ok , _message } ->
changeset = Message . changeset ( % Message { } , % { "name" => params [ "name" ] } )
{ :noreply , assign ( socket , changeset: changeset ) }
end
end La función create_message se llama con los valores del formulario. Si se produce un error al intentar guardar la información en la base de datos, por ejemplo, el changeset puede devolver un error si el nombre o el message está vacío o si el message es demasiado corto, el changeset se asigna nuevamente al socket. Esto permitirá que el formulario muestre la información error :

Si el mensaje se guarda sin ningún error, estamos creando un nuevo conjunto de cambios que contiene el nombre del formulario para evitar que las personas tengan que ingresar su nombre nuevamente en el formulario, y asignamos el nuevo Cambio al socket.

Ahora se muestra el formulario, podemos agregar las siguientes pruebas para test/liveview_chat_web/live/message_live_test.exs :
import Plug.HTML , only: [ html_escape: 1 ]
test "name can't be blank" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert view
|> form ( "#form" , message: % { name: "" , message: "hello" } )
|> render_submit ( ) =~ html_escape ( "can't be blank" )
end
test "message" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert view
|> form ( "#form" , message: % { name: "Simon" , message: "" } )
|> render_submit ( ) =~ html_escape ( "can't be blank" )
end
test "minimum message length" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert view
|> form ( "#form" , message: % { name: "Simon" , message: "h" } )
|> render_submit ( ) =~ "should be at least 2 character(s)"
end Estamos utilizando la función form/3 para seleccionar el formulario y activar el evento Enviar con diferentes valores para el nombre y el mensaje. Estamos probando que los errores se muestran correctamente.
En lugar de tener que volver a cargar la página para ver los mensajes recién creados, podemos usar PubSub ( Pub Lish Sub Scribe) para informar a todos los clientes conectados que se ha creado un nuevo mensaje y actualizar la interfaz de usuario para mostrar el nuevo mensaje.
Abra el archivo lib/liveview_chat/message.ex y agregue la siguiente línea cerca de la parte superior:
alias Phoenix.PubSubA continuación, agregue las siguientes 3 funciones:
def subscribe ( ) do
PubSub . subscribe ( LiveviewChat.PubSub , "liveview_chat" )
end
def notify ( { :ok , message } , event ) do
PubSub . broadcast ( LiveviewChat.PubSub , "liveview_chat" , { event , message } )
end
def notify ( { :error , reason } , _event ) , do: { :error , reason } subscribe/0 se llamará cuando un cliente haya mostrado correctamente la página LiveView y escuche nuevos mensajes. Es solo una función de envoltura para phoenix.pubsub.subscribe.
notify/2 se invoca cada vez que se crea un nuevo mensaje para transmitir el mensaje a los clientes conectados. Repo.insert puede devolver {:ok, message} o {:error, reason} , por lo que necesitamos definir notify/2 manejar ambos casos.
Actualice la función create_message/1 en message.ex para invocar nuestra función notify/2 recién creada:
def create_message ( attrs ) do
% Message { }
|> changeset ( attrs )
|> Repo . insert ( )
|> notify ( :message_created )
endmount/3 Ahora podemos conectar al cliente cuando se representa la página LiveView . En la parte superior del archivo lib/liveview_chat_web/live/message_live.ex , agregue la siguiente línea:
alias LiveviewChat.PubSub Luego actualice la función de mount/3 con:
def mount ( _params , _session , socket ) do
if connected? ( socket ) , do: Message . subscribe ( )
messages = Message . list_messages ( ) |> Enum . reverse ( )
changeset = Message . changeset ( % Message { } , % { } )
{ :ok , assign ( socket , messages: messages , changeset: changeset ) }
end mount/3 ahora verifica que el socket está conectado y luego llama al nuevo Message.subscribe/0 .
handle_event/3 Dado que el valor de retorno de create_message/1 ha cambiado, necesitamos actualizar handle_event/3 a lo siguiente:
def handle_event ( "new_message" , % { "message" => params } , socket ) do
case Message . create_message ( params ) do
{ :error , changeset } ->
{ :noreply , assign ( socket , changeset: changeset ) }
:ok -> # broadcast returns :ok (just the atom!) if there are no errors
changeset = Message . changeset ( % Message { } , % { "name" => params [ "name" ] } )
{ :noreply , assign ( socket , changeset: changeset ) }
end
endhandle_info/2 El último paso es manejar el evento :message_created definiendo la función handle_info/2 en lib/liveview_chat_web/live/message_live.ex :
def handle_info ( { :message_created , message } , socket ) do
messages = socket . assigns . messages ++ [ message ]
{ :noreply , assign ( socket , messages: messages ) }
endCuando se recibe el evento, el nuevo mensaje se agrega a la lista de mensajes existentes. Luego se asigna la nueva lista al socket que actualizará la interfaz de usuario para mostrar el nuevo mensaje.
Agregue las siguientes pruebas para test/liveview_chat_web/live/message_live_test.exs para asegurarse de que los mensajes se muestren correctamente en la página:
test "message form submitted correctly" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert view
|> form ( "#form" , message: % { name: "Simon" , message: "hi" } )
|> render_submit ( )
assert render ( view ) =~ "<b>Simon:</b>"
assert render ( view ) =~ "hi"
end
test "handle_info/2" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert render ( view )
# send :created_message event when the message is created
Message . create_message ( % { "name" => "Simon" , "message" => "hello" } )
# test that the name and the message is displayed
assert render ( view ) =~ "<b>Simon:</b>"
assert render ( view ) =~ "hello"
end ¡Ahora debería tener una aplicación de chat funcional utilizando LiveView! Ejecute la aplicación Phoenix con:
mix phx.server Visite la aplicación localhost:4000 en 2 o más navegadores, ¡y envíe algunos mensajes!

Un problema que podemos notar es que la entrada del mensaje no siempre se restablece a un valor vacío después de enviar un mensaje usando la tecla Enter en el campo de entrada. Esto nos obliga a eliminar el mensaje anterior manualmente antes de escribir y enviar uno nuevo.
La razón es:
El cliente JavaScript es siempre la fuente de la verdad para los valores de entrada actuales. Para cualquier entrada dada con Focus ,
LiveViewnunca sobrescribirá el valor actual de la entrada, incluso si se desvía de las actualizaciones renderizadas del servidor. Ver: https://hexdocs.pm/phoenix_live_view/form-bindings.html#javascript-client-especifics
Nuestra solución es usar phx-hook para ejecutar un JavaScript en el cliente después de una de las devoluciones de llamada LiveView Life-Cycle (montadas, antes superadas, actualizadas, destruidas, desconectadas, reconectadas).
Agregamos un gancho para monitorear cuando se updated el formulario de mensaje. En el archivo message.html.heex , agregue el atributo phx-hook al elemento <.form> :
< .form let={f} for={@changeset} id="form" phx-submit="new_message" phx-hook="Form" > Luego, en el archivo de assets/js/app.js , agregue la siguiente lógica JavaScript :
// get message input element
let msg = document . getElementById ( 'msg' ) ;
// define "Form" hook, the name must match the one
// defined with phx-hoo="Form"
let Hooks = { }
Hooks . Form = {
// Each time the form is updated run the code in the callback
updated ( ) {
// If no error displayed reset the message value
if ( document . getElementsByClassName ( 'invalid-feedback' ) . length == 0 ) {
msg . value = '' ;
}
}
}
let csrfToken = document . querySelector ( "meta[name='csrf-token']" ) . getAttribute ( "content" )
let liveSocket = new LiveSocket ( "/live" , Socket , { params : { _csrf_token : csrfToken } , hooks : Hooks } ) // Add hooks: Hooks La lógica principal para restablecer el valor del mensaje está contenido dentro de la función updated() de devolución de llamada:
if ( document . getElementsByClassName ( 'invalid-feedback' ) . length == 0 ) {
msg . value = '' ;
} Antes de configurar el valor en una cadena vacía, primero verificamos que no se muestran errores en el formulario marcando la clase CSS invalid-feedback . (Lea más sobre comentarios: https://hexdocs.pm/phoenix_live_view/form-bindings.html#phx-feedback-for)
El último paso es establecer los hooks en liveSocket con hooks: Hooks . ¡La entrada del mensaje ahora debe restablecerse cuando se agrega un nuevo mensaje!
En este momento, la función mount/3 primero inicializa la lista de mensajes cargando los últimos 20 mensajes de la base de datos:
def mount ( _params , _session , socket ) do
if connected? ( socket ) , do: Message . subscribe ( )
messages = Message . list_messages ( ) |> Enum . reverse ( ) # get the list of messages
changeset = Message . changeset ( % Message { } , % { } )
{ :ok , assign ( socket , messages: messages , changeset: changeset ) } ## assigns messages to socket
end Luego, cada vez que se crea un nuevo mensaje, la función handle_info Agregar el mensaje a la lista de mensajes:
def handle_info ( { :message_created , message } , socket ) do
messages = socket . assigns . messages ++ [ message ] # append new message to the existing list
{ :noreply , assign ( socket , messages: messages ) }
endEsto puede causar problemas si la lista de mensajes se vuelve demasiado tiempo, ya que todos los mensajes se mantienen en la memoria en el servidor.
Para minimizar el uso de la memoria, podemos definir los mensajes como una assign temporal:
def mount ( _params , _session , socket ) do
if connected? ( socket ) , do: Message . subscribe ( )
messages = Message . list_messages ( ) |> Enum . reverse ( )
changeset = Message . changeset ( % Message { } , % { } )
{ :ok , assign ( socket , messages: messages , changeset: changeset ) ,
temporary_assigns: [ messages: [ ] ] }
endLa lista de mensajes se recupera una vez, luego se restablece a una lista vacía.
Ahora el handle_info/2 solo necesita asignar el nuevo mensaje al socket:
def handle_info ( { :message_created , message } , socket ) do
{ :noreply , assign ( socket , messages: [ message ] ) }
end Finalmente, la plantilla de mensajes heex escucha cualquier cambio en la lista de mensajes con phx-update y agrega el nuevo mensaje a la lista visualizada existente.
< ul id =' msg-list ' phx-update =" append " >
< %= for message < - @messages do % >
< li id = {message.id} >
< b > < %= message.name % > : </ b >
< %= message.message % >
</ li >
< % end % >
</ ul > Consulte también la página de documentación de Phoenix temporary-assigns : https://hexdocs.pm/phoenix_live_view/dom-patching.html#tempuary-signs
Actualmente, el campo name se deja a la persona para definir manualmente antes de enviar un mensaje. Esto está bien en una aplicación de demostración básica, pero sabemos que podemos hacerlo mejor. En esta sección agregaremos autenticación usando auth_plug . Eso permitirá que las personas que usan la aplicación se autenticen con su cuenta GitHub o Google y luego se llenarán el name en el formulario de mensaje.
AUTH_API_KEYSegún las instrucciones, primero cree una nueva clave API en https://authdemo.fly.dev/ eg:

Luego cree un archivo .env y agregue su nueva clave API creada:
export AUTH_API_KEY = 88SwQGzaZoJYXs6ihvwMy2dRVtm6KVeg4tSCjRKtwDvMUYUbi/88SwQDatWtSTMd2rKPnaZsAWFNpbf4vv2ZK7JW2nwuSypMeg/authdemo.fly.devNota : Por razones de seguridad, esta no es una clave API válida. Por favor, cree el suyo, es gratis y toma menos de un minuto.
auth_plug Agregue el paquete Auth_Plug a sus dependencias. En el archivo mix.exs actualiza su función deps y agregue:
{ :auth_plug , "~> 1.4.10" } Esta dependencia creará nuevas sesiones para usted y se comunicará con la aplicación DWYL auth .
No olvides:
source .envmix deps.get Asegúrese de que el AUTH_API_KEY esté accesible antes de compilar la nueva dependencia.
Puede recompilar las dependencias con mix deps.compile --force .
Ahora podemos comenzar a agregar la función de autenticación.
router.ex Para permitir que los usuarios de "invitados" [no autenticados] tengan acceso al chat, usamos el enchufe AuthPlugOptional . Lea más en Auth Opcional.
En el archivo router.ex , creamos una nueva tubería Plug :
# define the new pipeline using auth_plug
pipeline :authOptional , do: plug ( AuthPlugOptional ) Siguiente actualización del scope "/", LiveviewChatWeb do Bloqueo a lo siguiente:
scope "/" , LiveviewChatWeb do
pipe_through [ :browser , :authOptional ]
live "/" , MessageLive
get "/login" , AuthController , :login
get "/logout" , AuthController , :logout
endAhora estamos permitiendo que la autenticación sea opcional para todas las rutas del enrutador. Fácil, oye?
AuthController Cree el AuthController con las funciones login/2 y logout/2 .
Cree un nuevo archivo: lib/liveview_chat_web/controllers/auth_controller.ex y agregue el siguiente código:
defmodule LiveviewChatWeb.AuthController do
use LiveviewChatWeb , :controller
def login ( conn , _params ) do
redirect ( conn , external: AuthPlug . get_auth_url ( conn , "/" ) )
end
def logout ( conn , _params ) do
conn
|> AuthPlug . logout ( )
|> put_status ( 302 )
|> redirect ( to: "/" )
end
end La función login/2 redirige a la aplicación DWYL AUTH. Lea más sobre cómo usar la función AuthPlug.get_auth_url/2 . Una vez autenticado, el usuario se redirigirá a / Endpoint y se crea una sesión jwt en el cliente.
La función logout/2 invoca AuthPlug.logout/1 que elimina la sesión (JWT) y redirige a la página de inicio.
on_mount/4 LiveView proporciona la devolución de llamada on_mount que nos permite ejecutar código antes del mount . Usaremos esta devolución de llamada para verificar la sesión jwt y asignar los valores de la person ( Map ) y loggedin ( boolean ) al socket .
En el archivo lib/liveview_chat_web/controllers/auth_controller.ex Agregue el siguiente código para definir dos versiones de mount/4 :
# import the assign_new function from LiveView
import Phoenix.LiveView , only: [ assign_new: 3 ]
# pattern match on :default auth and check session has jwt
def on_mount ( :default , _params , % { "jwt" => jwt } = _session , socket ) do
# verify and retrieve jwt stored data
claims = AuthPlug.Token . verify_jwt! ( jwt )
# assigns the person and the loggedin values
socket =
socket
|> assign_new ( :person , fn ->
AuthPlug.Helpers . strip_struct_metadata ( claims )
end )
|> assign_new ( :loggedin , fn -> true end )
{ :cont , socket }
end
# when jwt is not defined just returns the current socket
def on_mount ( :default , _params , _session , socket ) do
socket = assign_new ( socket , :loggedin , fn -> false end )
{ :cont , socket }
endasigne_new/3 asigna un valor al socket si no existe.
Una vez que se define la devolución de llamada on_mount/2 , podemos llamarlo en nuestro archivo lib/liveview_chat_web/live/message_live.ex :
defmodule LiveviewChatWeb.MessageLive do
use LiveviewChatWeb , :live_view
alias LiveviewChat.Message
# run authentication on mount
on_mount LiveviewChatWeb.AuthController
Ahora tenemos toda la lógica para permitir que las personas se autenticen, solo necesitamos actualizar nuestro archivo de diseño raíz lib/liveview_chat_web/templates/layout/root.html.heex para mostrar un enlace login (o logout )::::
< body >
< header >
< section class =" container " >
< nav >
< ul >
< %= if @loggedin do % >
< li >
< img width =" 40px " src = {@person.picture}/ >
</ li >
< li > < %= link "logout", to: "/logout" % > </ li >
< % else % >
< li > < %= link "Login", to: "/login" % > </ li >
< % end % >
</ ul >
</ nav >
< h1 > LiveView Chat Example </ h1 >
</ section >
</ header >
< %= @inner_content % >
</ body > Si la persona aún no loggedin mostramos un enlace login de lo contrario se muestra el enlace logout .
El último paso es mostrar el nombre de la persona iniciada en el campo Nombre del formulario de mensaje. Para eso, podemos actualizar el conjunto de cambios de forma en la función mount para establecer los parámetros de nombre:
def mount ( _params , _session , socket ) do
if connected? ( socket ) , do: Message . subscribe ( )
# add name parameter if loggedin
changeset =
if socket . assigns . loggedin do
Message . changeset ( % Message { } , % { "name" => socket . assigns . person [ "givenName" ] } )
else
Message . changeset ( % Message { } , % { } )
end
messages = Message . list_messages ( ) |> Enum . reverse ( )
{ :ok , assign ( socket , messages: messages , changeset: changeset ) ,
temporary_assigns: [ messages: [ ] ] }
end¡Ahora puede ejecutar la aplicación y poder iniciar sesión/cerrar sesión!

En esta sección utilizaremos Phoenix Presence para mostrar una lista de personas que actualmente están utilizando la aplicación.
El primer paso es crear el archivo lib/liveview_chat/presence.ex :
defmodule LiveviewChat.Presence do
use Phoenix.Presence ,
otp_app: :liveview_chat ,
pubsub_server: LiveviewChat.PubSub
end Luego, en lib/liveview_chat/application.ex agregamos el módulo Presence recién creado a la lista de aplicaciones para que el supervisor se inicie:
def start ( _type , _args ) do
children = [
# Start the Ecto repository
LiveviewChat.Repo ,
# Start the Telemetry supervisor
LiveviewChatWeb.Telemetry ,
# Start the PubSub system
{ Phoenix.PubSub , name: LiveviewChat.PubSub } ,
# Presence
LiveviewChat.Presence ,
# Start the Endpoint (http/https)
LiveviewChatWeb.Endpoint
# Start a worker by calling: LiveviewChat.Worker.start_link(arg)
# {LiveviewChat.Worker, arg}
]
...
Ahora estamos listos para usar las funciones de presencia en nuestro punto final LiveView.
En el archivo lib/liveview_chat_web/live/message_live.ex , actualice la función mount con lo siguiente:
@ presence_topic "liveview_chat_presence"
def mount ( _params , _session , socket ) do
if connected? ( socket ) do
Message . subscribe ( )
{ id , name } =
if socket . assigns . loggedin do
{ socket . assigns . person [ "id" ] , socket . assigns . person [ "givenName" ] }
else
{ socket . id , "guest" }
end
{ :ok , _ } = Presence . track ( self ( ) , @ presence_topic , id , % { name: name } )
Phoenix.PubSub . subscribe ( PubSub , @ presence_topic )
end
changeset =
if socket . assigns . loggedin do
Message . changeset ( % Message { } , % { "name" => socket . assigns . person [ "givenName" ] } )
else
Message . changeset ( % Message { } , % { } )
end
messages = Message . list_messages ( ) |> Enum . reverse ( )
{ :ok ,
assign ( socket ,
messages: messages ,
changeset: changeset ,
presence: get_presence_names ( )
) , temporary_assigns: [ messages: [ ] ] }
end Recapitulemos los principales cambios en la función mount/3 :
Primero creamos el atributo del módulo @presence_topic para definir el topic que usaremos con las funciones de presencia.
La siguiente parte del código define una tupla que contiene una id de la persona y su nombre. El nombre predeterminado a "Invitado" si la persona no está registrada.
{ id , name } =
if socket . assigns . loggedin do
{ socket . assigns . person [ "id" ] , socket . assigns . person [ "givenName" ] }
else
{ socket . id , "guest" }
endEn segundo lugar, usamos la función Track/4 para hacer que Presence sepa que un nuevo cliente está mirando la aplicación:
{ :ok , _ } = Presence . track ( self ( ) , @ presence_topic , id , % { name: name } )Tercero usamos PubSub para escuchar los cambios de presencia (persona que se une o abandona la aplicación):
Phoenix.PubSub . subscribe ( PubSub , @ presence_topic ) Finalmente creamos una nueva presence de asignación en el socket:
presence : get_presence_names ( ) La función get_presence_names devolverá una lista de usuarios registrados y, si es que los usuarios "invitados".
Agregue el siguiente código al final del módulo MessageLive :
defp get_presence_names ( ) do
Presence . list ( @ presence_topic )
|> Enum . map ( fn { _k , v } -> List . first ( v . metas ) . name end )
|> group_names ( )
end
# return list of names and number of guests
defp group_names ( names ) do
loggedin_names = Enum . filter ( names , fn name -> name != "guest" end )
guest_names =
Enum . count ( names , fn name -> name == "guest" end )
|> guest_names ( )
if guest_names do
[ guest_names | loggedin_names ]
else
loggedin_names
end
end
defp guest_names ( 0 ) , do: nil
defp guest_names ( 1 ) , do: "1 guest"
defp guest_names ( n ) , do: " #{ n } guests" La llamada de función importante en el código anterior es Presence.list(@presence_topic) . La función Lista/1 devuelve la lista de usuarios utilizando la aplicación. Las funciones group_names y guest_names están aquí para manipular los datos de presencia devueltos por list , consulte https://hexdocs.pm/phoenix/phoenix.presence.html#c:list/1-presence-stucture
Hasta ahora hemos rastreado a nuevas personas que usan la página de chat en la función mount y hemos estado usando PubSub para escuchar cambios de presencia. El paso final es manejar estos cambios agregando una función handle_info :
def handle_info ( % { event: "presence_diff" , payload: _diff } , socket ) do
{ :noreply , assign ( socket , presence: get_presence_names ( ) ) }
endFinalmente, se enviará una diferencia de presencia y eventos de licencia a los clientes a medida que ocurran en tiempo real con el evento "presencia_diff".
La función handle_info atrapa el evento presence_diff y reasigna al Sucket el valor presence con el resultado de la llamada de función get_presence_names .
Para mostrar los nombres, agregamos los siguientes en el archivo lib/liveview_chat_web/templates/message/messages.html.heex Template:
< b > People currently using the app: </ b >
< ul >
< %= for name < - @presence do % >
< li >
< %= name % >
</ li >
< % end % >
</ ul >Ahora debería poder ejecutar la aplicación y ver los usuarios de registro de registro y la cantidad de usuarios invitados.
Podemos probar que la plantilla se ha actualizado correctamente agregando estas dos pruebas en test/liveview_chat_web/live/message_live_test.exs :
test "1 guest online" , % { conn: conn } do
{ :ok , view , _html } = live ( conn , "/" )
assert render ( view ) =~ "1 guest"
end
test "2 guests online" , % { conn: conn } do
{ :ok , _view , _html } = live ( conn , "/" )
{ :ok , view2 , _html } = live ( conn , "/" )
assert render ( view2 ) =~ "2 guests"
end Si eres nuevo en Tailwind , consulte: https://github.com/dwyl/learn-tailwind
Reemplace el contenido de lib/liveview_chat_web/templates/layout/root.html.heex con:
<!DOCTYPE html >
< html lang =" en " >
< head >
< meta charset =" utf-8 " />
< meta http-equiv =" X-UA-Compatible " content =" IE=edge " />
< meta name =" viewport " content =" width=device-width, initial-scale=1.0 " />
< meta name =" csrf-token " content = {csrf_token_value()} >
< %= live_title_tag assigns[:page_title] || "LiveviewChat", suffix: " · Phoenix Framework" % >
< script defer phx-track-static type =" text/javascript " src = {Routes.static_path(@conn, "/assets/app.js")} > </ script >
< script src =" https://cdn.tailwindcss.com " > </ script >
</ head >
< body >
< header class =" bg-slate-800 w-full min-h-[15%] pt-5 pb-1 mb-2 " >
< section >
< nav >
< div class =" text-white width-[10%] float-left ml-3 -mt-5 align-middle " >
< b > People in Chat: </ b >
< ul >
< %= for name < - @presence do % >
< li >
< %= name % >
</ li >
< % end % >
</ ul >
</ div >
< ul class =" float-right mr-3 " >
< %= if @loggedin do % >
< li >
< img width =" 42px " src = {@person.picture} class =" -mt-3 " />
</ li >
< li class =" text-white " >
< %= link "logout", to: "/logout" % >
</ li >
< % else % >
< li class =" bg-green-600 text-white rounded-xl px-4 py-2 w-full mb-2 font-bold " >
< %= link "Login", to: "/login" % >
</ li >
< % end % >
</ ul >
</ nav >
< h1 class =" text-3xl mb-4 text-center font-mono text-white " > LiveView Chat Example </ h1 >
</ section >
</ header >
< %= @inner_content % >
</ body >
</ html > Y luego reemplace el contenido de lib/liveview_chat_web/templates/message/messages.html.heex con:
< ul id =' msg-list ' phx-update =" append " >
< %= for message < - @messages do % >
< li id = { "msg-#{message.id}"} class="px-5" >
< small class =" float-right text-xs align-middle " >
< %= message.inserted_at % >
</ small >
< b > < %= message.name % > : </ b >
< %= message.message % >
</ li >
< % end % >
</ ul >
< footer class =" fixed bottom-0 w-full bg-slate-300 pb-2 px-5 pt-2 " >
< .form let={f} for={@changeset} id="form" phx-submit="new_message" phx-hook="Form" >
< %= if @loggedin do % >
< %= text_input f, :name, id: "name", value: @person.givenName,
class: "hidden" % >
< % else % >
< %= text_input f, :name, id: "name", placeholder: "Name", autofocus: "true",
class: "border p-2 w-9/12 mb-2 mt-2 mr2" % >
< span class =" italic text-2xl ml-4 " > or </ span >
< span class =" bg-green-600 text-white rounded-xl px-4 py-2 mb-2 mt-3 float-right " >
< %= link "Login", to: "/login" % >
</ span >
< %= error_tag f, :name % >
< % end % >
< %= text_input f, :message, id: "msg", placeholder: "Message",
class: "border p-2 w-10/12 mb-2 mt-2 float-left" % >
< p class =" text-amber-600 " >
< %= error_tag f, :message % >
</ p >
< %= submit "Send", class: "bg-sky-600 text-white rounded-xl px-4 py-2 mt-2 float-right" % >
</ .form >
</ footer >Ahora debería tener una interfaz de usuario/diseño que se vea así:

Si tiene preguntas sobre cualquiera de las clases Tailwind utilizadas, gaste 2 minutos buscando en Google y luego si todavía está atrapado, abra un problema.
Si encontró este ejemplo útil, por favor, por favor, el repositorio de GitHub para que nosotros ( y otros ) sepamos que le gustó.
Aquí hay algunos otros repositorios que es posible que desee leer:
¿Alguna pregunta o sugerencia? ¡No dudes en abrir nuevos problemas!
¡Gracias!