https://github.com/elixir-volt/phoenix_vapor
Vue templates as native Phoenix LiveView rendered structs — compile Vue syntax to %Phoenix.LiveView.Rendered{} via Vapor IR
https://github.com/elixir-volt/phoenix_vapor
Last synced: about 1 month ago
JSON representation
Vue templates as native Phoenix LiveView rendered structs — compile Vue syntax to %Phoenix.LiveView.Rendered{} via Vapor IR
- Host: GitHub
- URL: https://github.com/elixir-volt/phoenix_vapor
- Owner: elixir-volt
- License: mit
- Created: 2026-03-11T19:46:02.000Z (2 months ago)
- Default Branch: master
- Last Pushed: 2026-03-25T05:42:32.000Z (about 2 months ago)
- Last Synced: 2026-03-26T10:33:22.597Z (about 2 months ago)
- Language: Elixir
- Size: 327 KB
- Stars: 12
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Phoenix Vapor
Vue template syntax compiled to native `%Phoenix.LiveView.Rendered{}` structs via Rust NIFs.
```elixir
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
use PhoenixVapor
def mount(_params, _session, socket), do: {:ok, assign(socket, count: 0)}
def render(assigns) do
~VUE"""
{{ count }}
+
"""
end
def handle_event("inc", _, socket), do: {:noreply, update(socket, :count, &(&1 + 1))}
end
```
Same WebSocket, same diff protocol, same LiveView client. No wrapper divs, no `phx-update="ignore"`.
## Three tiers
| Tier | What | How |
|------|------|-----|
| `~VUE` sigil | Vue templates in any LiveView | `~VUE"""
{{ count }}
"""` |
| `.vue` SFC | Complete LiveView from a `.vue` file | `use PhoenixVapor.Reactive, file: "Counter.vue"` |
| Vapor DOM | Bypass morphdom — direct DOM writes | `patchLiveSocket(liveSocket)` |
## Installation
```elixir
def deps do
[
{:phoenix_vapor, "~> 0.1.0"},
{:quickbeam, "~> 0.3.0", optional: true} # for complex JS expressions
]
end
```
## Supported syntax
`{{ expr }}` · `:attr="expr"` · `@click="handler"` · `v-if` / `v-else-if` / `v-else` · `v-for` · `v-show` · `v-model` · `v-html` · ternaries · arithmetic · `.length` · `.toUpperCase()` · dot access · components
Simple expressions evaluate in pure Elixir via OXC AST. Complex expressions (arrow functions, `.filter()`, `.map()`) fall back to [QuickBEAM](https://hex.pm/packages/quickbeam).
## `.vue` SFC mode
```vue
import { ref, computed } from "vue"
const count = ref(0)
const doubled = computed(() => count * 2)
function increment() { count++ }
{{ count }} × 2 = {{ doubled }}
+
```
```elixir
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
use PhoenixVapor.Reactive, file: "Counter.vue"
end
```
`ref()` → assigns, `computed()` → derived state, functions → event handlers. Three lines of Elixir.
## Vapor DOM
Opt-in morphdom bypass. The client parses statics once, builds a registry mapping each dynamic slot to its DOM node, then applies diffs as direct property writes.
```js
import { patchLiveSocket } from "phoenix_vapor"
patchLiveSocket(liveSocket)
```
See [ARCHITECTURE.md](ARCHITECTURE.md) for protocol-level details.
## Docs
- **[ARCHITECTURE.md](ARCHITECTURE.md)** — how it works at the protocol level, with wire format examples
- **[examples/demo](https://github.com/elixir-volt/phoenix_vapor/tree/master/examples/demo)** — runnable Phoenix app with all features
## License
MIT