Jskalc

Jskalc

LiveVue - seamless integration of Vue with Phoenix LiveView

Hi! Today, after a couple weeks of development I’ve released v0.1 of LiveVue.

It’s a seamless integration of Vue and Phoenix LiveView, introducing E2E reactivity of server and client-side state.

Started as a fork of LiveSvelte, evolved to use Vite and a slightly different syntax.

Why you might want to use it?

  • Your client-side state grows and it’s hard to deal with it
  • You misses declarative rendering on the client side
  • You’d like to use a vast ecosystem of Vue libraries
  • You’d like to introduce animations
  • You like Vue :heart_eyes:

Why I created it, if LiveSvelte already exists?

  • I love Vue and was missing it’s DX and ecosystem (VueUse is amazing)
  • More options are always better (right? :sweat_smile:)
  • Vite gives you best-in-class stateful-hot-reload, and paired with stateful-hot reload on the server you have a stateful hot reload across the whole stack (which is HUGE!)

Features:

  • E2E reactivity
  • Support for phx-* attributes inside Vue components
  • Uses Vite for an amazing DX
  • Server Side Rendering (optional)
  • Vue/Phoenix slots interop
  • Vue event handlers can be defined with JS module
  • ~V sigil for inline Vue component definition

Plans for the future:

  • On-demand lazy-loading of components
  • Optimised payload with LiveJson or similar
  • Better tests
  • Dedicated page with examples
  • Guide of handling changesets & forms
  • Pinia support? :thinking:

An example

defmodule LiveVueExamplesWeb.LiveCounter do
  use LiveVueExamplesWeb, :live_view

  def render(assigns) do
    ~H"""
    <.vue
      count={@count}
      v-component="Counter"
      v-socket={@socket}
      v-on:inc={JS.push("inc")}
    />
    """
  end

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

  def handle_event("inc", %{"value" => diff}, socket) do
    {:noreply, update(socket, :count, &(&1 + diff))}
  end
end
<script setup lang="ts">
import {ref} from "vue"
const props = defineProps<{count: number}>()
const emit = defineEmits<{inc: [{value: number}]}>()
const diff = ref<string>("1")
</script>

<template>
    Current count
    <div class="text-2xl text-bold">{{ props.count }}</div>
    <label class="block mt-8">Diff: </label>
    <input v-model="diff" class="my-4" type="range" min="1" max="10" />

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

You can read some additional details in my short twitter thread.

I’d greatly appreciate a star on the github repo, and giving me any feedback if everything is working :wink:

Most Liked

Jskalc

Jskalc

LiveVue v0.6.0 was released!

live_vue_logo_rounded

Hey everyone! :waving_hand:

I’m excited to announce the release of LiveVue v0.6.0 - this is by far the biggest update since the initial release, with many months of development packed into it. If you’ve been waiting for LiveVue to mature, this is the release that takes it to the next level! :rocket:

What’s New?

:high_speed_train: Massive Performance Boost with JSON Patch Diffs

LiveVue now uses JSON Patch operations to send only the minimal differences when props change. Instead of sending entire prop objects over the WebSocket, only the specific fields that changed are transmitted. This dramatically reduces payload sizes (in many cases over 90%), especially for complex nested structures and lists. Performance of diffing is carefully optimized to be a non-issue.

It works seamlessly with custom structs through the LiveVue.Encoder protocol, designed to be compatible with Jason.Encoder. You can easily implement custom encoders for your own types - for example, here’s how we hide sensitive fields from User struct:

defimpl LiveVue.Encoder, for: MyApp.Accounts.User do
  def encode(%MyApp.Accounts.User{} = user, _opts) do
    user 
    |> Map.take([:first_name, :last_name]) 
    |> LiveVue.Encoder.encode(opts)
  end
end

Pulling it of required changes to LiveView and multiple PRs to Jsonpatch library.

:books: Complete Documentation Overhaul

The docs have been completely rewritten with comprehensive guides, API reference, and examples. Much easier to get started and go deeper!

:toolbox: Client-side utilities

  • useLiveNavigation - programmatic navigation with live_patch and live_redirect
  • useLiveEvent - simplified server-pushed event handling with automatic lifecycle management
  • <Link> component - Vue component for LiveView navigation
  • $live available globally in Vue templates - you can now write @click="$live.pushEvent('click')"

:blue_heart: TypeScript by Default

