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

0.15.7 May 24, 2021

...

0.12.0 April 16, 2020

0.11.1 April 8, 2020

0.11.0 April 6, 2020

...

0.2.0 September 13, 2019

0.1.1 August 27, 2019

0.1.0 August 25, 2019

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

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?

Made with Slides.com