Skip to content

Docs reorganization #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 26, 2025
Merged
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
516 changes: 44 additions & 472 deletions README.md

Large diffs are not rendered by default.

38 changes: 10 additions & 28 deletions example_project/assets/vue/Counter.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script setup lang="ts">
import {ref} from "vue"
import ShowState from "./ShowState.vue";
const props = defineProps<{count: number}>()
const emit = defineEmits<{inc: [{value: number}]}>()
import { ref } from "vue"
import ShowState from "./ShowState.vue"
const props = defineProps<{ count: number }>()
const emit = defineEmits<{ inc: [{ value: number }] }>()
const diff = ref<string>("1")
</script>

<template>
<ShowState :server-state="props" :client-state="{diff}">
<ShowState :server-state="props" :client-state="{ diff }">
Current count

<Transition mode="out-in">
Expand All @@ -17,32 +17,15 @@ const diff = ref<string>("1")
</Transition>

<label class="block mt-8">Diff: </label>
<input v-model="diff" class="mt-4 w-full" type="range" min="1" max="10">
<input v-model="diff" class="mt-4 w-full" type="range" min="1" max="10" />

<button
@click="emit('inc', {value: parseInt(diff)})"
class="mt-4 bg-black text-white rounded p-2 block">
Increase counter by {{ parseInt(diff) * 2 }}
</button>
<button @click="emit('inc', { value: parseInt(diff) })" class="mt-4 bg-black text-white rounded p-2 block">
Increase counter by {{ parseInt(diff) }}
</button>
</ShowState>
</template>















<style scoped>

