Elixir is a functional, concurrent, general-purpose programming language.
Uses Erlang virtual machine (BEAM).
Very useful for distributed and fault-tolerant applications.
Most used web framework in Elixir
Provides channels/web sockets
Uses plug library and the cowboy HTTP server
Single Page Application made very easy
Don't need to learn JavaScript / React
(Almost) No JavaScript code
Server side render
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
Live navigation to enrich links and redirects
Testing tools
Image by Mike Clark (@clarkware)
Image by Mike Clark (@clarkware)
Image by Mike Clark (@clarkware)
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
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
Handle changes in via live_patch
Requests via websocket
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>
<%= 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 %>
...
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
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.
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>
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>
Communication between LiveComponents
send_update(Cart, id: "cart", status: "cancelled")
Communication between LiveComponent and LiveView
send self(), {:search, params}
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
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" />
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 %>">
Client Side Framework
A rugged, minimal framework for composing JavaScript behavior in your markup.
AlpineJS is a perfect companion to LiveView
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>
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>
render_click/1
render_focus/2
render_blur/1
render_submit/1
render_change/1
render_keydown/1
render_keyup/1
render_hook/3
{: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
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
The Pragmatic Studio - Phoenix LiveView
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
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