LiveVue

repository·main·Indexed 20 days ago

https://github.com/valian/live_vue

A library providing end-to-end reactivity between Phoenix LiveView and Vue.js. It enables hybrid applications where Vue components coexist with server-side Phoenix logic, supporting props synchronization, event handling, and SSR. Features include a Vite plugin for HMR of Elixir files, specialized hooks for forms and navigation (useLiveForm, useLiveNavigation), and a VS Code extension for ~VUE sigil syntax highlighting.

Tokens
50.7K
Snippets
160
Records
197
Agent score
69%

What's inside live_vue

  1. Difference between Live Views and Dead Views in LiveVue

    main

    LiveVue components can be used in two different Phoenix contexts:

    1. Live Views: Full reactivity enabled via WebSockets. Components receive real-time updates.
    2. Dead Views: Static rendering. Components are rendered once (e.g., during SSR) and do not receive WebSocket updates. No manual v-socket configuration is required for Dead Views.
  2. Pass data to Vue components via props

    main

    The LiveView should act as the source of truth. Always pass necessary data from the LiveView to the Vue component as props. Do not attempt to fetch data inside the Vue component using fetch or similar client-side methods; this breaks the LiveView state model.

    <.vue
      v-component="ShoppingCart"
      cartItems={@cart_items}
      cartTotal={@cart_total}
      currency={@currency}
    />
  3. How SSR works in LiveVue

    main

    LiveVue uses Server-Side Rendering (SSR) to improve SEO and initial page load performance.

    The Lifecycle of SSR

    1. Dead Renders (Initial Load): When a user first requests a page via HTTP (before a WebSocket connection is established), LiveVue performs a "dead render". It renders the Vue component to HTML on the server so the user sees content immediately.
    2. Hydration: Once the LiveView WebSocket connects, the Vue components are hydrated on the client.
    3. Live Updates: During subsequent LiveView updates or navigation, SSR is skipped. Updates are sent via WebSockets and applied directly to the already-mounted Vue instances. This avoids the overhead of re-running SSR for every interaction.

    Per-Component Control You can override the global SSR setting for specific components using the v-ssr prop:

    • <.vue v-component="CriticalContent" v-ssr={true} />: Forces SSR.
    • <.vue v-component="InteractiveChart" v-ssr={false} />: Disables SSR (useful for client-only widgets).
    • <.vue v-component="RegularComponent" />: Uses the global default.
  4. Use slots in Vue components from HEEX

    main

    Vue components can receive slots from LiveView templates. You can pass standard HEEX content or Phoenix components into these slots.

    Warning: Slots are rendered server-side as raw HTML. Because they exist outside the LiveView lifecycle, hooks inside slots are not supported. You cannot directly nest .vue components inside regular slots.

    <.vue v-component="Card" title="Example Card">
      <p>This is the default slot content!</p>
      <:footer>
        This is a named slot
      </:footer>
    </.vue>
  5. Implement complex nested forms with LiveVue

    main

    LiveVue supports advanced form structures including:

    • Nested objects: Access fields using dot notation (e.g., owner.name, owner.email).
    • Arrays of objects: Manage collections of items (e.g., team_members[]).
    • Deeply nested arrays: Handle complex hierarchies (e.g., tasks[].assignees[]).
    • Dynamic operations: Add, remove, or reorder fields within arrays.
    • Complex validation: Manage validation logic across nested and dynamic structures.

    For specific implementation patterns for these features, refer to the detailed Client-Side API documentation or interactive examples at livevue.skalecki.dev.

  6. Understand the AsyncResult type

    main

    The AsyncResult<T> type represents the state of an asynchronous operation (like assign_async, stream_async, or start_async) from Phoenix LiveView. It is fully typed for use in Vue components.

    Fields:

    • ok (boolean): Indicates if the operation has completed successfully at least once.
    • loading (string[] | null): A list of loading keys (from assign_async) or null when not loading.
    • failed (any | null): The error state, unwrapped from Elixir error tuples for JSON compatibility, or null if no error.
    • result (T | null): The successful result data of type T, or null if not yet loaded.
    import type { AsyncResult } from 'live_vue'
    
    interface Props {
      userResult: AsyncResult<User>
    }
    
    const props = defineProps<Props>()
    
    // Check if data is available
    if (props.userResult.ok && props.userResult.result) {
      console.log('User:', props.userResult.result.name)
    }
    
    // Handle loading states
    if (props.userResult.loading) {
      console.log('Loading keys:', props.userResult.loading)
    }
    
    // Handle errors
    if (props.userResult.failed) {
      console.error('Failed:', props.userResult.failed)
    }
  7. How slots work in LiveVue

    main

    Vue components can receive content from LiveView templates using standard HEEX slot syntax.

    Basic and Named Slots

    • Default Slot: Content inside the <.vue> tag is passed to the <slot /> in the Vue template.
    • Named Slots: Use <:slot_name> in HEEX to target <slot name="slot_name" /> in the Vue template.

    Limitations

    • Each slot is wrapped in a div element.
    • HEEX slots are rendered server-side; they cannot directly contain Vue components (use v-inject instead).
    • Phoenix hooks do not work inside slots.
    • Slots remain reactive and update when their content changes.
    <.vue v-component="Modal">
      <:header>
        <h2>Modal Title</h2>
      </:header>
    
      <p>Modal content goes here</p>
    
      <:footer>
        <button>Cancel</button>
      </:footer>
    </.vue>
  8. Compare persistent layout patterns

    main

    Choose a pattern based on whether your layout needs server-side reactivity and how much control you want pages to have over their own layout.

    PatternUse WhenMain BenefitMain Limitation
    Root layout with v-injectThe layout is mostly client-side UIOne Vue layout app survives navigationRoot layout props are not socket-reactive
    Sticky LiveView layout with v-injectThe layout needs server-backed state or eventsPersistent backend process and persistent Vue layout appMore moving parts
    Headless sticky layout statePages render their own layout but need shared persistent propsGlobal reactive props across navigationTop-level page Vue apps still remount
  9. Optimize data transmission with LiveVue.Encoder

    main

    LiveVue minimizes WebSocket payload sizes by tracking only modified props, slots, and handlers. For complex data structures, you can implement the LiveVue.Encoder protocol to enable efficient diffing. By converting structs to consistent map representations, LiveVue can calculate minimal JSON patches (using Jsonpatch) instead of re-sending entire objects.

    When a field changes, LiveVue sends a JSON patch operation (e.g., replace) rather than the full struct, which reduces payload size and improves client-side rendering performance.

    # For complex types, use Jsonpatch to find minimal diff
    old_value ->
      old_value
      |> Encoder.encode()
      |> Jsonpatch.diff(new_value)
      |> update_in([Access.all(), :path], fn path -> "/#{k}#{path}" end)

    Example of a patch instead of a full struct:

    Instead of sending the entire user struct:

    %{user: %{name: "John", email: "new@example.com", created_at: ~U[...]}}

    LiveVue sends only the changed field:

    [%{op: "replace", path: "/user/email", value: "new@example.com"}]

  10. When to choose LiveVue

    main

    Choose LiveVue if:

    • Your team has expertise in Vue.js.
    • You want a balance of simplicity and power.
    • You prefer Vue's template syntax and the Composition API.
    • You need excellent documentation, examples, and strong TypeScript support via Vite.
    • You require features like component shortcuts, slot support, and optional SSR.
  11. Manage array fields with fieldArray

    main

    To handle arrays in a form, use form.fieldArray(path). This returns a FormFieldArray which provides methods for manipulating the collection and accessing its elements.

    Basic Operations

    • add(value): Adds a new item to the array.
    • remove(index): Removes the item at the specified index.
    • move(from, to): Reorders items by moving an item from one index to another.

    Accessing Array Items

    You can access individual items in an array using:

    1. Numeric index: array.field(0)
    2. Bracket notation: array.field('[1]')
    3. Iteration: Accessing array.fields.value to loop through all fields in a template.
    type TagsForm = {
      tags: string[]
    }
    
    const form = useLiveForm<TagsForm>(/* ... */)
    const tagsArray = form.fieldArray('tags')
    
    // Operations
    tagsArray.add('')
    tagsArray.remove(0)
    tagsArray.move(0, 1)
    
    // Accessing items
    const firstTag = tagsArray.field(0)
    const secondTag = tagsArray.field('[1]')
  12. How LiveVue handles data flow and state

    main

    LiveVue operates on a hybrid state model:

    • Props Flow: LiveView acts as the source of truth, sending data to Vue components via props.
    • Event Handling: Vue components communicate back to the server by emitting Phoenix LiveView events using phx-click and passing extra data via :phx-value-<name> attributes.
    • State Split:
      • Application State: Managed by LiveView (e.g., a database record or a global counter).
      • Local UI State: Managed by Vue (e.g., form input values, slider positions, or toggle states) to avoid unnecessary server round-trips.
    • Transitions: Client-side animations are handled entirely by Vue's transition system based on prop changes.