Workshop 3: Frontend
Antes de empezar
En la migración de posts escribimos mal el on_delete. Pusimos:
add :post_id, references("posts"), on_delete: :delete_all
Y debería ser:
add :post_id, references("posts", on_delete: :delete_all)
Vamos a cambiar los estilos. Clonar projecto:
git clone https://github.com/nicanor/estilo-yo.git
Reemplazar contenido de la carpeta /assets/css/
y /lib/yo_web/templates/layout/
.
Agregar class table a las tablas:
<table class="table">
Agregar class button submit-button al botón del formulario:
<%= submit "Save", class: "button submit-button" %>
Procesos
Crear un proceso:
spawn(fn -> 1 + 2 end)
Vida de un proceso:
pid = spawn(fn -> 1 + 2 end)
Process.alive?(pid)
self()
Process.alive?(self())
Cola de mensajes:
send self(), {:mensaje, "Hola mundo"}
send self(), {:otra_cosa, "Como va?"}
Process.info(self(), :messages)
Llamamos receive 3 veces (la tercera va a bloquear):
receive do
{:mensaje, msg} -> msg
{:otra_cosa, _msg} -> "No me importa"
end
Ejemplo más complejo:
send self(), {:mensaje, "Hola mundo"}
send self(), {:mensaje, "Como va?"}
send self(), {:otra_cosa, "Como va?"}
send self(), {:no_matchea, "Como va?"}
send self(), {:mensaje, "Aguante Elixir"}
Process.info(self(), :messages)
receive do
{:mensaje, msg} -> msg
{:otra_cosa, _msg} -> "No me importa"
end
Process.info(self(), :messages)
Podemos usar timeout:
receive do
{:mensaje, msg} -> msg
{:otra_cosa, _msg} -> "No me importa"
after
1_000 -> "Pasó 1 segundo"
end
Linkear procesos y EXIT:
self()
spawn_link fn -> raise "oops" end
self()
Tareas:
# Mejor returno al lanzarlo
Task.start fn -> 1 + 1 end
# Mejor reporte de errores
Task.start fn -> raise "oops" end
Async y await:
# Lanza la tarea que puedo captar luego
task = Task.async(fn -> 1 + 1 end)
# Espero el resultado de la tarea
Task.await(task)
# Mejor ejemplo
Task.async(fn -> :timer.sleep(4000); 1 + 5 end) |> Task.await
task = Task.async(fn -> 1 + 5 end)
Process.info(self(), :messages)
Task.await(task)
Estado con procesos:
defmodule KV do
def start_link do
Task.start_link(fn -> loop(%{}) end)
end
defp loop(map) do
receive do
{:get, key, caller} ->
send caller, Map.get(map, key)
loop(map)
{:put, key, value} ->
loop(Map.put(map, key, value))
end
end
end
Podemos llamar al proceso:
# Lanza el proceso
{:ok, pid} = KV.start_link()
# Envia el mensage put
send(pid, {:put, :hello, :world})
# Envia el mensaje get
send(pid, {:get, :hello, self()})
# Veo los mensajes
Process.info(self, :messages)
# Recibo todos los mensajes
flush()
Agentes
{:ok, pid} = Agent.start_link(fn -> %{} end)
Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
Agent.get(pid, fn map -> Map.get(map, :hello) end)
Plug
%Plug.Conn
tiene información del request y el response. Correr en iex:
%Plug.Conn{}
Endpoint
Mirada simplificada del endpoint por defecto:
# lib_web/endpoint.ex
defmodule YoWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :yo
plug Plug.Static, []
plug Plug.RequestId
plug Plug.Telemetry, []
plug Plug.Parsers, []
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, []
plug YoWeb.Router
end
Plug.Static
Busca archivos estáticos enpriv/static
.Plug.RequestId
genera un ID único por cada request.Plug.Telemetry
agrega puntos de instrumentación para loguear el path, status code y tiempos.Plug.Parsers
nos dice como parsear el Request Body.Plug.MethodOverride
usa parametro_method
para cambiar POST por PUT, PATCH o DELETE.Plug.Head
convierte solicitudHEAD
a solicitudGET
.Plug.Session
maneja las cookies de sesión y las session stores.YoWeb.Router
es el último paso del endpoint.
Agregamos un Plug
defp unauthorize!(conn, _) do
conn
|> Plug.Conn.resp(401, "No autorizado!")
|> halt()
end
plug :unauthorize!
También podemos pasarle parametros:
defp unauthorize!(conn, options) do
message = options[:message] || "No autorizado!"
conn
|> Plug.Conn.resp(401, message)
|> halt()
end
plug :unauthorize!, message: "Paso mensaje por parámetro"
Y podemos escribirlo como módulo. Creamos la carpeta lib_web/plugs/
y agregamos el archivo unauthorized.ex
:
defmodule YoWeb.Plugs.Unauthorized do
import Plug.Conn
def init(default), do: default
def call(conn, options) do
message = options[:message] || "No autorizado"
conn
|> resp(401, message)
|> halt()
end
end
Y lo llamamos desde el endpoint:
plug YoWeb.Plugs.Unauthorized, message: "Ahora uso un Módulo"
Rutas
La linea resources "/posts", PostController
equivale a:
get "/posts", PostController, :index
get "/posts/:id/edit", PostController, :edit
get "/posts/new", PostController, :new
get "/posts/:id", PostController, :show
post "/posts", PostController, :create
put "/posts/:id", PostController, :update
patch "/posts/:id", PostController, :update
delete "/posts/:id", PostController, :delete
Corremos mix phx.routes
:
mix phx.routes
Cuando definimos rutas, automaticamente se crean helpers:
alias YoWeb.Router.Helpers, as: Routes
Routes.post_path(YoWeb.Endpoint, :index)
Routes.post_path(YoWeb.Endpoint, :show, 1)
Routes.post_path(YoWeb.Endpoint, :new)
Routes.post_path(YoWeb.Endpoint, :create)
Routes.post_path(YoWeb.Endpoint, :update, 2)
Routes.post_path(YoWeb.Endpoint, :delete, 3)
En las vistas los podemos usar así:
<%= link "Posts", to: Routes.post_path(@conn, :index) %>
Se pueden nestear resources:
resources "/posts", PostController do
resources "/comments", CommentController
end
Corremos mix phx.routes
y testeamos las nuevas rutas con iex -S mix
:
alias YoWeb.Router.Helpers, as: Routes
Routes.post_comment_path(YoWeb.Endpoint, :index, 1)
Routes.post_comment_path(YoWeb.Endpoint, :edit, 1, 2)
Plugs del router:
plug :accepts
define el formato de solicitud aceptado.plug :fetch_session
cargaconn
con los datos de sesión.plug :fetch_flash
cargaconn
con mensajes flash seteados.plug :protect_from_forgery
protege de cross site forgery.plug :put_secure_browser_headers
también protege de cross site forgery.
Controladores
En Rails:
class PostsController < ApplicationController
def show @post = Post.find(params[:id])
# render "show.html"
end
end
En Phoenix:
defmodule YoWeb.PostController do
use YoWeb, :controller
def show(conn, %{"id" => id}) do
post = Blog.get_post!(id)
render(conn, "show.html", post: post)
end
end
Agregamos plug a controlador:
plug YoWeb.Plugs.Unauthorized, message: "Desde el controlador"
Views
Las vistas se encargan de renderizar los templates.
defmodule YoWeb.PostView do
use YoWeb, :view
def render("index.html", _assigns) do
"Hola Mundo"
end
end
Phoenix.View.render(YoWeb.PageView, "index.html", %{})
Templates:
Agregar a show.html.erb
:
<%= "Puedo embeber código Elixir" %>
En Post.View
agregar:
def saludo do
"Hola Mundo"
end
Y en show.html.erb
agregar:
<%= saludo %>
Agregar comentarios en show.html.erb
:
<h2>Comments</h2>
<ul>
<%= for comment <- @post.comments do %>
<li><%= comment.body %></li>
<% end %>
</ul>
API
Agregamos rutas para api:
scope "/api", YoWeb.Api, as: :api do
pipe_through :api
resources "/posts", PostController, only: [:show, :index]
end
Agregamos nuevo controlador controllers/api/post_controller.ex
:
defmodule YoWeb.Api.PostController do
use YoWeb, :controller
alias Yo.Blog
def index(conn, _params) do
posts = Blog.list_posts()
render(conn, "index.json", posts: posts)
end
def show(conn, %{"id" => id}) do
post = Blog.get_post!(id)
render(conn, "show.json", post: post)
end
end
Agregamos nueva vista controllers/api/post_controller.ex
:
defmodule YoWeb.Api.PostView do
use YoWeb, :view
def render("index.json", %{posts: posts}) do
render_many(posts, YoWeb.Api.PostView, "show.json")
end
def render("show.json", %{post: post}) do
%{id: post.id, title: post.title, body: :body}
end
end