# Book 5 — LiveView: Real-Time UI Without JS

```elixir
Mix.install([
  {:kino, "~> 0.14"},
  {:phoenix_playground, "~> 0.1"}
])
```

## Setup

Companion to `book5-liveview.html`. [Phoenix Playground](https://github.com/phoenix-playground/phoenix_playground) lets us run a **real Phoenix LiveView server from a notebook cell** — no project generation needed.

Each server cell below starts (or restarts) an app on **http://localhost:5001**. Open that URL in a browser tab next to Livebook. Re-running a cell hot-swaps the LiveView.

> If port 5001 is busy, change the `port:` option in the cells.

## 1. The counter — a GenServer per browser tab

```elixir
defmodule CounterLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    {:ok, assign(socket, count: 0, pid: inspect(self()))}
  end

  def handle_event("inc", _params, socket) do
    {:noreply, update(socket, :count, &(&1 + 1))}
  end

  def handle_event("dec", _params, socket) do
    {:noreply, update(socket, :count, &(&1 - 1))}
  end

  def render(assigns) do
    ~H"""
    <div style="font-family: sans-serif; max-width: 400px; margin: 4rem auto; text-align: center;">
      <h1 style="font-size: 3rem;">{@count}</h1>
      <button phx-click="dec" style="font-size: 1.5rem; padding: 0.3rem 1.2rem;">-</button>
      <button phx-click="inc" style="font-size: 1.5rem; padding: 0.3rem 1.2rem;">+</button>
      <p style="color: #888; margin-top: 2rem;">
        This tab's LiveView process: <code>{@pid}</code><br/>
        Open a second tab — different process, independent state.
      </p>
    </div>
    """
  end
end

PhoenixPlayground.start(live: CounterLive, port: 5001, open_browser: false)
```

Now open **http://localhost:5001**:

* Click the buttons — no page reload, no fetch calls, no client state. Every click is a WebSocket message to `handle_event/3`.
* Open a **second tab**: it shows its own PID and its own count. One process per tab, exactly like Book 3's process-per-entity.
* Open dev tools → Network → WS and watch the diffs: single-digit bytes per update.

### Exercise 5.1

Add a `"reset"` event and button, plus a `step` assign controlled by two more buttons ("step: 1" / "step: 5") so +/- move by the current step. Re-run the cell to hot-swap and test in the browser.

<details>
<summary>💡 Solution sketch</summary>

```elixir
# in mount: assign(socket, count: 0, step: 1, ...)
# events:
def handle_event("reset", _p, socket), do: {:noreply, assign(socket, count: 0)}
def handle_event("set-step", %{"n" => n}, socket),
  do: {:noreply, assign(socket, step: String.to_integer(n))}
def handle_event("inc", _p, socket),
  do: {:noreply, update(socket, :count, &(&1 + socket.assigns.step))}

# template:
# <button phx-click="set-step" phx-value-n="5">step: 5</button>
```

`phx-value-*` attributes arrive in the params map — that's how you pass data with a click.

</details>

## 2. The two mounts, observed

```elixir
defmodule MountLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    connected = connected?(socket)
    IO.puts("mount/3 called — connected?: #{connected} — pid: #{inspect(self())}")

    if connected do
      :timer.send_interval(1000, :tick)
    end

    {:ok, assign(socket, connected: connected, ticks: 0)}
  end

  def handle_info(:tick, socket) do
    {:noreply, update(socket, :ticks, &(&1 + 1))}
  end

  def render(assigns) do
    ~H"""
    <div style="font-family: sans-serif; max-width: 500px; margin: 4rem auto;">
      <h2>connected?: {@connected}</h2>
      <h2>ticks: {@ticks}</h2>
      <p style="color:#888">Check the Livebook cell output: mount ran TWICE —
      once for the dead render (connected?: false, process exits immediately)
      and once for the live socket (connected?: true, where the timer belongs).</p>
    </div>
    """
  end
end

PhoenixPlayground.start(live: MountLive, port: 5001, open_browser: false)
```

Reload the browser tab and watch this notebook's output: two `mount/3` lines per page load, different PIDs. The timer only exists in the second (connected) one — put a timer in the dead render and it's pure waste.

## 3. Live form validation with a changeset-shaped form

```elixir
defmodule SignupLive do
  use Phoenix.LiveView

  # A tiny "changeset" stand-in so this notebook needs no Ecto dep.
  # In a real app this is Ecto.Changeset — same shape, same flow (Book 4).
  defp validate(params) do
    errors =
      []
      |> then(fn e ->
        if String.length(params["name"] || "") < 3,
          do: [{:name, "must be at least 3 characters"} | e], else: e
      end)
      |> then(fn e ->
        if String.match?(params["email"] || "", ~r/@/),
          do: e, else: [{:email, "must contain @"} | e]
      end)

    %{valid?: errors == [], errors: Map.new(errors), params: params}
  end

  def mount(_params, _session, socket) do
    {:ok, assign(socket, check: validate(%{}), saved: false)}
  end

  def handle_event("validate", %{"user" => params}, socket) do
    {:noreply, assign(socket, check: validate(params), saved: false)}
  end

  def handle_event("save", %{"user" => params}, socket) do
    check = validate(params)
    {:noreply, assign(socket, check: check, saved: check.valid?)}
  end

  def render(assigns) do
    ~H"""
    <div style="font-family: sans-serif; max-width: 420px; margin: 4rem auto;">
      <h2>Sign up</h2>
      <form phx-change="validate" phx-submit="save">
        <p>
          <input type="text" name="user[name]" placeholder="name"
                 value={@check.params["name"]} phx-debounce="300"
                 style="width:100%; padding:0.5rem;"/>
          <span :if={@check.errors[:name]} style="color:#c00; font-size:0.85rem">
            {@check.errors[:name]}
          </span>
        </p>
        <p>
          <input type="text" name="user[email]" placeholder="email"
                 value={@check.params["email"]} phx-debounce="300"
                 style="width:100%; padding:0.5rem;"/>
          <span :if={@check.errors[:email]} style="color:#c00; font-size:0.85rem">
            {@check.errors[:email]}
          </span>
        </p>
        <button disabled={not @check.valid?} style="padding:0.5rem 1.5rem;">Create</button>
      </form>
      <h3 :if={@saved} style="color: #2a2;">✅ saved!</h3>
    </div>
    """
  end
end

PhoenixPlayground.start(live: SignupLive, port: 5001, open_browser: false)
```

Type in the form: errors appear/disappear **as you type** (`phx-change` + `phx-debounce`), the button enables only when valid — zero lines of JavaScript, one set of validation rules on the server. With Ecto, `to_form(changeset)` + `<.input>` components give you this with even less code.

## 4. Multi-user real time: PubSub

The finale: shared state across all connected browsers. A tiny OTP layer (an Agent — Book 3) + PubSub broadcast + `handle_info`:

```elixir
defmodule ClickStore do
  use Agent
  def start_link(_), do: Agent.start_link(fn -> %{} end, name: __MODULE__)

  def click(user) do
    counts = Agent.get_and_update(__MODULE__, fn m ->
      m = Map.update(m, user, 1, &(&1 + 1))
      {m, m}
    end)

    Phoenix.PubSub.broadcast(PhoenixPlayground.PubSub, "clicks", {:clicks, counts})
    counts
  end

  def all, do: Agent.get(__MODULE__, & &1)
end

defmodule RaceLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(PhoenixPlayground.PubSub, "clicks")
    end

    name = "player-" <> String.slice(inspect(self()), -5, 4)
    {:ok, assign(socket, me: name, counts: ClickStore.all())}
  end

  def handle_event("click", _params, socket) do
    ClickStore.click(socket.assigns.me)
    {:noreply, socket}
  end

  # EVERY connected tab gets this message when ANYONE clicks:
  def handle_info({:clicks, counts}, socket) do
    {:noreply, assign(socket, counts: counts)}
  end

  def render(assigns) do
    ~H"""
    <div style="font-family: sans-serif; max-width: 480px; margin: 3rem auto; text-align:center;">
      <h2>🏁 Click race — you are {@me}</h2>
      <button phx-click="click" style="font-size:2rem; padding: 0.5rem 2rem;">CLICK!</button>
      <div style="margin-top: 2rem; text-align: left;">
        <div :for={{user, n} <- Enum.sort_by(@counts, &elem(&1, 1), :desc)}
             style="margin: 0.4rem 0;">
          <b>{user}</b>
          <div style={"background:#7e57c2; height:20px; width:#{min(n * 8, 400)}px; border-radius:4px;"}></div>
          {n}
        </div>
      </div>
      <p style="color:#888; margin-top:2rem;">Open 2–3 tabs and race yourself. Every click
      broadcasts via PubSub; every tab's process re-renders its diff.</p>
    </div>
    """
  end
end

PhoenixPlayground.start(live: RaceLive, port: 5001, open_browser: false)
```

Open **two or three tabs** of http://localhost:5001 and click in each. All tabs update in real time. Trace the plumbing — it's all Books 1–3:

1. `phx-click` → WebSocket → `handle_event` in *your* tab's process
2. `ClickStore.click` updates shared state (Agent) and `broadcast`s
3. PubSub delivers `{:clicks, counts}` to every subscribed process (plain messages!)
4. each `handle_info` assigns → diff → DOM patch

### Exercise 5.2

Add a **reset button** that any player can press: broadcast a `:reset` message, handle it in `handle_info`, and clear the Agent state. Bonus: show a "🏆" next to the leader.

```elixir
# modify the modules above and re-run
```

<details>
<summary>💡 Solution sketch</summary>

```elixir
# ClickStore:
def reset do
  Agent.update(__MODULE__, fn _ -> %{} end)
  Phoenix.PubSub.broadcast(PhoenixPlayground.PubSub, "clicks", {:clicks, %{}})
end

# RaceLive:
def handle_event("reset", _p, socket) do
  ClickStore.reset()
  {:noreply, socket}
end

# leader: compute in render — `{leader, _} = Enum.max_by(@counts, &elem(&1, 1), fn -> {nil, 0} end)`
```

</details>

## Wrap-up

You ran four real LiveViews from a notebook: per-tab state, the double mount, live validation, and PubSub-synced multi-user UI. That's the full stack.

**Next:** `book6-capstone.livemd` — design and build a complete supervised, real-time system from a blank page.
