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:

31 Likes

Version 0.2.0 has been released. Changelog entry:

0.2.0 - 2024-05-17

QoL release

Added

  • @ added to Vite & typescript paths. Points to assets, so it’s possible to import files from @/img/someimage.svg. To migrate, see assets/copy/tsconfig.json and assets/copy/vite.config.js
  • Added Vite types to tsconfig.json to support special imports, eg. svg. To migrate, add "types": ["vite/client"].
  • Added possibility to colocate Vue files in lib directory. To migrate, copy assets/copy/vue/index.js to your project.

Changed

  • Adjusted files hierarchy to match module names
  • Publishing with expublish
3 Likes

Version 0.3.1 has been released. Lazy loading components is now supported :exploding_head::tada: Your app.js doesn’t have to grow as your application grow! SSR renders these lazy components + preload hints, so it has a minimal impact on performance :wink:

How to do it? Simply import files lazily using Vite

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

in this example, Vue files colocated with elixir files will be loaded on-demand, while files in assets/vue will be always part of the app.js bundle. You can decide which files should be loaded on demand.

Another approach:

import component1 from './Component1.vue'
import component2 from './Component2.vue'
export default {
  Component1: component1,
  Component2: component2,
  Component3Lazy: () => import('./Component3.vue')
}

Changelog entries:

0.3.1 - 2024-05-17

Changed

  • Simplified assets/vue/index.js file - mapping filenames to keys is done by the library. Previous version should still work.

0.3.0 - 2024-05-17

CHANGED

  • removed esbuild from live_vue, package.json points directly to assets/js/live_vue
  • added support to lazy loading components. See more in README. To migrate, ensure all steps from installation are up-to-date.
5 Likes

Version 0.3.2 has been released. It included a fix to tailwind classes not being hot-updated correctly on phoenix change.

This allows to have an amazing DX: Hot code reload across the whole stack :heart_eyes:

Changelog:

0.3.2 - 2024-05-19

Fixed

  • Hot reload of CSS when updating Elixir files
3 Likes

You should rename the project to… anything that couldn’t be confused for LiveView.

3 Likes

I was already thinking about this.

  1. The name was a natural consequence of following LiveSvelte.
  2. In written english it’s perfect. Short, easy to comprehend and understand.
  3. In spoken english - yes, it’s not perfect. I realised it too late, after publishing the initial release and generating a bit of a buzz around it.

My idea to solve it - in speech it can be referred to as LiveVuejs. I added a section to the official FAQ. I’d prefer to avoid renaming package and all references to it.

Hopefully it won’t be a dealbreaker? :sweat_smile:

2 Likes

I totally get the part where it matches the LiveSvelte thing, it definitely follows logically. It’s just that it will, I 100% guarantee you, will lead to avoidable confusion.

When you have the choice of naming it literally anything else, why not make that choice, instead of stepping on the toes of what is possibly the Elixir community’s flagship project?

1 Like

VueLive then you can market it as a Phoenix extension to Vue instead of a Vue extension to Phoenix

1 Like

There is already a LiveViewJS which itself is confusing with the LiveView.JS module.

VueLive has almost the same problem, doesn’t it? I can imagine when speaking someone might reply “you meant LiveView?” and then you’d need to explain “yeah but I mean frontend framework Vue”

And also, it’s a Vue extension to Phoenix, not the other way around. Hard to market it that way.

Argh. Didn’t know that one. Do you think it’s well-known so renaming package from live_vue to live_vuejs will still cause confusion?

I agree. I might do it. Just, please help me to decide on name.

VueLive - I think it has the same problem
LiveVuejs - my personal favourite. That suffix should make it distinguishable and keep that nice “vibe”

some other reasonable options…? :thinking:

1 Like

VueLive is a step in the right direction, but I agree that it’s also confusing.

There’s no rush on picking a name. I find that if you give these things a bit of time, something will pop into your head when you’re doing the dishes or something like that.

Maybe something like LiveInterVue? As in “LiveView with Vue Interop”.

VueJsLive is also an option. A little awkward but gets the point across pretty well IMO. It’s Vue.js, but Live.

I’ll see if I can think of anything else. I don’t want to be the guy who only brings up problems and offers no solutions. But this one is tough because the names are like, the same. :sweat_smile:

1 Like

The clearest in intent so far

2 Likes

Agreed.

How about:

  • Livue
  • Liue
  • Lue

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.

12 Likes

Another thing to consider is the fact that this project started as a fork of LiveSvelte. I’m unsure how much it’s deviated since the fork, but if there is still a sizable chunk of the original code remaining you could team up with @woutdp and extract the core as a new library, LiveSPA, which is just an integration tool for JS frontends. Then in the docs you can have an Integrating with Svelte, Integrating with Vue, Integrating with … and drop the LiveFramework names altogether. I’m unsure if it’s possible but having one place in the docs to go to learn how to add any framework would be great as the docs are very scattered as is.

Elixir is pretty dynamic. You could have the real core in LiveSPA. The Vue specific changes you’ve made in LiveSPA.Vue and the Svelte specific config in LiveSPA.Svelte

2 Likes

I’d like to have a single interface for these kinds of libraries, but we’d have to make sure it’s the correct abstraction. LiveVue does some things different from LiveSvelte, for example it uses Vite instead of Esbuild. So the setup wouldn’t be the same.

I’m more in favor of manually converging the libraries. LiveSvelte can learn from LiveVue. For example, maybe it would be an idea of having both an Esbuild (which I picked since it’s closer to the documentation Elixir and the default setup you get with a Phoenix project) and a Vite setup which is a bit more modern as a build tool in the frontend world.

What I’m trying to say is that even though the libraries are a solution to the same problem, their implementation (and future other potential frameworks) might have completely different ways of solving things. Currently I don’t see how a shared library might make things easier for users and for maintainers. But a manual convergence of the libraries does make sense, and if enough frameworks get implemented and we see certain patterns that are always the same, then it might make sense to do it at that point.

2 Likes

esbuild is younger than LiveView and no longer considered “modern.” sigh :upside_down_face:

4 Likes