LiveSvelte

repository·master·Indexed 23 days ago

https://github.com/woutdp/live_svelte

A bridge between Phoenix LiveView and Svelte that enables end-to-end reactivity by integrating Svelte components inside LiveView. It supports Server-Side Rendering (SSR), Svelte 5, and TypeScript, allowing developers to manage complex client-side state and UI while maintaining real-time, server-driven communication over websockets. Key features include support for Phoenix streams, Ecto changeset validation via useLiveForm(), and integration with phoenix_vite.

Tokens
31.2K
Snippets
101
Records
147
Agent score
81%

What's inside live_svelte

  1. Overview of LiveSvelte Demo Features

    master

    The example project demonstrates several key integration patterns between Svelte 5 and Phoenix LiveView:

    Core Integration

    • Struct Props: Passing Elixir structs as props (requires @derive Jason.Encoder on the struct).
    • Sigil Usage: Using the ~V sigil for inline Svelte templates.
    • Lodash: Integrating npm packages directly within Svelte components.

    Real-Time & Data

    • Streams: Using Phoenix stream() for efficient list updates.
    • Diffing: Utilizing Props Diff (JSON Patch) and ID List Diff for minimal updates.
    • PubSub: Real-time updates via pushEvent and PubSub.

    Composables (Hooks)

    • useLiveForm(): Integration with Ecto changeset validation.
    • useLiveUpload(): Handling file uploads with progress and validation.
    • useLiveNavigation(): Managing patch/navigate actions.
    • useLiveSvelte(): Enabling pushEvent within component trees.
    • useEventReply(): Implementing request-response patterns.
  2. Overview of LiveSvelte

    master

    LiveSvelte enables seamless end-to-end reactivity by integrating Svelte components inside Phoenix LiveView. It allows developers to manage complex local state and leverage the JavaScript ecosystem (like Svelte animations and scoped CSS) while maintaining communication over the LiveView websocket.

    Key features include:

    • End-To-End Reactivity: Syncs state between Svelte and LiveView via websockets.
    • Server-Side Rendering (SSR): Supports SSR for Svelte components.
    • Svelte Preprocessing: Compatible with svelte-preprocess.
    • Tailwind Support: Built-in support for Tailwind CSS.
    • TypeScript: Client assets are written in TypeScript, and the public Elixir API provides type-safe definitions.
    • Interoperability: Supports 'Dead View' and slot interoperability.
  3. Use composables to avoid prop drilling

    master

    If you need to interact with the LiveView from a deeply nested component, you can use composables instead of passing the live prop through every layer. This avoids 'prop drilling'.

    • useLiveSvelte(): Provides access to pushEvent and other core LiveSvelte functionality.
    • useLiveEvent(event, callback): Subscribes to server-sent events from anywhere in the component tree.
    <script>
      import { useLiveSvelte, useLiveEvent } from "live_svelte"
    
      const { pushEvent } = useLiveSvelte()
    
      useLiveEvent("flash", ({ message }) => {
        alert(message)
      })
    
      function save(data) {
        pushEvent("save", data)
      }
    </script>
  4. How ID-Based Diffing works in LiveSvelte

    master

    LiveSvelte uses ID-based list diffing (Tier 3 of the props diffing system) for arrays where items contain an :id field.

    Because Phoenix Streams automatically adds a __dom_id to every item, this diffing is active by default. This enables high-performance updates such as:

    • Inserting at position 0 sends a single upsert operation instead of $N$ replace operations.
    • Reordering items sends minimal operations.
    • Updates remain efficient regardless of the total list size.
  5. Manage component identity in loops

    master

    When rendering multiple instances of the same Svelte component (e.g., in a loop), LiveSvelte needs stable DOM IDs to reconcile elements correctly. Use the following priority order for identity:

    1. Explicit id: Pass id="my-id" to the component.
    2. Explicit key: Pass key={index} to the component. The ID becomes ComponentName-<key>.
    3. Auto-detected identity: If props contains :id, :key, :index, or :idx (or string equivalents), LiveSvelte uses that value.
    4. Counter fallback: Sequential IDs (Name, Name-1, etc.). Note: This is unreliable inside comprehensions; always use one of the first three methods for loops.
    <%!-- Using index in props for auto-generated IDs --%
    <%= for {item, index} <- Enum.with_index(@list) do %>
      <.svelte name="Card" props={%{index: index, color: @color}} />
    <% end %>
    
    <%!-- Using explicit key attribute --%
    <%= for {item, index} <- Enum.with_index(@list) do %>
      <.svelte name="Chart" key={index} props={%{data: item.data}} />
    <% end %>
  6. How LiveSvelte architecture works

    master

    LiveSvelte enables end-to-end reactivity between Phoenix LiveView and Svelte 5 by bridging the server and client through a three-layer architecture:

    1. LiveView (Elixir): The server renders an HTML wrapper div containing JSON-encoded props in a data-props attribute. State is managed on the server.
    2. SvelteHook (Phoenix hook): A JavaScript hook that mounts and updates Svelte components. It reads the data-props attribute on mount and applies patches when the server sends updates.
    3. Svelte 5 component: The client-side component that receives props via $props() and re-renders reactively using Svelte 5 runes (like $state() or $derived()) whenever updates are received.

    User interactions in Svelte components are pushed back to the LiveView process using pushEvent, while server state flows down as reactive props.

    LiveView (Elixir)     →   SvelteHook (Phoenix hook)   →   Svelte 5 (component)
    server assigns              reads data attrs               reactive props
    handle_event/3              pushEvent/handleEvent          $props(), $state()
  7. When to use LiveSvelte

    master

    Use LiveSvelte when:

    • You need rich, interactive UI components that require real-time synchronization with server state.
    • You want to leverage Svelte 5's reactive primitives (runes, snippets, $derived()) alongside LiveView's server-side logic.
    • You want to gradually adopt Svelte by mixing <.svelte> components into existing HEEX templates.

    Stick to plain LiveView when:

    • UI interactions map naturally to standard LiveView events without needing complex local component state.
    • You do not require Svelte-specific features like snippets or advanced reactive derivations.
  8. Important: Use Svelte 5 syntax

    master

    LiveSvelte requires Svelte 5 runes syntax. Do not use Svelte 4 patterns.

    Feature❌ Svelte 4 (Avoid)✅ Svelte 5 (Use)
    Propsexport let countlet { count } = $props()
    Reactivitylet x = 0let x = $state(0)
    Derived State$: doubled = x * 2let doubled = $derived(x * 2)
    Module Code<script context="module">Use .js files
  9. Understand LiveSvelte 'Secret State' Caveats

    master
    Because LiveSvelte communicates via JSON sent to Svelte, the Svelte client-side code contains the logic for all conditional rendering branches. Even if a LiveView conditional is currently false, the Svelte component code for the true branch is present in the client's browser. This means sensitive logic or data structures that should only exist on the server might be visible in the client-side JavaScript bundle. Always ensure that sensitive data is filtered out in the Phoenix/LiveView layer before being passed to LiveSvelte.
  10. Create a Phoenix app using phx.new and manual LiveSvelte installation

    master

    If you want to avoid using igniter.new entirely, you can use the standard phx.new command and then manually add LiveSvelte via Igniter.

    1. Create the app:
      cd live_svelte
      mix phx.new my_app --no-ecto
    2. Configure mix.exs: Add the following to your deps/0 list:
      {:igniter, "~> 0.6", only: [:dev, :test]},
      {:phoenix_vite, "~> 0.4"},
      {:live_svelte, path: ".."}
    3. Install and Setup:
      cd my_app
      mix deps.get
      mix igniter.install live_svelte
    4. Configure package.json: Set live_svelte to "file:.." and Phoenix dependencies to "file:./deps/...".
    5. Run:
      mix assets.setup
      mix phx.server
    mix phx.new my_app --no-ecto
  11. Install LiveSvelte via Igniter (Recommended)

    master

    The recommended installation method uses the Igniter installer. This automates the configuration of phoenix_vite, Vite, app.js, HTML helpers, SSR, and the layout with PhoenixVite.Components.assets.

    For a New Project

    Requires Phoenix 1.8+ and Node.js 19+ (or Bun).

    mix archive.install hex igniter_new
    mix igniter.new my_app --with phx.new --install live_svelte
    cd my_app
    mix setup
    mix phx.server

    For an Existing Project

    1. Add {:igniter, "~> 0.6"} to your mix.exs dependencies.
    2. Run the installer:
    mix deps.get
    mix igniter.install live_svelte
    1. Install npm packages and build assets:
    mix assets.setup     # Installs npm assets via phoenix_vite
    mix assets.build     # Builds Vite client + SSR
    1. Start the server:
    mix phx.server

    Using Bun

    To use Bun instead of npm/npx, append the --bun flag:

    • Existing project: mix igniter.install live_svelte --bun
    • New project: mix igniter.new my_app --with phx.new --install live_svelte --bun
  12. Configure Vite and Assets for LiveSvelte

    master

    For manual installations, you must configure the frontend build system to work with LiveSvelte and Phoenix.

    1. Create package.json (Project Root)

    Create this in your project root (not in assets/). If using Tailwind, include the @tailwindcss/vite and tailwindcss packages.

    {
      "type": "module",
      "dependencies": {
        "live_svelte": "file:./deps/live_svelte",
        "phoenix": "file:./deps/phoenix",
        "phoenix_html": "file:./deps/phoenix_html",
        "phoenix_live_view": "file:./deps/phoenix_live_view",
        "topbar": "^3.0.0"
      },
      "devDependencies": {
        "@sveltejs/vite-plugin-svelte": "^7.0.0",
        "phoenix_vite": "file:./deps/phoenix_vite",
        "svelte": "^5.0.0",
        "vite": "^8.0.0",
        "@tailwindcss/vite": "^4.1.0",
        "tailwindcss": "^4.1.0"
      }
    }

    2. Create assets/vite.config.mjs

    This configuration handles the Svelte plugin, the LiveSvelte Vite plugin, and Tailwind (if used).

    import { defineConfig } from "vite"
    import { svelte } from "@sveltejs/vite-plugin-svelte"
    import liveSveltePlugin from "live_svelte/vitePlugin"
    import tailwindcss from "@tailwindcss/vite"
    
    export default defineConfig(({ isSsrBuild }) => ({
      server: {
        host: "127.0.0.1",
        port: 5173,
        strictPort: true,
        cors: { origin: "http://localhost:4000" },
      },
      optimizeDeps: {
        include: ["live_svelte", "phoenix", "phoenix_html", "phoenix_live_view"],
      },
      ssr: { noExternal: process.env.NODE_ENV === "production" ? true : undefined },
      build: {
        manifest: false,
        ssrManifest: false,
        rollupOptions: {
          input: ["js/app.js", "css/app.css"],
          output: isSsrBuild ? { entryFileNames: "[name].mjs" } : undefined,
        },
        outDir: "../priv/static",
        emptyOutDir: true,
      },
      resolve: {
        alias: {
          "phoenix-colocated": `${process.env.MIX_BUILD_PATH}/phoenix-colocated`,
        },
      },
      plugins: [
        tailwindcss(),
        svelte({ compilerOptions: { css: "injected" } }),
        liveSveltePlugin({ entrypoint: "./js/server.mjs" }),
      ],
    }))

    3. Create assets/js/server.mjs

    This file is required for SSR:

    import { getRender } from "live_svelte"
    import Components from "virtual:live-svelte-components"
    export const render = getRender(Components)

    4. Update assets/js/app.js

    Import the LiveSvelte hooks and merge them into your LiveSocket:

    import {getHooks} from "live_svelte"
    import Components from "virtual:live-svelte-components"
    
    const liveSocket = new LiveSocket("/live", Socket, {
      hooks: {...colocatedHooks, ...getHooks(Components)},
    })

    5. Update Layout and CSS

    • Layout: In lib/<app_name>_web/components/layouts/root.html.heex, use PhoenixVite.Components.assets to load your JS and CSS.
    • CSS: If using Tailwind v4, ensure your assets/css/app.css includes the Svelte source glob:
    @import "tailwindcss";
    @source "../svelte/**/*.svelte";