The client-side setup is now TypeScript out of the box, with full type safety and autocompletion. The $live property is now automatically available in all Vue templates and fully typed.

:test_tube: Testing Utilities

New LiveVue.Test module provides helpers to inspect Vue component configuration within your LiveView tests.

Migration

If you have an existing setup, there’s a small migration to move from assets/vue/index.js to assets/vue/index.ts and use the new TypeScript setup. The migration guide in the changelog has all the details - it’s straightforward!

What’s Next?

This release really solidifies LiveVue as a mature solution for Vue + LiveView integration. The performance improvements and developer experience enhancements should make it even more enjoyable to work with.

As always, I’d love to hear your feedback and experiences with the new release! And if you find LiveVue useful, a :star: on the GitHub repo would be much appreciated :blush:


Full Release Notes

Click to expand

:sparkles: Features and Improvements

  • JSON Patch Diffs for Props: LiveVue now uses JSON Patch operations to send only the minimal differences when props change, dramatically reducing WebSocket payload sizes. Instead of sending entire prop objects, only the specific fields that changed are transmitted using RFC 6902 JSON Patch format. This optimization works seamlessly with complex nested structures, lists, and custom structs through the LiveVue.Encoder protocol. It’s possible to skip diffs by setting v-diff to false on the component or by setting config :live_vue, enable_props_diff: false in your config. (#60)
  • New useLiveNavigation Composable: A new useLiveNavigation composable has been added for programmatic navigation, mirroring the functionality of live_patch and live_redirect. (#59).
  • New useLiveEvent Composable: A new useLiveEvent composable has been added to simplify listening to server-pushed events. It automatically manages event listener lifecycle, reducing boilerplate and preventing memory leaks (#58).
  • New Link Component: A new <Link> Vue component has been added to simplify live_patch and live_redirect navigation within your Vue components. (#47).
  • TypeScript by Default: The client-side entrypoint at assets/vue/index.ts is now a TypeScript file by default, improving type safety and the development experience out of the box.
  • $live Template Property: The LiveView hook instance is now automatically available in all Vue templates as the $live property, providing a convenient alternative to useLiveVue(). The property is now also fully typed, providing autocompletion and type checking in your editor. (#56).
  • Documentation Overhaul: The documentation has been completely rewritten and expanded. It now includes comprehensive guides on architecture, basic and advanced usage, a full client-side API reference, a component reference, and much more. (#49)
  • Testing Utilities: The new LiveVue.Test module provides helpers to inspect Vue component configuration (props, slots, event handlers) within your LiveView tests, making it easier to write assertions. (#46)
  • GitHub CI: A new GitHub Actions workflow has been added to run tests automatically.

:up_arrow: Migration Guide

This version transitions the default client-side setup to TypeScript and renames ~V sigil to ~VUE. If you have an existing assets/vue/index.js, follow these steps to upgrade:

  1. Rename and replace index.js:

    • Delete your existing assets/vue/index.js.
    • Create a new file at assets/vue/index.ts with the following content:
    // polyfill recommended by Vite https://vitejs.dev/config/build-options#build-modulepreload
    import "vite/modulepreload-polyfill"
    import { Component, h } from "vue"
    import { createLiveVue, findComponent, type LiveHook, type ComponentMap } from "live_vue"
    
    // needed to make $live available in the Vue component
    declare module "vue" {
      interface ComponentCustomProperties {
        $live: LiveHook
      }
    }
    
    export default createLiveVue({
      // name will be passed as-is in v-component of the .vue HEEX component
      resolve: name => {
        // we're importing from ../../lib to allow collocating Vue files with LiveView files
        // eager: true disables lazy loading - all these components will be part of the app.js bundle
        // more: https://vite.dev/guide/features.html#glob-import
        const components = {
          ...import.meta.glob("./**/*.vue", { eager: true }),
          ...import.meta.glob("../../lib/**/*.vue", { eager: true }),
        } as ComponentMap
    
        // finds component by name or path suffix and gives a nice error message.
        // `path/to/component/index.vue` can be found as `path/to/component` or simply `component`
        // `path/to/Component.vue` can be found as `path/to/Component` or simply `Component`
        return findComponent(components as ComponentMap, name)
      },
      // it's a default implementation of creating and mounting vue app, you can easily extend it to add your own plugins, directives etc.
      setup: ({ createApp, component, props, slots, plugin, el }) => {
        const app = createApp({ render: () => h(component as Component, props, slots) })
        app.use(plugin)
        // add your own plugins here
        // app.use(pinia)
        app.mount(el)
        return app
      },
    })
    
  2. Update tsconfig.json:
    Add vue/index.ts to the include array in your assets/tsconfig.json file.

:bug: Bug Fixes

  • SSR Attribute Rendering: Fixed a bug where the data-ssr attribute was not being rendered correctly as a boolean true or false in the final HTML.

Housekeeping

  • Optional Floki Dependency: floki is now an optional dependency, only required if you use the new testing utilities.
  • Dependency Updates: NPM dependencies have been updated to address security vulnerabilities.
  • Dropped Elixir 1.12 Support: Support for Elixir 1.12 has been removed to align with Phoenix LiveView’s supported versions.
katafrakt

katafrakt

I seem to be in a minority that thinks that this name is fine. We have LiveSvelte, I’m sure we will soon have LiveReact, LiveSolid, LiveQwik and whatnot. It will hurt the discoverability of this one project and the phonetic similarity is less of a problem when the distinction in written text is clear.

Jskalc

Jskalc

A small update about the project. It’s still growing, both when it comes to adoption and features :heart_eyes:

Recently I’ve added a roadmap to the readme of the project. I’m posting it here as well:

Roadmap :bullseye:

  • Add a default handler for Vue emits to eg. automatically push them to the server without explicit v-on handlers.
  • try to use Igniter as a way of installing LiveVue in a project
  • usePushEvent - an utility similar to useFetch making it easy to get data from &handle_event/3 -> {:reply, data, socket} responses
  • useLiveForm - an utility to efforlessly use Ecto changesets & server-side validation, similar to HEEX
  • useEventHandler - an utility automatically attaching & detaching handleEvent
  • optimize payload - send only json_patch diffs of updated props
  • VS code extension higlighting ~V sigil

useLiveForm

I’m working currently on the early version of useLiveForm utility. I’ve posted more details on Twitter, you can find it here.

Jskalc

Jskalc

Me again! Just finished a big internal cleanup & migration to TS, fixed some bugs along the way and prepared for further development.

Excerpt from the changelog:

0.5.1 - 2024-10-08

Fixed

  • Fixed a bug where the server was not preloading the correct assets for the Vue components. It happened because CursorAI “skipped” important part of the code when migrating to the TypeScript :sweat_smile:

0.5.0 - 2024-10-08

Changed

  • Migrated the project to TypeScript :purple_heart: #32
  • Added createLiveVue entrypoint to make it easier to customize Vue app initialization

Deprecations

  • assets/vue/index.js should export app created by createLiveVue(), not just available components. See migration below.

Migration

In assets/js/app.js, instead of:

export default {
  ...import.meta.glob("./**/*.vue", { eager: true }),
  ...import.meta.glob("../../lib/**/*.vue", { eager: true }),
}

use:

// polyfill recommended by Vite https://vitejs.dev/config/build-options#build-modulepreload
import "vite/modulepreload-polyfill"
import { h } from "vue"
import { createLiveVue, findComponent } from "live_vue"

export default createLiveVue({
  resolve: name => {
    const components = {
      ...import.meta.glob("./**/*.vue", { eager: true }),
      ...import.meta.glob("../../lib/**/*.vue", { eager: true }),
    }

    // finds component by name or path suffix and gives a nice error message.
    // `path/to/component/index.vue` can be found as `path/to/component` or simply `component`
    // `path/to/Component.vue` can be found as `path/to/Component` or simply `Component`
    return findComponent(components, name)
  },
  setup: ({ createApp, component, props, slots, plugin, el }) => {
    const app = createApp({ render: () => h(component, props, slots) })
    app.use(plugin)
    app.mount(el)
    return app
  },
})

then, in assets/js/app.js, instead of:

import components from "./vue"

simply do

import { getHooks } from "live_vue"
import liveVueApp from "./vue"
// ...

const hooks = { ...getHooks(liveVueApp) }

If you had any custom initialization code, you have to move it to createLiveVue().setup() function.

Fixed

  • Nicely formatted JS error stracktraces during SSR commit
  • Previously initializeVueApp was not working in SSR mode, since it was declared in app.js which couldn’t be imported by server bundle. Now it’s in a separate file as createLiveVue().setup() and can be imported by both client and server bundles.
Jskalc

Jskalc

LiveVue 0.7.0 - File Upload Composable & Comprehensive Testing Suite!

Hey everyone! :waving_hand:

I’m excited to announce the release of LiveVue v0.7.0! This release focuses on making file uploads seamless and establishing a robust testing foundation for the library. If you’ve been waiting for better file upload support or wondering about LiveVue’s reliability, this release addresses both! :rocket:

What’s New?

:file_folder: New useLiveUpload Composable

The biggest new feature in this release is the new useLiveUpload() composable that makes Phoenix LiveView file uploads feel native in Vue! Let’s take a look at how it works:

Backend (LiveView):

defmodule MyAppWeb.UploadLive do
  use MyAppWeb, :live_view

  def render(assigns) do
    ~H"""
    <.vue
      upload={@uploads.documents}
      uploaded_files={@uploaded_files}
      v-component="FileUploader"
      v-socket={@socket}
    />
    """
  end

  def mount(_params, _session, socket) do
    {:ok,
     socket
     |> assign(:uploaded_files, [])
     |> allow_upload(:documents,
       accept: ~w(.pdf .txt .jpg .png),
       max_entries: 3,
       auto_upload: true
     )}
  end

  def handle_event("validate", _params, socket), do: {:noreply, socket}

  def handle_event("save", _params, socket) do
    uploaded_files =
      consume_uploaded_entries(socket, :documents, fn %{path: path}, entry ->
        dest = Path.join("uploads", entry.client_name)
        File.cp!(path, dest)
        {:ok, %{name: entry.client_name, size: entry.client_size}}
      end)

    {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
  end
end

Frontend (Vue):

<template>
  <div>
    <button @click="showFilePicker">Select Files</button>
    <button v-if="entriesCount > 0" @click="cancel()">Cancel All</button>

    <div v-for="entry in entries" :key="entry.ref">
      <span>{{ entry.client_name }} ({{ entry.client_size }} bytes)</span>
      <span>{{ entry.progress || 0 }}%</span>
      <span>{{ entry.done ? 'done' : 'pending' }}</span>
      <button @click="cancel(entry.ref)">×</button>
    </div>

    <div v-if="uploadedFiles.length">
      <h3>Uploaded Files</h3>
      <div v-for="file in uploadedFiles" :key="file.name">
        {{ file.name }} ({{ file.size }} bytes)
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed } from "vue"
import { useLiveUpload, UploadConfig } from "live_vue"

interface Props {
  upload: UploadConfig
  uploadedFiles: { name: string; size: number }[]
}

const props = defineProps<Props>()

const { entries, showFilePicker, submit, cancel } = useLiveUpload(() => props.upload, {
  changeEvent: "validate",
  submitEvent: "save"
})

const entriesCount = computed(() => entries.value?.length || 0)
</script>

Key features:

  • Reactive upload state - Progress tracking, metadata, and validation status
  • Automatic DOM management - File inputs are handled seamlessly
  • Drag-and-drop support - Built-in drag-and-drop file handling
  • LiveView integration - Works perfectly with Phoenix’s upload validation and submission
  • Full TypeScript support - Complete type definitions for all upload configurations
  • Declarative API - Vue-friendly approach to complex upload workflows

:test_tube: Comprehensive Testing Foundation

This release establishes LiveVue as a thoroughly tested library with multiple testing layers:

Frontend Test Suite with Vitest:

  • 36 test cases covering JSON Patch functionality with edge cases
  • Tests for escaped characters, deep cloning, and complex patch sequences
  • jsdom environment for DOM testing
  • Full coverage of the TypeScript codebase

End-to-End Testing with Playwright:

  • 5 comprehensive test suites covering all major functionality
  • Tests for Vue component rendering, props passing, event emission
  • LiveView/Vue synchronization testing
  • File upload workflow testing
  • Navigation and prop diffing validation

Improved CI/CD:

  • E2E CI: Automatically runs E2E tests on every PR
  • Frontend CI: Tests across Node.js 20, 22, and 24 (LTS and current)
  • Elixir CI: Focused Elixir/OTP matrix testing

New Mix Commands:

mix assets.test          # Run frontend tests
npm test                 # Run tests once
npm run test:watch       # Run tests in watch mode
npm run test:ui          # Interactive test UI
npm run e2e:test         # E2E tests
npm run e2e:test:headed  # E2E with browser UI
npm run e2e:test:debug   # E2E debug mode

Why This Matters

File uploads have always been one of the trickier aspects of web development, especially when bridging server-side frameworks like Phoenix with client-side reactive frameworks. The useLiveUpload composable eliminates this complexity entirely, giving you a Vue-native way to handle uploads while leveraging all of Phoenix LiveView’s powerful upload features.

The comprehensive testing suite ensures LiveVue’s reliability and gives confidence for production use. With both unit tests for the TypeScript code and E2E tests covering real-world scenarios, you can trust that LiveVue will work as expected.

What’s Next?

With file uploads now seamlessly integrated and a solid testing foundation in place, LiveVue continues to mature as the go-to solution for Vue + Phoenix LiveView applications. The testing infrastructure also sets us up for more confident development of future features.

Next step: useLiveForm() composable for seamless form handling :purple_heart:

As always, I’d love to hear your feedback and experiences with the new release! And if you find LiveVue useful, a :star: on the GitHub repo would be much appreciated :blush:


Full Release Notes

:rocket: New Composable: useLiveUpload

  • File Upload Composable: Added useLiveUpload() composable for seamless Vue integration with Phoenix LiveView file uploads
    • Provides reactive upload state management with progress tracking and metadata
    • Automatic DOM element management for file inputs
    • Built-in support for drag-and-drop file handling
    • Integration with LiveView’s upload validation and submission system
    • TypeScript support with full type definitions for upload configurations
    • Simplifies complex upload workflows with a declarative Vue-friendly API

:test_tube: Testing Improvements

  • Frontend Test Suite: Added comprehensive test suite for frontend TypeScript code using Vitest framework
    • Tests for JSON Patch functionality with 36 test cases covering all operations
    • Test configuration with jsdom environment for DOM testing
    • Coverage includes edge cases like escaped characters, deep cloning, and complex patch sequences
  • End-to-End Test Suite: Added comprehensive E2E test suite using Playwright framework
    • Tests for Vue component rendering, props passing, event emission, and LiveView/Vue synchronization
    • 5 test suites covering basic functionality, navigation, events, prop diffs, and file uploads
    • Test server setup with dedicated Phoenix endpoint for isolated testing
    • Utilities for LiveView/Vue synchronization and server-side code execution
  • Separate CI Workflows: Split testing into dedicated workflows for better separation of concerns
    • Frontend CI: Tests TypeScript code across Node.js versions 20, 22, and 24 (LTS and current stable)
    • Elixir CI: Focused purely on Elixir/OTP matrix testing without redundant frontend test runs
  • New Mix Commands: Added mix assets.test alias for running frontend tests alongside existing npm scripts
    • npm test - Run tests once
    • npm run test:watch - Run tests in watch mode
    • npm run test:ui - Run tests with interactive UI
    • npm run e2e:test - Run E2E tests
    • npm run e2e:test:headed - Run E2E tests with browser UI
    • npm run e2e:test:debug - Run E2E tests in debug mode

Happy coding! :rocket:

Where Next?

Popular in Libraries Top

Qqwy
TypeCheck: Fast and flexible runtime type-checking for your Elixir projects. Core ideas Type- and function specifications are const...
336 13801 100
New
pkrawat1
Hey guyz We at @aviabird are working on a payment library in elixir/phoenix. We are targeting March 2018 to add 56 Gateways to it. Have...
New
woylie
I released Doggo, a collection of unstyled Phoenix components. Features Unstyled Phoenix components. Storybook that can be added to...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 49522 488
New
cjen07
parameterized pipe in elixir: |n&gt; edit: negative index in |n&gt; and mixed usage with |&gt; are supported example: use ParamP...
New
jakub-zawislak
Hi everyone, I’m coming from the Symfony (PHP) framework. I like Phoenix, but it has a one thing that was build much better in the Symfo...
New
danschultzer
In short Plug n’ play OAuth 2.0 provider library. Just set up a resource owner schema with Ecto (your user schema), install the dependen...
New
martinthenth
Hello everybody :wave: Recently, some of my colleagues talked about database ids and uuids and their problems, and I remembered the pain...
New
engineeringdept
I’ve just released the first version of Snap, an Elasticsearch client. It borrows ideas about application structure and process managemen...
New
KallDrexx
For a good number of months I've been working on creating a very basic RTMP live video streaming server. Now that I have a very, very ba...
New

Other popular topics Top

chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

Sub Categories:

We're in Beta

About us Mission Statement