Phoenix LiveView
Elixir
Elixir is a functional, concurrent, general-purpose programming language.
Uses Erlang virtual machine (BEAM).
Very useful for distributed and fault-tolerant applications.
Phoenix Framework
Most used web framework in Elixir
Provides channels/web sockets
Uses plug library and the cowboy HTTP server
Phoenix LiveView
Single Page Application made very easy
Don't need to learn JavaScript / React
(Almost) No JavaScript code
Server side render
First impressions
Phoenix LiveView
Render HTML on the server over WebSockets
Smart templating and change tracking
Live form validation with file upload support
API with the client with phx-click, etc
Code reuse via components
Features
Phoenix LiveView
Live navigation to enrich links and redirects
Testing tools
Features
Phoenix LiveView
Image by Mike Clark (@clarkware)
Phoenix LiveView
Image by Mike Clark (@clarkware)
Phoenix LiveView
Image by Mike Clark (@clarkware)
Phoenix LiveView
LiveView
Example of a page
defmodule MyAppWeb.UsersLive.Index do
use Phoenix.LiveView
def mount(_unsigned_params, _session, socket) do
{:ok, socket}
end
def handle_params(_unsigned_params, _session, socket) do
{:noreply, socket}
end
def render(assigns) do
~L"""
<h1>Users</h1>
"""
end
end
scope "/", MyAppWeb do
live "/users", UsersLive.Index
end
LiveView - Mount
Initial request, server side render.
Should always reply {:ok, socket}
unsigned_params
User input via URL
session
Connection parameters in session (cookies)
socket
Websocket properties, should always be returned in each interation
LiveView - handle_params
Handle changes in via live_patch
Requests via websocket
LiveView - handle_event
How actions are handled in LiveView
// Structure
def handle_event(event_name, event_values, socket) do
{:noreply, socket}
end
// Example
def handle_event("inc", %{}, socket) do
{:noreply, update(socket, :val, &(&1 +1))}
end
How actions are set in HTML
<a phx-click="inc">Increment</a>
<button phx-click="add" phx-value-id="123">Add</button>
LiveView - Forms - HTML
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save], fn form -> %>
<%= if !@changeset.valid? do %>
<div class="alert alert-danger" role="alert">
Errors were found, please check the items bellow.
</div>
<% end %>
<div class="form-group">
<%= label form, :email, class: "form-check-label" do %>Email<% end %>
<%= text_input form, :email, class: "form-control #{add_error_class(@changeset, :email, true)}" %>
<%= error_tag form, :email %>
</div>
<%= submit "Add", class: "btn btn-primary" %>
<% end %>
LiveView - Forms - Elixir
...
def handle_event("validate", %{"allowed_emails" => params}, socket) do
changeset =
%AllowedEmails{}
|> AllowedEmails.changeset(params)
|> Map.put(:action, :insert)
{:noreply, assign(socket, changeset: changeset)}
end
def handle_event("save", %{"allowed_emails" => %{"email" => email}}, socket) do
case Accounts.insert_allowed_email(email) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "User added!")
|> push_redirect(to: Routes.live_path(socket, MyAppWeb.Admin.UserLive.Index))}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
end
Phoenix LiveComponent
Similar to LiveView, but for reusable "views".
Exists in two flavours: stateless or stateful
Stateful components can be re-rendered based on changes in their own state.
Stateless components are only re-rendered when their hosting LiveView is re-rendered.
Phoenix LiveComponent
Add into the render file / method
<div>
<%= live_component ImageComponent, url: "https://image.jpg" %>
</div>
ID is needed for stateful components
<div>
<%= live_component UserComponent, id: 1, name: "User 1" %>
</div>
Phoenix LiveComponent
We can use target to define which component should handle the event
<a href="#" phx-click="say_hello" phx-target="<%= @myself %>">
Say hello!
</a>
ID is needed for stateful components
<div>
<%= live_component SideBarComponent, id: 1, name: "Xpto" %>
</div>
Phoenix LiveComponent
Communication between LiveComponents
send_update(Cart, id: "cart", status: "cancelled")
Phoenix LiveComponent
Communication between LiveComponent and LiveView
send self(), {:search, params}
Subscription
Listening for events, publish events, handling events
# Listening to events
def mount(_, _, socket) do
if connected?(socket), do: Phoenix.PubSub.subscribe("events")
{:ok, socket}
end
# Create event
def handle_event("add", %{"event" => %{"title" => title}}, socket) do
Phoenix.PubSub.broadcast("events", {:add_event, %{"title" => title}})
{:noreply, socket}
end
# Handle event
def handle_info({:add_event, event}, socket) do
{:noreply, assign(ssocket, event: event)}
end
JavaScript interaction
Client hooks
let Hooks = {}
Hooks.PhoneNumber = {
mounted() {
this.el.addEventListener("input", e => {
let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
if(match) {
this.el.value = `${match[1]}-${match[2]}-${match[3]}`
}
})
}
}
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})
<input type="text" name="user[phone_number]" id="user-phone-number"
phx-hook="PhoneNumber" />
JavaScript interaction
Client-Server interaction
Hooks.InfiniteScroll = {
page() { return this.el.dataset.page },
mounted(){
this.pending = this.page()
window.addEventListener("scroll", e => {
if(this.pending == this.page() && scrollAt() > 90){
this.pending = this.page() + 1
this.pushEvent("load-more", {})
}
})
},
updated(){ this.pending = this.page() }
}
<div id="infinite-scroll" phx-hook="InfiniteScroll" data-page="<%= @page %>">
AlpineJS
Client Side Framework
A rugged, minimal framework for composing JavaScript behavior in your markup.
AlpineJS is a perfect companion to LiveView
AlpineJS
Example
<div x-data="{ open: false }">
<div>
<button @click="open = !open">
<img src="avatar.jpg" alt="" />
</button>
</div>
<div x-show="open"
x-cloak>
<div>
<a href="#">Your Profile</a>
<a href="#">Settings</a>
<a href="#">Sign out</a>
</div>
</div>
</div>
AlpineJS
Example
<div id="<%= @id %>"
phx-hook="Modal"
x-data="{ open: <%= @show %> }"
x-init="() => {
$watch('open', isOpen => {
if (!isOpen) {
modalHook.modalClosing(<%= @leave_duration %>)
}
})
}"
@keydown.escape.window="if (connected) open = false"
x-show="open"
x-cloak>
Tests
LiveView Tests
render_click/1
render_focus/2
render_blur/1
render_submit/1
render_change/1
render_keydown/1
render_keyup/1
render_hook/3
LiveView Tests
{:ok, view, _html} = live(conn, "/thermo")
assert view
|> element("button#inc")
|> render_click() =~ "The temperature is: 31℉"
{:ok, view, html} =
conn
|> put_connect_params(%{"param" => "value"})
|> live_isolated(AppWeb.ClockLive, session: %{"tz" => "EST"})
LiveView
Isolated LiveView
LiveView Tests
live_view
|> render_click("redirect")
|> follow_redirect(conn, "/redirected/page")
assert view |> element("#some-element") |> has_element?()
assert render_component(MyComponent, id: 123, user: %User{}) =~
"some markup in component"
Redirect
Element selector
Component render
Free Course
The Pragmatic Studio - Phoenix LiveView
Demo
References
ElixirConf 2019 - How LiveView Handles File Uploads - Gary Rannie - https://www.youtube.com/watch?v=svpk-hKvTNk
The Lifecycle of a Phoenix LiveView https://pragmaticstudio.com/tutorials/the-life-cycle-of-a-phoenix-liveview
References
https://github.com/phoenixframework/phoenix_live_view
https://hexdocs.pm/phoenix_live_view/js-interop.html
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveViewTest.html
References
Questions?
Phoenix Live View
By David Magalhães
Phoenix Live View
WIP
- 390