LiveVue v1.0 released
After four release candidates and a lot of community feedback, LiveVue 1.0 is stable ![]()
I’ve built a dedicated website with interactive examples: livevue.skalecki.dev
And wrote the full story behind the library: The Story of LiveVue
What Changed
New Composables
useLiveForm — Server-side validation with Ecto changesets, nested objects, dynamic arrays. Fully typed:
<script setup lang="ts">
import { useLiveForm, type Form } from 'live_vue'
type UserForm = {
name: string
profile: { bio: string; skills: string[] }
}
const props = defineProps<{ form: Form<UserForm> }>()
const form = useLiveForm(() => props.form, {
changeEvent: 'validate',
submitEvent: 'submit'
})
// Type-safe field access — typos caught at compile time
const nameField = form.field('name')
const bioField = form.field('profile.bio')
const skillsArray = form.fieldArray('profile.skills')
</script>
<template>
<input v-bind="nameField.inputAttrs.value" />
<span v-if="nameField.errorMessage.value">{{ nameField.errorMessage.value }}</span>
<div v-for="(skill, i) in skillsArray.fields.value" :key="i">
<input v-bind="skill.inputAttrs.value" />
<button @click="skillsArray.remove(i)">Remove</button>
</div>
<button @click="skillsArray.add('')">Add Skill</button>
<button @click="form.submit()" :disabled="!form.isValid.value">Submit</button>
</template>
LiveView side stays exactly as you’d expect:
def handle_event("validate", %{"user" => params}, socket) do
changeset = User.changeset(%User{}, params) |> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset, as: :user))}
end
useLiveUpload — File uploads with progress tracking and drag-and-drop:
<script setup>
import { useLiveUpload } from 'live_vue'
const { entries, showFilePicker, addFiles, progress } = useLiveUpload(
() => props.upload,
{ submitEvent: 'save' }
)
</script>
<template>
<div @drop.prevent="e => addFiles(e.dataTransfer.files)" @dragover.prevent>
Drop files or <button @click="showFilePicker">browse</button>
<div v-for="entry in entries" :key="entry.ref">
{{ entry.client_name }} — {{ entry.progress }}%
</div>
</div>
</template>
useLiveConnection — Reactive WebSocket status for offline indicators:
<script setup>
import { useLiveConnection } from 'live_vue'
const { isConnected, connectionState } = useLiveConnection()
</script>
<template>
<div :class="isConnected ? 'text-green-500' : 'text-red-500'">
{{ connectionState }}
</div>
</template>
useEventReply — Bi-directional events with server responses:
<script setup>
import { useEventReply } from 'live_vue'
const { data, isLoading, execute } = useEventReply('fetch_user')
</script>
<template>
<button @click="execute({ id: 123 })" :disabled="isLoading">
{{ isLoading ? 'Loading...' : 'Fetch User' }}
</button>
<div v-if="data">{{ data.name }}</div>
</template>
Performance: JSON Patch Diffs
Props now use RFC 6902 JSON Patch. Only differences are sent:
[{"op": "replace", "path": "/users/1/name", "value": "Robert"}]
Inserting at the beginning of a 100-item list? One operation, not 100 — thanks to ID-based matching.
Phoenix Streams
Streams work transparently as reactive arrays:
<.vue messages={@streams.messages} v-component="Chat" v-socket={@socket} />
<script setup>
// messages is a reactive array — streams handled automatically
const props = defineProps<{ messages: Message[] }>()
</script>
Developer Experience
- One-command install:
mix igniter.install live_vue - No JS build step — TypeScript source exported directly, Vite handles it
- VS Code extension for
~VUEsigil syntax highlighting
Breaking Changes
shared_propsremoved — pass props explicitlynillify_not_loaded→nilify_not_loaded(typo fix)
Links
- Interactive examples: livevue.skalecki.dev
- Blog post: The Story of LiveVue
- Hex: hex.pm/packages/live_vue
- Docs: hexdocs.pm/live_vue
- GitHub: github.com/Valian/live_vue
- ElixirConf EU talk: Why mixing LiveView and a frontend framework is a great idea
Discuss and upvote also on Hacker News.
Happy New Year!






















