Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 10 additions & 83 deletions local-live-view/README.md
Original file line number Diff line number Diff line change
@@ -1,90 +1,17 @@
# First steps
# LocalLiveView

## Usage
To use LocalLiveView you need to:
1. Add `:local_live_view` to your mix.exs file:
```elixir
defmodule LocalThermostat.MixProject do
use Mix.Project
...
defp deps do
[
{:local_live_view, github: "software-mansion/popcorn", sparse: "local-live-view"}
]
end
end
```
2. Attach local_live_view.js script in your .html file:
```html
<html>
<script type="module" src="./local_live_view/local_live_view.js" defer></script>
<body>
</body>
</html>
```
3. Define your LocalLiveView in the `lib` directory:
```elixir
defmodule ThermostatLive do
use LocalLiveView
**LocalLiveView is a library for running Phoenix LiveView state in the browser, powered by [Popcorn](https://hexdocs.pm/popcorn).**

def render(assigns) do
~H"""
<p>Current temperature: {@temperature}°F</p>
<button pop-click="inc_temperature">+</button>
<p>Country: {@country}</p>
"""
end
LocalLiveView compiles your Elixir modules to WebAssembly and runs them via Popcorn directly in the browser. You get zero-latency UI and offline capability while using the same Phoenix LiveView API you already know.

def mount(_params, _session, socket) do
temperature = 65
{:ok, assign(socket, :temperature, temperature)}
end
## Documentation

def handle_event("inc_temperature", _params, socket) do
{:noreply, update(socket, :temperature, &(&1 + 1))}
end
end
```
4. Add html tag to render defined view, by using data-pop-view:
```html
<html>
<script type="module" src="./local_live_view/local_live_view.js" defer></script>
<body>
<div data-pop-view="ThermostatLive"></div>
</body>
</html>
```

## Build

To build the project after defining the above, run:

```bash
mix deps.get
mix build
```

This will generate the necessary scripts and popcorn files into the output directory.

## Serve

To serve the project locally:

```bash
mix dev
```

