Skip to the content.

(Partly generated by LLM)

Phoenix LiveView tutorial

Purpose: Phoenix LiveView has great documentation in Hexdocs, but it is in a top-down format. You can read it from start to end and understand many things, but if you want help on how to do a certain thing, it is extremely difficult to find concise information. Additionally, it provides various generator utilities that generate all CRUD pages and code. However, these are so complicated—especially in the LiveView framework—that making even trivial changes is quite complex unless you understand everything in it.

This tutorial is my attempt to build a simple app in Phoenix LiveView in a bottom-up fashion. I’ll start with very basic but complete examples and build complexity gradually until we have a complete CRUD app. Finally, I’ll try to compare my code with the generator code.

1: LiveView basics:

1.1: Setup

To get started with Phoenix LiveView, first create a new Phoenix project with LiveView enabled. Once your project is set up, you need to add a route for your LiveView in the router:

defmodule YourWeb.Scratchpad do
  use YourWeb, :live_view

  def render(assigns) do
    ~H"""
    <h1>Scratchpad</h1>
    """
  end
end

This sets up a basic LiveView page that simply displays a heading.

1.2: Assigns

LiveView uses assigns to manage state between the server and the client. You can set initial assigns in the mount/3 callback. Here’s how you can assign some data and use it in your render function:

  def mount(_params, _session, socket) do
    IO.puts("mount called")
    {:ok, assign(socket, :data, "test data")}
  end
def render(assigns) do
    IO.inspect(assigns, label: "Scratchpad assigns")
    ~H"""
    <h1>Scratchpad</h1>
    data: {@data}    
    """
end

Now, when you visit the page, you’ll see the value of @data rendered.

1.3: Interactivity using Events

LiveView allows you to handle client-side events on the server. For example, you can handle button clicks by defining a handle_event/3 function.

  def handle_event(event, _params, socket) do
    IO.puts("handle_event called with event: #{inspect(event)}")
    {:noreply, socket}
  end
  def render(assigns) do
    IO.inspect(assigns, label: "Scratchpad assigns")
    ~H"""
    <h1>Scratchpad</h1>
    data: {@data}
    <button class="bg-blue-300 rounded-md p-2 m-2" phx-click="btn-click">Click me</button>
    """
  end

When the button is clicked, the btn-click event is sent to the server and handled by your handle_event/3 function.

1.4: Submit Data

You can use forms in LiveView to submit data to the server. The form submission triggers an event, and you can update assigns based on the submitted data.

  def render(assigns) do
    IO.inspect(assigns, label: "Scratchpad assigns")
    ~H"""
    <h1>Scratchpad</h1>
    Your input: {@data}
    <form phx-submit="form-submit">
      <input type="text" name="input_data" />
      <button type="submit" class="bg-blue-200 p-2 m-2">Submit</button>
    </form>
    """
  end
  def handle_event(event, params, socket) do
    IO.puts("handle_event called with event: #{inspect(event)}")
    {:noreply, assign(socket, :data,
      params["input_data"])}
  end

Now, when you submit the form, the input value is assigned to @data and displayed on the page.

1.5: JavaScript Interop

LiveView supports interop with client-side JavaScript using hooks. You can push events from JavaScript to the server and update the UI accordingly.

Example: Timer with JS and LiveView

Suppose you want a JavaScript timer that ticks every second, and every 10 ticks, it sends a message to the server. Both the JS ticks and server messages are shown in the browser.

Add a hook in your assets/js/app.js:

// assets/js/app.js
let Hooks = {}
Hooks.TimerHook = {
  mounted() {
    let count = 0
    this.timer = setInterval(() => {
      count++
      
      document.getElementById("actual_tick").innerText = "Actual tick " + count
      if (count % 10 === 0) {
        this.pushEvent("tick", {count: count})
      }
    }, 1000)
  },
  destroyed() {
    clearInterval(this.timer)
  }
}


let liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken},
  hooks: Hooks,
})
  def mount(_params, _session, socket) do
    IO.puts("mount called")
    {:ok, socket
    |> assign(:data, "")
    |> assign(:event, "")}
  end
  def render(assigns) do
    IO.inspect(assigns, label: "Scratchpad assigns")
    ~H"""
    <div id="timer" phx-hook="TimerHook">
      <h1>Scratchpad</h1>
      <p id="actual_tick"></p>
      <p>Event: {@event}</p>
      <p>Your input: {@data}</p>
      <form phx-submit="form-submit">
        <input type="text" name="input_data" />
        <button type="submit" class="bg-blue-200 p-2 m-2">Submit</button>
      </form>
    </div>
    """
  end
  def handle_event(event, params, socket) do
    case event do
      "form-submit" ->
        IO.puts("Form submitted with params: #{inspect(params)}")
        {:noreply,
        socket
        |> assign(:data, params["input_data"] || "")
        |> assign(:event, event)}

      "tick" ->
        IO.puts("Tick event received")
        {:noreply,
        socket
        |> assign(:event, event <> " " <> to_string(params["count"]))}
      _ ->
        IO.puts("Unknown event: #{event}")
        {:noreply,
        socket
        |> assign(:event, event)}
    end
  end

This setup allows you to see real-time updates from both JavaScript and server events in your LiveView.

1.6: Conditional Attributes and Rendering

LiveView makes it easy to conditionally render elements and change their attributes based on state. For example, you can show or hide a div and change its class based on a checkbox value.

  def mount(_params, _session, socket) do
    IO.puts("mount called")
    {:ok, socket
    |> assign(:data, :off)
    |> assign(:event, "")}
  end

Note the conditional checked attribute and the use of phx-change to handle checkbox changes. The class and visibility of elements are updated based on the value of @data.

  def render(assigns) do
    IO.inspect(assigns, label: "Scratchpad assigns")
    ~H"""
    <div id="timer">
      <h1>Scratchpad</h1>
      <p id="actual_tick"></p>
      <p>Event: {@event}</p>
      <p>Your input: {@data}</p>
      <form phx-change="checkbox">
        <input type="checkbox" name="show?" checked={@data==:on}/> Show Div
      </form>
      <div class={if @data==:on, do: "bg-green-100"}> Random string</div>
      <div id="toggle_div" :if={@data == :on}>
        <p>This is a toggleable div.</p>
      </div>
    </div>
    """
  end
  def handle_event(event, params, socket) do
    case event do
      "checkbox" ->
        case params["show?"] do
          "on" ->
            IO.puts("Checkbox checked, showing div")
            {:noreply,
            socket
            # }
            |> assign(:data, :on)
            }
          _ ->
            IO.puts("Checkbox unchecked, hiding div")
            {:noreply,
            socket
            # }
            |> assign(:data, :off)
            }
        end

      _ ->
        IO.puts("Unknown event: #{event}")
        {:noreply,
        socket
        |> assign(:event, event)}
    end
    # IO.puts("handle_event called with event: #{inspect(event)}")

  end

This example demonstrates how to use LiveView’s assigns and event handling to create dynamic, interactive UIs with conditional rendering and attribute changes.

This completes part 1 of the tutorial, which covers fundamentals of liveview. In next tutorial we’ll build basic components for a typical CRUD app.