.v-enter-active,
.v-leave-active {
position: relative;
Expand All @@ -61,5 +44,4 @@ const diff = ref<string>("1")
.v-leave-to {
opacity: 0;
}

</style>
</style>
16 changes: 13 additions & 3 deletions example_project/lib/live_vue_examples_web/live/counter.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule LiveVueExamplesWeb.LiveCounter do
use LiveVueExamplesWeb, :live_view
@topic "shared_session"

def render(assigns) do
~H"""
Expand All @@ -9,12 +10,21 @@ defmodule LiveVueExamplesWeb.LiveCounter do
end

def mount(_params, _session, socket) do
{:ok, assign(socket, count: 10)}
if connected?(socket) do
LiveVueExamplesWeb.Endpoint.subscribe(@topic)
end

{:ok, assign(socket, :count, 0)}
end

def handle_event("inc", %{"value" => diff}, socket) do
socket = update(socket, :count, &(&1 + diff))

new_count = socket.assigns.count + diff
LiveVueExamplesWeb.Endpoint.broadcast(@topic, "update_count", new_count)
{:noreply, socket}
end

def handle_info(%{event: "update_count", payload: new_count}, socket) do
# Update the count for all connected users
{:noreply, assign(socket, :count, new_count)}
end
end
226 changes: 226 additions & 0 deletions guides/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# How LiveVue Works

This guide explains the architecture and inner workings of LiveVue, helping you understand the design decisions and implementation details.

> #### Practical Usage {: .tip}
>
> Looking for practical examples? Check out [Basic Usage](basic_usage.html) for common patterns and [Getting Started](getting_started.html) for your first component.

## Overview

LiveVue bridges two different paradigms: **Phoenix LiveView** with its server-side state management and HTML over WebSockets, and **Vue.js** with its client-side reactivity and virtual DOM. The challenge is making these two systems work together seamlessly while maintaining the benefits of both.

## Architecture Diagram

![LiveVue flow](./images/lifecycle.png)

## Component Lifecycle

### 1. Server-Side Rendering (SSR)

When a LiveView renders a Vue component, LiveVue generates a special `div` element with component configuration stored in data attributes. Here's what the server generates:

```elixir
# In your LiveView template
<.vue v-component="MyComponent" message={@message} v-on:click="handle_click" />
```

This produces HTML like:

```html
<div
id="MyComponent-1"
data-name="MyComponent"
data-props="{&quot;message&quot;:&quot;Hello World&quot;}"
data-handlers="{&quot;click&quot;:[&quot;push&quot;,{&quot;event&quot;:&quot;handle_click&quot;}]}"
data-slots="{}"
data-ssr="true"
phx-hook="VueHook"
>
<!-- Optional SSR content here -->
</div>
```

The component name, props serialized as JSON, event handlers, and slots are all embedded as data attributes, with an optional Phoenix LiveView hook attachment.

### 2. Client-Side Hydration

When the page loads and Phoenix LiveView connects, the `VueHook` activates. Here's the simplified flow from `hooks.ts`:

```typescript
export const getVueHook = ({ resolve, setup }: LiveVueApp): LiveHookInternal => ({
async mounted() {
const componentName = this.el.getAttribute("data-name") as string
const component = await resolve(componentName)

const props = reactive(getProps(this.el, this.liveSocket))
const slots = reactive(getSlots(this.el))

const app = setup({
createApp: makeApp,
component,
props,
slots,
// ... other options
})

this.vue = { props, slots, app }
}
})
```

The hook resolves the component name to the actual Vue component, makes props and slots reactive using Vue's reactivity system, mounts the Vue component (optionally hydrating existing SSR content), and configures event handlers for bidirectional communication.

### 3. Reactive Updates

When server state changes, LiveView sends new data via WebSocket. Phoenix updates only the changed data attributes, Vue's reactivity system automatically detects these changes, and only affected parts of the Vue component re-render. This happens through the `updated()` hook:

```typescript
updated() {
Object.assign(this.vue.props ?? {}, getProps(this.el, this.liveSocket))
Object.assign(this.vue.slots ?? {}, getSlots(this.el))
}
```

## Data Flow

### Props Flow (Server → Client)

LiveView manages authoritative state and passes it to Vue components as props. When LiveView assigns are updated, the HEEX template generates new prop data, only changed props are sent over WebSocket, and the Vue component automatically re-renders with new props.

The server-side extraction logic in `live_vue.ex` ensures efficient updates:

```elixir
defp extract(assigns, type) do
Enum.reduce(assigns, {%{}, false}, fn {key, value}, {acc, changed} ->
case normalize_key(key, value) do
^type -> {Map.put(acc, key, value), changed || key_changed(assigns, key)}
{^type, k} -> {Map.put(acc, k, value), changed || key_changed(assigns, key)}
_ -> {acc, changed}
end
end)
end
```

### Event Flow (Client → Server)

There are three main approaches for handling events:

**Standard Phoenix Events** (recommended for most cases) use direct `phx-click` attributes that work inside Vue components:

```html
<button phx-click="increment">Click me</button>
```

**Programmatic Events** use `useLiveVue().pushEvent()` for complex logic:

```javascript
const live = useLiveVue()
live.pushEvent("custom_event", { data: "value" })
```

**Vue Event Handlers** use the `v-on:` syntax for reusable components:

```elixir
<.vue v-component="Counter" v-on:increment="handle_increment" />
```

The event handlers are processed on the client side by invoking `liveSocket.execJS` with the payload defined by `JS` module.

> #### Event Handling Best Practices {: .tip}
>
> Use `phx-click` for simple, direct event handling. Use `live.pushEvent()` when you need programmatic control or complex logic. Use `v-on:` syntax when creating reusable Vue components that should be decoupled from specific LiveView implementations.

## Key Design Decisions

### Hook-Based Integration

LiveVue uses Phoenix LiveView's hook system rather than a separate JavaScript framework. This provides seamless integration within LiveView's lifecycle, automatic cleanup when elements are removed, and natural compatibility with all Phoenix events.

### Reactive Props and Slots

Props and slots are made reactive using Vue's reactivity system, enabling efficient updates where only changed data triggers re-renders, full compatibility with Vue features like computed properties and watchers, and minimal overhead for prop updates.

## Performance Optimizations

### Selective Updates

LiveVue minimizes data transmission by tracking only modified props, slots, and handlers. The JSON encoding is optimized to prevent redundant work, and Phoenix updates only specific data attributes rather than re-rendering entire elements.

### SSR Optimization

Server-side rendering is intelligently applied only during initial page loads (dead renders), can be configured per component, and is skipped during live navigation for better performance.

### Automatic Preloading

During server-side rendering, LiveVue automatically uses the Vite-generated manifest file to inject resource preload links (`<link rel="modulepreload">` and others) for all the assets required by a component. This ensures that the browser can download necessary JavaScript and CSS files earlier in the page load process, improving perceived performance and reducing the time to an interactive page.

### Memory Management

Automatic cleanup prevents memory leaks through proper hook lifecycle management. Vue apps are unmounted when hooks are destroyed, with special handling for Phoenix navigation events and automatic removal of event listeners:

```typescript
destroyed() {
const instance = this.vue.app
if (instance) {
window.addEventListener("phx:page-loading-stop", () => instance.unmount(), { once: true })
}
}
```

## Security Considerations

### Data Sanitization

All data passed between server and client is properly sanitized. Props are safely encoded with HTML escaping using `Jason.encode!(data, escape: :html_safe)`, all user data is escaped before transmission, and events go through Phoenix's standard validation.

### Event Security

Event handling maintains Phoenix's security model where all events are validated on the server, standard Phoenix CSRF protection applies, and LiveView's authorization patterns work normally.

## Slots Implementation

Slots bridge HEEX templates and Vue components by rendering slots server-side as HTML, encoding content as Base64 for safe transport, and decoding on the client for integration into Vue's slot system:

```typescript
const getSlots = (el: HTMLElement): Record<string, () => any> => {
const dataSlots = getAttributeJson(el, "data-slots")
return mapValues(dataSlots, base64 => () => h("div", { innerHTML: atob(base64).trim() }))
}
```

**Limitation**: Since slots are rendered server-side, they can't contain other Vue components or Phoenix hooks.

## Debugging and Development

### Development Tools

LiveVue works with standard development tools including Vue DevTools for full component inspection and debugging, Phoenix LiveView Dashboard for server-side state monitoring, and browser DevTools for network and WebSocket inspection.

### Debug Features

Built-in debugging capabilities include debug mode for detailed logging of component lifecycle, component resolution logs to help identify loading issues, and event tracing to track events flowing between Vue and LiveView.

## Limitations and Trade-offs

### Current Limitations

Vue components can't contain other Vue components, Phoenix hooks don't work inside slots, and there's limited browser API access during server rendering.

### Design Trade-offs

The Vue runtime adds approximately 34KB gzipped to your application. There's an additional abstraction layer between Phoenix and the client, and it requires understanding both Phoenix LiveView and Vue.js.

### When to Use LiveVue

LiveVue is a good fit for complex client-side interactions, rich UI components with local state, leveraging the Vue ecosystem (animations, charts, etc.), and teams with Vue.js expertise.

Consider alternatives for simple forms and basic interactions, applications prioritizing minimal JavaScript, or teams without Vue.js experience.

## Next Steps

Now that you understand how LiveVue works:

- [Configuration](configuration.html) to customize behavior and SSR settings
- [Basic Usage](basic_usage.html) for practical patterns and examples
- [Client-Side API](client_api.html) for detailed API reference
Loading
Loading