and visit [localhost:4000](http://localhost:4000).

You can also use `mix popcorn.server` directly, or any HTTP server that sets the required headers:

```
Cross-Origin-Opener-Policy: "same-origin"
Cross-Origin-Embedder-Policy: "require-corp"
```
The API documentation and guides are available at <https://hexdocs.pm/local_live_view>

## Examples

- `examples/local-lv-thermostat` - Simple thermostat app demonstrating basic LocalLiveView state management and events.
- `examples/local-lv-forms` - Form handling with LocalLiveView, including validation and submission.
- `examples/local-lv-compare` - Side-by-side comparison of Phoenix LiveView and LocalLiveView.
The source code for all examples is in the `examples/` directory of this repository:

- `local-lv-thermostat` — Basic state management and events
- `local-lv-forms` — Form handling with validation and Mirror Sync
- `local-lv-compare` — Side-by-side comparison of Phoenix LiveView vs LocalLiveView
4 changes: 2 additions & 2 deletions local-live-view/lib/server/mirror.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ defmodule LocalLiveView.Mirror do
It receives synced payloads from the local runtime via the `handle_sync/2` callback.

```
defmodule Mirror.MyLive do
defmodule Mirror.MyLocal do
use LocalLiveView.Mirror

@impl true
def handle_sync(local_assigns, _mirror_assigns) do
Phoenix.PubSub.broadcast(MyApp.PubSub, "llv_mirror:MyLive", {:llv_attrs, local_assigns})
Phoenix.PubSub.broadcast(MyApp.PubSub, "llv_mirror:MyLocal", {:llv_attrs, local_assigns})
{:ok, local_assigns}
end
end
Expand Down
7 changes: 5 additions & 2 deletions local-live-view/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ defmodule LocalLiveView.MixProject do
filter_modules: ~r/^(?!Elixir.Phoenix\.).*/,
extras: [
"pages/introduction/welcome.md",
"README.md"
"pages/getting-started/installation.md",
"pages/guides/first-view.md",
"pages/guides/mirror-sync.md"
],
groups_for_extras: [
Introduction: ~r"/introduction/",
"Getting started": "README.md"
"Getting started": ~r"/getting-started/",
Guides: ~r"/guides/"
]
]
end
Expand Down
108 changes: 108 additions & 0 deletions local-live-view/pages/getting-started/installation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Installation

LocalLiveView is installed into an existing Phoenix project using the `mix llv.install` generator.

## Prerequisites

- A Phoenix project generated with `mix phx.new`
- `:local_live_view` added as a dependency
- `pnpm` or `npm` for JS package management

## Step 1 — Add the dependency

Add `:local_live_view` to your `mix.exs`:

```elixir
defp deps do
[
# ...existing deps...
{:local_live_view, github: "software-mansion/popcorn", sparse: "local-live-view"}
]
end
```

Then fetch it:

```bash
mix deps.get
```

## Step 2 — Run the installer

```bash
mix llv.install
```

The installer configures your project automatically:

| What | Where |
|---|---|
| Adds `LocalLiveView.Socket` | `lib/*_web/endpoint.ex` |
| Adds COOP/COEP security headers (required for WASM) | `lib/*_web/endpoint.ex` |
| Registers `LocalLiveView.ChannelRegistry` | `lib/<app>/application.ex` |
| Imports `LocalLiveView.Component` | `lib/*_web.ex` (html_helpers) |
| Changes app.js script tag to `type="module"` | `lib/*_web/components/layouts/root.html.heex` |
| Adds `setup` call for the JS bridge | `assets/js/app.js` |
| Adds `local_live_view` JS package | `assets/package.json` |
| Replaces esbuild watcher with `build.mjs` | `mix.exs`, `config/dev.exs` |
| Generates the `local/` WASM project | `local/` |

> **Manual fallback:** If the installer can't find a file (e.g. your project has a non-standard structure), it prints the exact snippet to add manually.

## Step 3 — Install JS dependencies

```bash
pnpm install
# or: npm install --prefix assets
```

## Step 4 — Build the WASM bundle

```bash
mix llv.build
```

This compiles your `local/` project to a WASM bundle at `priv/static/assets/js/wasm/bundle.avm`.

To build automatically as part of `mix setup`, the installer already adds `llv.build` to the `setup` alias in `mix.exs`.

## Step 5 — Start the server

```bash
mix phx.server
```

Visit [localhost:4000](http://localhost:4000). The installer generated a sample `HelloLocal` view — add it to any template to confirm everything works:

```heex
<.local_live_view view="HelloLocal" />
```

## What was generated

The installer creates a `local/` directory — a separate Mix project for your client-side Elixir code:

```
local/
├── config/
│ └── config.exs # Popcorn output path config
├── lib/
│ ├── local/
│ │ └── application.ex # OTP application
│ └── hello_local.ex # Sample LocalLiveView
├── .formatter.exs
└── mix.exs # Compiles to WASM via popcorn.cook
```

Add your LocalLiveView modules to `local/lib/`. They will be compiled into the WASM bundle when you run `mix llv.build`.

## Security headers

LocalLiveView requires [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer), which browsers only allow with the following HTTP headers set:

```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```

The installer adds a `put_wasm_security_headers/2` plug to your endpoint automatically. If you use a CDN or reverse proxy, make sure these headers are forwarded.
142 changes: 142 additions & 0 deletions local-live-view/pages/guides/first-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Your first LocalLiveView

This guide walks through building a simple counter view to introduce the LocalLiveView programming model.

## Creating a view module

LocalLiveView modules live in the `local/lib/` directory of your project. Create `local/lib/counter_local.ex`:

```elixir
defmodule CounterLocal do
use LocalLiveView

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

def render(assigns) do
~H"""
<div>
<p>Count: {@count}</p>
<button phx-click="increment">+</button>
<button phx-click="decrement">-</button>
</div>
"""
end

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

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

This should look familiar if you've used Phoenix LiveView. The differences are:

- `use LocalLiveView` instead of `use Phoenix.LiveView`

## Mounting the view

Use the `<.local_live_view>` component in any Phoenix template:

```heex
<.local_live_view view="CounterLocal" />
```

The `view` attribute is the module name as a string. The component renders a `<div>` that becomes the mount point for the WASM view.

## Building and running

After adding the module, rebuild the WASM bundle:

```bash
mix llv.build
```

Then start or reload the server:

```bash
mix phx.server
```

The counter is now fully local — clicks are handled in the browser with no server round-trips.

## Callbacks

### `mount/3`

Called once when the view is initialized. Use it to set up initial assigns.

```elixir
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0, label: "Counter")}
end
```

### `render/1`

Returns the HEEx template for the current state. Called automatically after every state change.

```elixir
def render(assigns) do
~H"""
<p>{@label}: {@count}</p>
"""
end
```

### `handle_event/3`

Handles events triggered from the template. Returns `{:noreply, socket}` with updated assigns.

```elixir
def handle_event("reset", _params, socket) do
{:noreply, assign(socket, count: 0)}
end
```

## Assigning state

LocalLiveView uses the same assign functions as Phoenix LiveView:

```elixir
# Assign a single key
assign(socket, :count, 0)

# Assign multiple keys at once
assign(socket, count: 0, label: "Counter")

# Update a key using the current value
update(socket, :count, &(&1 + 1))
```

## Timers and periodic updates

You can schedule recurring messages using `Process.send_after/3`, just like in Phoenix LiveView:

```elixir
def mount(_params, _session, socket) do
Process.send_after(self(), :tick, 1000)
{:ok, assign(socket, time: Time.utc_now())}
end

def handle_info(:tick, socket) do
Process.send_after(self(), :tick, 1000)
{:noreply, assign(socket, time: Time.utc_now())}
end
```

## Multiple views on one page

Each `<.local_live_view>` on the page runs as an independent process in the WASM runtime. You can mount as many as you need:

```heex
<.local_live_view view="CounterLocal" />
<.local_live_view view="CounterLocal" id="second-counter" />
<.local_live_view view="ThermostatLocal" />
```

When mounting the same view multiple times, use the `id` attribute to give each instance a unique identifier.
Loading
Loading