chore: improved docs. removed live json

This commit is contained in:
Denis Donici 2026-03-05 23:00:30 +02:00
parent a70774e138
commit 09d353e883
48 changed files with 3351 additions and 2110 deletions

View file

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Removed
- **live_json** — The `live_json` hex dependency and `live_json_props` feature have been removed from the library and example project. Use built-in props diffing and JSON Patch (see Configuration) for efficient prop updates instead.
## 0.17.4 - 2026-02-18
### Added

View file

@ -26,7 +26,6 @@ Svelte inside Phoenix LiveView with seamless end-to-end reactivity
- ⭐ **Svelte Preprocessing** Support with [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess)
- 🦄 **Tailwind** Support
- 💀 **Dead View** Support
- 🤏 **live_json** Support
- 🦥 **Slot Interoperability**
- 📘 **TypeScript** — client assets in TypeScript; public API is type-safe with exported type definitions for consumers
@ -734,42 +733,7 @@ end
Phoenix.HTML.Form, Ecto.Changeset, and Phoenix LiveView upload structs have built-in encoders. Date/Time types are encoded as ISO8601 strings.
### live_json
LiveSvelte has support for [live_json](https://github.com/Miserlou/live_json).
By default, LiveSvelte sends your entire json object over the wire through LiveView. This can be expensive if your json object is big and changes frequently.
`live_json` on the other hand allows you to only send a _diff_ of the json to Svelte. This is very useful the bigger your json objects get.
Counterintuitively, you don't always want to use `live_json`. Sometimes it's cheaper to send your entire object again. Although diffs are small, they do add a little bit of data to your json. So if your json is relatively small, I'd recommend not using `live_json`, but it's something to experiment with for your use-case.
#### Usage
1. Install [live_json](https://github.com/Miserlou/live_json#installation)
2. Use `live_json` in your project with LiveSvelte. For example:
```elixir
def render(assigns) do
~H"""
<.svelte name="Component" live_json_props={%{my_prop: @ljmy_prop}} socket={@socket} />
"""
end
def mount(_, _, socket) do
# Get `my_big_json_object` somehow
{:ok, LiveJson.initialize("my_prop", my_big_json_object)}
end
def handle_info(%Broadcast{event: "update", payload: my_big_json_object}, socket) do
{:noreply, LiveJson.push_patch(socket, "my_prop", my_big_json_object)}
end
```
#### Example
You can find an example [here](https://github.com/woutdp/live_svelte/blob/master/example_project/lib/example_web/live/live_json.ex).
For efficient updates when only some props change, use the built-in **props diffing** (see [Configuration](https://hexdocs.pm/live_svelte/configuration.html)) and JSON Patch support; the former `live_json` integration has been removed.
### Structs and Ecto

View file

@ -29,25 +29,9 @@ function getSlots(ref) {
return snippets
}
function getLiveJsonProps(ref) {
const json = getAttributeJson(ref, "data-live-json")
// On SSR, data-live-json is the full object we want
// After SSR, data-live-json is an array of keys, and we'll get the data from the window
if (!Array.isArray(json)) return json
const liveJsonData = {}
for (const liveJsonVariable of json) {
const data = window[liveJsonVariable]
if (data !== undefined) liveJsonData[liveJsonVariable] = data
}
return liveJsonData
}
function getProps(ref) {
return {
...getAttributeJson(ref, "data-props"),
...getLiveJsonProps(ref),
...getSlots(ref),
live: ref,
}
@ -89,9 +73,7 @@ function update_state(ref) {
else state[key] = payload[key]
}
}
// Always keep live ref, liveJson, and slots in sync
const liveJson = getLiveJsonProps(ref)
for (const key in liveJson) state[key] = liveJson[key]
// Always keep live ref and slots in sync
const slots = getSlots(ref)
for (const key in slots) state[key] = slots[key]
state.live = ref
@ -125,11 +107,6 @@ export function getHooks(components) {
const Component = components[componentName]
if (!Component) throw new Error(`Unable to find ${componentName} component.`)
for (const liveJsonElement of Object.keys(getAttributeJson(this, "data-live-json"))) {
window.addEventListener(`${liveJsonElement}_initialized`, (_event) => update_state(this), false)
window.addEventListener(`${liveJsonElement}_patched`, (_event) => update_state(this), false)
}
// Mount into the inner phx-update="ignore" div so LiveView's DOM
// patching won't destroy Svelte's rendered content on server updates.
const target = this.el.querySelector("[data-svelte-target]")

1759
assets/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,12 @@
{
"type": "module",
"devDependencies": {
"@vitest/coverage-v8": "^2.1.0",
"esbuild": "^0.24.0",
"esbuild-svelte": "^0.9.0",
"@vitest/coverage-v8": "^4.0.18",
"esbuild": "^0.27.3",
"esbuild-svelte": "^0.9.4",
"jsdom": "^28.1.0",
"typescript": "^5.6.0",
"vitest": "^2.1.0"
"typescript": "^5.9.3",
"vitest": "^4.0.18"
},
"dependencies": {
"phoenix": "file:../deps/phoenix",

View file

@ -1,37 +1,98 @@
# Example
# LiveSvelte Example Project
To start your Phoenix server:
A working Phoenix application demonstrating LiveSvelte features — Svelte 5 components
integrated with Phoenix LiveView.
- Run `mix setup` to install and setup dependencies
- Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
## Setup
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
1. Install Elixir + Node.js 19+
2. Run `mix setup` (installs deps + npm packages + creates DB)
3. Start server: `mix phx.server`
4. Visit: http://localhost:4000
## Demo Categories
### Basics
- **Hello World** (`/hello-world`) — Simplest component rendering
- **Struct Props** (`/live-struct`) — Passing Elixir structs as props (requires `@derive Jason.Encoder`)
- **Lodash** (`/lodash`) — Using npm packages in Svelte components
### Interactive
- **Counter** (`/live-simple-counter`) — Server state + client events
- **Plus/Minus (Live)** (`/live-plus-minus`) — LiveView event handling
- **Hybrid Counter** (`/live-plus-minus-hybrid`) — Mix of client and server events
- **Lights** (`/live-lights`) — Multiple components sharing LiveView state
- **Sigil** (`/live-sigil`) — Inline Svelte templates with the `~V` sigil
### Data & Real-Time
- **Streams** (`/streams`) — Phoenix `stream()` for efficient list updates
- **Props Diff** (`/live-props-diff`) — Only changed assigns sent on update (JSON Patch)
- **ID List Diff** (`/live-id-list-diff`) — ID-based list diffing for minimal ops
- **Chat** (`/live-chat`) — Real-time updates with PubSub + `pushEvent`
- **Log List** (`/live-log-list`) — Dynamic list updates
- **Breaking News** (`/live-breaking-news`) — Real-time ticker with `~V` sigil
### Slots
- **Simple Slots** (`/live-slots-simple`) — Basic slot usage
- **Dynamic Slots** (`/live-slots-dynamic`) — Named slots with dynamic content
### Composables
- **Form** (`/live-form`) — `useLiveForm()` with Ecto changeset validation
- **File Upload** (`/live-upload`) — `useLiveUpload()` with progress and validation
- **Navigation** (`/live-navigation`) — `useLiveNavigation()` for patch/navigate
- **Composition** (`/live-composition`) — `useLiveSvelte()` for pushEvent in component trees
- **Event Reply** (`/live-event-reply`) — `useEventReply()` for request-response
### Advanced
- **SSR Demo** (`/live-ssr`) — Server-side rendering with NodeJS (see SSR section below)
- **Client Loading** (`/live-client-side-loading`) — Loading slot shown until hydration
### Ecto
- **Notes OTP** (`/live-notes-otp`) — SQLite-backed notes with Ecto
## Testing
- Run all tests: `mix test` (by default this excludes E2E tests; use `mix test --only e2e` to run browser tests).
- Run only E2E (browser) tests: `mix test --only e2e`
E2E test modules must use `@moduletag :e2e`. E2E tests use [Wallaby](https://hexdocs.pm/wallaby) and serve the app from **built** assets (`priv/static`). After changing Svelte (or other frontend) code, rebuild before E2E so tests see your changes:
```bash
mix assets.js && mix test --only e2e
# Server-side tests (fast, no browser)
mix test --only phoenix_test
# Browser E2E tests (requires ChromeDriver)
mix test --only e2e
# All tests
mix test
```
Or use the alias (builds assets then runs tests; pass `--only e2e` to run only E2E):
**After changing JS/Svelte files**, rebuild before running tests:
```bash
mix test.e2e --only e2e
mix assets.js && mix test
```
You need **Chrome** and **ChromeDriver** on your `PATH` for E2E. If Chromedriver is not installed, run `mix test --exclude e2e` to run the rest of the suite.
E2E tests require Chrome + ChromeDriver in PATH. Install with your OS package manager.
See `live_svelte/CLAUDE.md` for detailed testing guidance.
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
## SSR (Server-Side Rendering)
SSR is disabled by default in development. To enable:
**config/dev.exs:**
```elixir
config :live_svelte, ssr: true, ssr_module: LiveSvelte.SSR.ViteJS
```
**For production**, use NodeJS SSR (already configured):
```elixir
config :live_svelte, ssr: true, ssr_module: LiveSvelte.SSR.NodeJS
```
Build the SSR bundle before SSR works:
```bash
mix assets.js && mix compile
```
The SSR demo page (`/live-ssr`) uses `ssr={true}` on the component. NodeJS must be available for SSR in production; in test env SSR is disabled globally via `config :live_svelte, ssr: false`.
## Learn more
- Official website: https://www.phoenixframework.org/
- Guides: https://hexdocs.pm/phoenix/overview.html
- Docs: https://hexdocs.pm/phoenix
- Forum: https://elixirforum.com/c/phoenix-forum
- Source: https://github.com/phoenixframework/phoenix
- LiveSvelte: https://github.com/woutdp/live_svelte
- Phoenix: https://hexdocs.pm/phoenix

View file

@ -6,8 +6,6 @@ import "phoenix_html"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"
// TODO(Epic 9): remove createLiveJsonHooks once live_json dependency is removed
import {createLiveJsonHooks} from "live_json"
import {getHooks} from "live_svelte"
import Components from "virtual:live-svelte-components"
@ -49,7 +47,6 @@ const PropsDiffPayloadDisplay = {
}
const Hooks = {
...createLiveJsonHooks(),
...getHooks(Components),
PropsDiffPayloadDisplay,
}

View file

@ -5,52 +5,47 @@
"packages": {
"": {
"dependencies": {
"live_json": "file:../deps/live_json",
"live_svelte": "file:../..",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/lodash": "^4.14.192",
"daisyui": "^5.0.0",
"dynamic-marquee": "^2.6.2",
"lodash": "^4.17.21",
"stylus": "^0.55.0",
"svelte": "^5.19.8",
"typescript": "^5.6.3",
"vite": "^6.0.0"
"svelte": "^5.53.7",
"typescript": "^5.9.3",
"vite": "^7.3.1"
}
},
"../..": {
"version": "0.17.4",
"license": "MIT",
"devDependencies": {
"prettier": "3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"svelte": "^5.1.13"
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.1",
"svelte": "^5.53.7"
}
},
"../deps/live_json": {
"version": "0.4.3",
"license": "MIT"
},
"../deps/phoenix": {
"version": "1.8.4",
"version": "1.8.5",
"license": "MIT",
"devDependencies": {
"@babel/cli": "7.28.6",
"@babel/core": "7.29.0",
"@babel/preset-env": "7.29.0",
"@eslint/js": "^9.28.0",
"@eslint/js": "^10.0.1",
"@stylistic/eslint-plugin": "^5.0.0",
"documentation": "^14.0.3",
"eslint": "9.39.2",
"eslint-plugin-jest": "29.12.1",
"eslint": "10.0.2",
"eslint-plugin-jest": "29.15.0",
"jest": "^30.0.0",
"jest-environment-jsdom": "^30.0.0",
"jsdom": "^27.0.0",
"jsdom": "^28.1.0",
"mock-socket": "^9.3.1"
}
},
@ -82,24 +77,10 @@
"phoenix": "1.7.21"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
"cpu": [
"ppc64"
],
@ -114,9 +95,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
"cpu": [
"arm"
],
@ -131,9 +112,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
"cpu": [
"arm64"
],
@ -148,9 +129,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
"cpu": [
"x64"
],
@ -165,9 +146,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
"cpu": [
"arm64"
],
@ -182,9 +163,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
"cpu": [
"x64"
],
@ -199,9 +180,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
"cpu": [
"arm64"
],
@ -216,9 +197,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
"cpu": [
"x64"
],
@ -233,9 +214,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
"cpu": [
"arm"
],
@ -250,9 +231,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
"cpu": [
"arm64"
],
@ -267,9 +248,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
"cpu": [
"ia32"
],
@ -284,9 +265,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
"cpu": [
"loong64"
],
@ -301,9 +282,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
"cpu": [
"mips64el"
],
@ -318,9 +299,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
"cpu": [
"ppc64"
],
@ -335,9 +316,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
"cpu": [
"riscv64"
],
@ -352,9 +333,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
"cpu": [
"s390x"
],
@ -369,9 +350,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
"cpu": [
"x64"
],
@ -386,9 +367,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
"cpu": [
"arm64"
],
@ -403,9 +384,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
"cpu": [
"x64"
],
@ -420,9 +401,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
"cpu": [
"arm64"
],
@ -437,9 +418,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
"cpu": [
"x64"
],
@ -454,9 +435,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
"cpu": [
"arm64"
],
@ -471,9 +452,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
"cpu": [
"x64"
],
@ -488,9 +469,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
"cpu": [
"arm64"
],
@ -505,9 +486,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
"cpu": [
"ia32"
],
@ -522,9 +503,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
"cpu": [
"x64"
],
@ -539,18 +520,25 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@ -563,16 +551,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@ -581,9 +559,9 @@
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -941,96 +919,56 @@
"win32"
]
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz",
"integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==",
"node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
"debug": "^4.4.1",
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
"deepmerge": "^4.3.1",
"kleur": "^4.1.5",
"magic-string": "^0.30.17",
"vitefu": "^1.0.6"
"magic-string": "^0.30.21",
"obug": "^2.1.0",
"vitefu": "^1.1.1"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22"
"node": "^20.19 || ^22.12 || >=24"
},
"peerDependencies": {
"svelte": "^5.0.0",
"vite": "^6.0.0"
"vite": "^6.3.0 || ^7.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz",
"integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==",
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.7"
"obug": "^2.1.0"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22"
"node": "^20.19 || ^22.12 || >=24"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0-next.0",
"svelte": "^5.0.0",
"vite": "^6.0.0"
"vite": "^6.3.0 || ^7.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte-inspector/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@sveltejs/vite-plugin-svelte-inspector/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/@sveltejs/vite-plugin-svelte/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@sveltejs/vite-plugin-svelte/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -1045,10 +983,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true,
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
@ -1059,20 +1004,10 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-typescript": {
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": ">=8.9.0"
}
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@ -1199,6 +1134,13 @@
"node": ">=0.10.0"
}
},
"node_modules/devalue": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
"dev": true,
"license": "MIT"
},
"node_modules/dynamic-marquee": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/dynamic-marquee/-/dynamic-marquee-2.6.4.tgz",
@ -1207,13 +1149,12 @@
"license": "Apache-2.0"
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@ -1221,32 +1162,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
"@esbuild/aix-ppc64": "0.27.3",
"@esbuild/android-arm": "0.27.3",
"@esbuild/android-arm64": "0.27.3",
"@esbuild/android-x64": "0.27.3",
"@esbuild/darwin-arm64": "0.27.3",
"@esbuild/darwin-x64": "0.27.3",
"@esbuild/freebsd-arm64": "0.27.3",
"@esbuild/freebsd-x64": "0.27.3",
"@esbuild/linux-arm": "0.27.3",
"@esbuild/linux-arm64": "0.27.3",
"@esbuild/linux-ia32": "0.27.3",
"@esbuild/linux-loong64": "0.27.3",
"@esbuild/linux-mips64el": "0.27.3",
"@esbuild/linux-ppc64": "0.27.3",
"@esbuild/linux-riscv64": "0.27.3",
"@esbuild/linux-s390x": "0.27.3",
"@esbuild/linux-x64": "0.27.3",
"@esbuild/netbsd-arm64": "0.27.3",
"@esbuild/netbsd-x64": "0.27.3",
"@esbuild/openbsd-arm64": "0.27.3",
"@esbuild/openbsd-x64": "0.27.3",
"@esbuild/openharmony-arm64": "0.27.3",
"@esbuild/sunos-x64": "0.27.3",
"@esbuild/win32-arm64": "0.27.3",
"@esbuild/win32-ia32": "0.27.3",
"@esbuild/win32-x64": "0.27.3"
}
},
"node_modules/esm-env": {
@ -1257,9 +1198,9 @@
"license": "MIT"
},
"node_modules/esrap": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.3.tgz",
"integrity": "sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1339,20 +1280,6 @@
"@types/estree": "^1.0.6"
}
},
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/live_json": {
"resolved": "../deps/live_json",
"link": true
},
"node_modules/live_svelte": {
"resolved": "../..",
"link": true
@ -1433,6 +1360,17 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
],
"license": "MIT"
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -1627,23 +1565,25 @@
}
},
"node_modules/svelte": {
"version": "5.19.8",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.8.tgz",
"integrity": "sha512-56Vd/nwJrljV0w7RCV1A8sB4/yjSbWW5qrGDTAzp7q42OxwqEWT+6obWzDt41tHjIW+C9Fs2ygtejjJrXR+ZPA==",
"version": "5.53.7",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.7.tgz",
"integrity": "sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.6.3",
"esm-env": "^1.2.1",
"esrap": "^1.4.3",
"esrap": "^2.2.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
@ -1703,9 +1643,9 @@
}
},
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -1717,25 +1657,25 @@
}
},
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@ -1744,14 +1684,14 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@types/node": "^20.19.0 || >=22.12.0",
"jiti": ">=1.21.0",
"less": "*",
"less": "^4.0.0",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
@ -1852,9 +1792,9 @@
"license": "ISC"
},
"node_modules/zimmerframe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"dev": true,
"license": "MIT"
}

View file

@ -1,18 +1,17 @@
{
"type": "module",
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/lodash": "^4.14.192",
"daisyui": "^5.0.0",
"dynamic-marquee": "^2.6.2",
"lodash": "^4.17.21",
"stylus": "^0.55.0",
"svelte": "^5.19.8",
"typescript": "^5.6.3",
"vite": "^6.0.0"
"svelte": "^5.53.7",
"typescript": "^5.9.3",
"vite": "^7.3.1"
},
"dependencies": {
"live_json": "file:../deps/live_json",
"live_svelte": "file:../..",
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",

View file

@ -1,22 +0,0 @@
<script>
/** @type {{ big_data_set: any }} */
let {big_data_set} = $props()
let keyCount = $derived(Object.keys(big_data_set).length)
let byteSize = $derived(JSON.stringify(big_data_set).length)
</script>
<div class="flex flex-col gap-3">
<p class="text-xs text-base-content/50">Check the WebSocket to see how much data is transferred.</p>
<dl class="flex flex-col gap-2">
<div class="flex justify-between items-center gap-4 text-sm">
<dt class="text-base-content/60">Key length</dt>
<dd data-testid="live-json-key-count" class="font-mono font-semibold tabular-nums text-brand">{keyCount.toLocaleString()}</dd>
</div>
<div class="flex justify-between items-center gap-4 text-sm">
<dt class="text-base-content/60">Rough byte size</dt>
<dd data-testid="live-json-byte-size" class="font-mono font-semibold tabular-nums text-brand">{byteSize.toLocaleString()}</dd>
</div>
</dl>
<button data-testid="live-json-remove-element" phx-click="remove_element" class="btn btn-sm bg-brand text-white border-0 hover:opacity-90 w-fit mt-1"> Remove element </button>
</div>

View file

@ -0,0 +1,17 @@
<script>
let {greeting} = $props()
let clicks = $state(0)
</script>
<div class="card bg-base-100 border border-base-300 p-6 max-w-sm text-center space-y-4">
<p data-testid="ssr-greeting" class="text-lg">{greeting}</p>
<p class="text-sm text-base-content/50">This text was rendered by the server before JavaScript loaded.</p>
<div class="flex items-center justify-center gap-4">
<button data-testid="ssr-increment" class="btn btn-sm bg-brand text-white border-0 hover:opacity-90 w-fit" onclick={() => clicks++}>
Click me
</button>
<span data-testid="click-count" class="font-mono">{clicks}</span>
</div>
<p class="text-xs text-base-content/30">Click counter is client-side only (not SSR)</p>
</div>

View file

@ -1,13 +1,21 @@
<script>
let {struct} = $props();
let {struct} = $props()
</script>
<div class="card bg-base-100 shadow-md border border-base-300/50 overflow-hidden md:min-w-md">
<div class="card-body gap-4 p-5">
<span class="badge badge-ghost badge-sm font-medium text-base-content/70 w-fit"> Struct </span>
<pre data-testid="struct-json" class="font-mono text-sm bg-base-200/80 text-base-content p-4 rounded-lg border border-base-300/50 overflow-x-auto"><code
<pre
data-testid="struct-json"
class="font-mono text-sm bg-base-200/80 text-base-content p-4 rounded-lg border border-base-300/50 overflow-x-auto"><code
>{JSON.stringify(struct, null, 2)}</code
></pre>
<button phx-click="randomize" class="btn btn-sm bg-brand text-white border-0 hover:opacity-90 w-fit" data-testid="struct-randomize-age"> Randomize Age </button>
<button
phx-click="randomize"
class="btn btn-sm bg-brand text-white border-0 hover:opacity-90 w-fit"
data-testid="struct-randomize-age"
>
Randomize Age
</button>
</div>
</div>

View file

@ -43,7 +43,7 @@ defmodule ExampleWeb do
layouts: [html: ExampleWeb.Layouts]
import Plug.Conn
import ExampleWeb.Gettext
use Gettext, backend: ExampleWeb.Gettext
unquote(verified_routes())
end
@ -85,7 +85,7 @@ defmodule ExampleWeb do
import Phoenix.HTML
# Core UI components and translation
import ExampleWeb.CoreComponents
import ExampleWeb.Gettext
use Gettext, backend: ExampleWeb.Gettext
import LiveSvelte

View file

@ -9,9 +9,9 @@ defmodule ExampleWeb.CoreComponents do
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
"""
use Phoenix.Component
use Gettext, backend: ExampleWeb.Gettext
alias Phoenix.LiveView.JS
import ExampleWeb.Gettext
@doc """
Renders a modal.

View file

@ -32,7 +32,6 @@ defmodule ExampleWeb.Layouts do
%{label: "Log List", to: ~p"/live-log-list"},
%{label: "Breaking News", to: ~p"/live-breaking-news"},
%{label: "Chat", to: ~p"/live-chat"},
%{label: "LiveJSON", to: ~p"/live-json"},
%{label: "Props Diff", to: ~p"/live-props-diff"},
%{label: "ID List Diff", to: ~p"/live-id-list-diff"}
]
@ -47,7 +46,8 @@ defmodule ExampleWeb.Layouts do
%{
label: "Advanced",
links: [
%{label: "Client Loading", to: ~p"/live-client-side-loading"}
%{label: "Client Loading", to: ~p"/live-client-side-loading"},
%{label: "SSR Demo", to: ~p"/live-ssr"}
]
},
%{

View file

@ -114,12 +114,6 @@
</a>
<span class="text-base-content/50 text-sm">- PubSub + pushEvent</span>
</li>
<li>
<a href={~p"/live-json"} class="link link-primary">
LiveJSON
</a>
<span class="text-base-content/50 text-sm">- Efficient JSON diffing</span>
</li>
<li>
<a href={~p"/live-props-diff"} class="link link-primary">
Props Diff
@ -180,6 +174,12 @@
</a>
<span class="text-base-content/50 text-sm">- Loading states and SSR</span>
</li>
<li>
<a href={~p"/live-ssr"} class="link link-primary">
SSR Demo
</a>
<span class="text-base-content/50 text-sm">- Server-side rendering with NodeJS</span>
</li>
</ul>
</div>
</div>

View file

@ -20,5 +20,5 @@ defmodule ExampleWeb.Gettext do
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
"""
use Gettext, otp_app: :example
use Gettext.Backend, otp_app: :example
end

View file

@ -1,65 +0,0 @@
defmodule ExampleWeb.LiveJson do
use ExampleWeb, :live_view
def render(assigns) do
~H"""
<div class="flex flex-col justify-center items-center gap-6 p-6">
<h2 class="text-center text-2xl font-light my-4">
Live JSON
</h2>
<p class="text-sm text-base-content/50 text-center max-w-md">
Large payloads are patched over the wire. Compare SSR vs no-SSR and watch the WebSocket traffic when removing elements.
</p>
<div class="flex flex-wrap justify-center gap-8 w-full max-w-4xl">
<section class="card bg-base-100 shadow-md border border-base-300/50 overflow-hidden w-full max-w-sm">
<div class="card-body gap-2 p-4">
<span class="badge badge-outline badge-sm font-medium text-base-content/70 w-fit">
SSR
</span>
<.svelte
name="LiveJson"
live_json_props={%{big_data_set: @ljbig_data_set}}
socket={@socket}
/>
</div>
</section>
<section class="card bg-base-100 shadow-md border border-base-300/50 overflow-hidden w-full max-w-sm">
<div class="card-body gap-2 p-4">
<span class="badge badge-outline badge-sm font-medium text-base-content/70 w-fit">
No SSR
</span>
<.svelte name="LiveJson" live_json_props={%{big_data_set: @ljbig_data_set}} ssr={false} />
</div>
</section>
</div>
</div>
"""
end
def mount(_session, _params, socket) do
data =
for i <- 1..100_000,
into: %{} do
{i, Enum.random(1..1_000_000_000)}
end
{:ok, LiveJson.initialize(socket, "big_data_set", data)}
end
def handle_event("remove_element", _values, socket) do
random_key =
socket.assigns.ljbig_data_set
|> Map.keys()
|> Enum.random()
{
:noreply,
LiveJson.push_patch(
socket,
"big_data_set",
Map.delete(socket.assigns.ljbig_data_set, random_key)
)
}
end
end

View file

@ -0,0 +1,20 @@
defmodule ExampleWeb.LiveSsr do
use ExampleWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, greeting: "Hello from the server!")}
end
def render(assigns) do
~H"""
<div class="flex flex-col justify-center items-center gap-6 p-6">
<h2 class="text-center text-2xl font-light my-4">SSR Demo</h2>
<p class="text-sm text-base-content/50 text-center max-w-md">
This component is rendered on the server using NodeJS. The initial HTML includes
the Svelte output before the client-side JavaScript runs.
</p>
<.svelte name="SsrDemo" props={%{greeting: @greeting}} socket={@socket} ssr={true} />
</div>
"""
end
end

View file

@ -32,7 +32,6 @@ defmodule ExampleWeb.Router do
live("/live-log-list", LiveLogList)
live("/live-breaking-news", LiveBreakingNews)
live("/live-chat", LiveChat)
live("/live-json", LiveJson)
live("/live-props-diff", LivePropsDiff)
live("/streams", Streams)
live("/live-id-list-diff", LiveIdListDiff)
@ -47,6 +46,7 @@ defmodule ExampleWeb.Router do
live("/live-event-reply", LiveEventReply)
live("/live-navigation", LiveNavigation)
live("/live-navigation/:page", LiveNavigation)
live("/live-ssr", LiveSsr)
live("/live-composition", LiveComposition)
end

View file

@ -36,8 +36,6 @@ defmodule Example.MixProject do
{:ecto_sql, "~> 3.12"},
{:gettext, "~> 0.20"},
{:json_diff_ex, "~> 0.6", override: true},
{:jsonpatch, "~> 2.3", override: true},
{:live_json, "~> 0.4.5"},
{:live_svelte, path: ".."},
# {:live_svelte, "~> 0.17.4"},
{:phoenix, "~> 1.8.0"},

View file

@ -22,14 +22,13 @@
"json_diff_ex": {:hex, :json_diff_ex, "0.7.0", "ef9a7809fce09fecc7fe4ffcac85658ab6de6aaccd55e056dea16da4ecfa6121", [:mix], [], "hexpm", "be71212b736f0f36ef5d805c7d72de4d4e57b5176d0cca99d78eb4d8198da547"},
"jsonpatch": {:hex, :jsonpatch, "2.3.1", "49c380f458debbd2bc6e256daeab1081dc89624288f3d77ea83952229388d316", [:make, :mix], [], "hexpm", "06c3e4fff3574cc54d335041f6322fe1b72756e396dd472615ce350d3dd5e758"},
"lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"},
"live_json": {:hex, :live_json, "0.4.5", "7a97932a8bb944d546a2e0af231aa90889eeaa0ab3f77afc50748636d9202581", [:mix], [{:jason, ">= 1.3.0", [hex: :jason, repo: "hexpm", optional: true]}, {:json_diff_ex, "~> 0.5.0", [hex: :json_diff_ex, repo: "hexpm", optional: false]}, {:jsonpatch, "~> 0.13.1", [hex: :jsonpatch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 3.1.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.16.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "43f941c94ef3a917f0940552316ae23128b8da274511d1d356494441a69edd71"},
"live_svelte": {:hex, :live_svelte, "0.17.2", "b644a22c96e44e03491240e6302fc21cae0c66e2b702531cbbd4732a9f08cc1d", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:nodejs, "~> 3.1", [hex: :nodejs, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 3.3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "17ddf9bca37ddc8e1090dad267cc81f846a8ad8647864e6172fb9fcdd7a0c066"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
"nodejs": {:hex, :nodejs, "3.1.3", "8693fae9fbefa14fb99329292c226df4d4711acfa5a3fa4182dd8d3f779b30bf", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1", [hex: :poolboy, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.7", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e7751aad77ac55f8e6c5c07617378afd88d2e0c349d9db2ebb5273aae46ef6a9"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.8.4", "0387f84f00071cba8d71d930b9121b2fb3645197a9206c31b908d2e7902a4851", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c988b1cd3b084eebb13e6676d572597d387fa607dab258526637b4e6c4c08543"},
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},

View file

@ -1,76 +0,0 @@
defmodule ExampleWeb.LiveJsonTest do
@moduledoc """
E2E test for the LiveJson LiveView (/live-json).
Validates that the page mounts with two sections (SSR and No SSR), shows key length
and byte size, and that clicking Remove element decreases the key count.
"""
use ExampleWeb.FeatureCase, async: false
@moduletag :e2e
# First key-count dd in the first section (SSR)
defp first_key_count_dd(session) do
session |> all(Query.css("[data-testid='live-json-key-count']")) |> List.first()
end
defp wait_for_key_count(session, expected, attempts \\ 50) do
dd = first_key_count_dd(session)
actual = dd && Wallaby.Element.text(dd)
cond do
actual == expected ->
session
attempts == 0 ->
raise "timeout waiting for key count #{inspect(expected)}, got #{inspect(actual)}"
true ->
:timer.sleep(100)
wait_for_key_count(session, expected, attempts - 1)
end
end
test "page mounts and shows heading and description", %{session: session} do
session
|> visit("/live-json")
|> assert_has(Query.css("h2", text: "Live JSON"))
|> assert_has(
Query.css("p",
text:
"Large payloads are patched over the wire. Compare SSR vs no-SSR and watch the WebSocket traffic when removing elements."
)
)
end
test "renders two sections with key length and Remove element button", %{session: session} do
session = visit(session, "/live-json")
# Two section cards (SSR and No SSR); at least one of each badge
ssr_badges = session |> all(Query.css("section.card span.badge", text: "SSR"))
no_ssr_badges = session |> all(Query.css("section.card span.badge", text: "No SSR"))
assert length(ssr_badges) >= 1
assert length(no_ssr_badges) >= 1
# Wait for Svelte to hydrate and show key count
session = wait_for_key_count(session, "100,000")
key_length_dts = session |> all(Query.css("dt", text: "Key length"))
assert length(key_length_dts) >= 1
remove_btns = session |> all(Query.css("[data-testid='live-json-remove-element']"))
assert length(remove_btns) >= 1
end
test "clicking Remove element decreases key count", %{session: session} do
session =
session
|> visit("/live-json")
|> wait_for_key_count("100,000")
# Click the first section's Remove element button
[remove_btn | _] = session |> all(Query.css("[data-testid='live-json-remove-element']"))
Wallaby.Element.click(remove_btn)
session = wait_for_key_count(session, "99,999")
dd = first_key_count_dd(session)
assert Wallaby.Element.text(dd) == "99,999"
end
end

View file

@ -0,0 +1,24 @@
defmodule ExampleWeb.Live.LiveSsrTest do
@moduledoc """
E2E test for the LiveSsr LiveView (/live-ssr).
Validates that the SSR demo page renders the greeting from props and that
the client-side click counter works after hydration.
"""
use ExampleWeb.FeatureCase, async: false
@moduletag :e2e
test "renders SSR greeting and click counter works", %{session: session} do
session = visit(session, "/live-ssr")
session |> find(Query.css("[data-testid='ssr-greeting']", text: "Hello from the server!"))
count_el = session |> find(Query.css("[data-testid='click-count']"))
assert Wallaby.Element.text(count_el) == "0"
session = session |> click(Query.css("[data-testid='ssr-increment']"))
count_el = session |> find(Query.css("[data-testid='click-count']"))
assert Wallaby.Element.text(count_el) == "1"
end
end

View file

@ -1,48 +0,0 @@
defmodule ExampleWeb.PhoenixTest.LiveJsonTest do
@moduledoc """
PhoenixTest (in-process) for LiveJson (/live-json).
Validates that the page renders two LiveJson sections (SSR and No SSR)
and shows key length and Remove element button. Remove-element behavior
is covered in E2E (live_json_test.exs).
"""
use ExampleWeb.ConnCase, async: false
import PhoenixTest
@moduletag :phoenix_test
setup do
ssr = Application.get_env(:live_svelte, :ssr, false)
Application.put_env(:live_svelte, :ssr, true)
on_exit(fn ->
Application.put_env(:live_svelte, :ssr, ssr)
end)
:ok
end
test "renders page heading and description", %{conn: conn} do
conn
|> visit("/live-json")
|> assert_has("h2", text: "Live JSON")
|> assert_has("p",
text:
"Large payloads are patched over the wire. Compare SSR vs no-SSR and watch the WebSocket traffic when removing elements."
)
end
test "renders two sections (SSR and No SSR) with LiveJson component", %{conn: conn} do
conn
|> visit("/live-json")
|> assert_has("span.badge", text: "SSR")
|> assert_has("span.badge", text: "No SSR")
|> assert_has("[data-name='LiveJson']", count: 2)
end
test "shows key length and Remove element button", %{conn: conn} do
conn
|> visit("/live-json")
|> assert_has("dt", text: "Key length")
|> assert_has("[data-testid='live-json-remove-element']")
end
end

View file

@ -0,0 +1,13 @@
defmodule ExampleWeb.LiveSsrTest do
use ExampleWeb.ConnCase, async: true
@moduletag :phoenix_test
import PhoenixTest
test "renders SSR demo page", %{conn: conn} do
conn
|> visit("/live-ssr")
|> assert_has("h2", text: "SSR Demo")
|> assert_has("[data-props*='greeting']")
end
end

386
guides/api_reference.md Normal file
View file

@ -0,0 +1,386 @@
# API Reference
Complete reference for all public LiveSvelte APIs.
## Elixir API
### `LiveSvelte.svelte/1`
Renders a Svelte component in a LiveView template.
```heex
<.svelte name="Counter" props={%{count: @count}} socket={@socket} />
```
**Attributes:**
| Attribute | Type | Default | Required | Description |
|-----------|------|---------|----------|-------------|
| `name` | `string` | — | ✓ | Component name (filename without `.svelte`, relative to `assets/svelte/`) |
| `props` | `map` | `%{}` | | Props to pass to the component |
| `socket` | `map` | `nil` | | LiveView socket — required when `ssr: true` |
| `id` | `string` | auto | | Stable DOM id override |
| `key` | `any` | `nil` | | Identity key for DOM id generation in loops |
| `class` | `string` | `nil` | | CSS class on the wrapper div |
| `ssr` | `boolean` | `true` | | Enable SSR for this component |
| `diff` | `boolean` | `true` | | Enable props diffing (requires `enable_props_diff: true` in config) |
| `:loading` slot | | | | Content shown while component loads (only with `ssr={false}`) |
| `:inner_block` slot | | | | Inner content (passed to Svelte as a slot) |
**Name examples:**
```
Counter.svelte → name="Counter"
forms/UserForm.svelte → name="forms/UserForm"
```
---
### `~V` Sigil
Inline Svelte template as a LiveView render macro.
```elixir
def render(assigns) do
~V"""
<script>
let { count } = $props()
</script>
<p>Count: {count}</p>
"""
end
```
All LiveView assigns are automatically available as props. The template is written to `assets/svelte/_build/MyModule.svelte` at compile time.
---
### `LiveSvelte.Components`
Auto-generated shorthand component functions based on discovered `.svelte` files.
```elixir
# In web module html_helpers:
use LiveSvelte.Components
# In templates — instead of <.svelte name="Counter" ...>:
<.Counter count={@count} socket={@socket} />
```
`Counter.svelte``<.Counter>`, `forms/UserForm.svelte``<.forms_UserForm>` (slashes converted to underscores).
---
### `LiveSvelte.Test.get_svelte/1,2`
Inspect Svelte component props from HTML in tests.
```elixir
import LiveSvelte.Test
# Get first component in HTML
component = get_svelte(html)
# Get component by name
component = get_svelte(html, name: "Counter")
# Get component by DOM id
component = get_svelte(html, id: "Counter-1")
# Get directly from a LiveView
{:ok, view, _html} = live(conn, "/counter")
component = get_svelte(view, name: "Counter")
```
Returns a map with:
- `name` — component name string
- `id` — DOM id of the wrapper element
- `props` — decoded props map (string keys)
- `slots` — map of slot name → HTML string
- `ssr` — boolean, whether SSR was active
**Example:**
```elixir
{:ok, _view, html} = live(conn, "/counter")
component = get_svelte(html, name: "Counter")
assert component.props["count"] == 0
```
---
### `LiveSvelte.Encoder` Protocol
Protocol for encoding custom structs as JSON props. Implement it directly or use `@derive`:
```elixir
# Simple derive — exposes all public fields
@derive LiveSvelte.Encoder
defstruct [:id, :name]
# Restricted derive — only expose listed fields
@derive {LiveSvelte.Encoder, only: [:id, :name, :email]}
defstruct [:id, :name, :email, :password_hash]
# Excluded fields derive
@derive {LiveSvelte.Encoder, except: [:password_hash]}
defstruct [:id, :name, :email, :password_hash]
```
Without `@derive`, passing a struct as a prop will raise an error.
---
### `LiveSvelte.Reload` / `vite_assets/0`
HMR helper for development. Includes the Vite dev server client script.
```heex
<!-- In root layout, development only -->
<%= if Application.get_env(:live_svelte, :ssr_module) == LiveSvelte.SSR.ViteJS do %>
<LiveSvelte.Reload.vite_assets path="/assets/js/app.js" />
<% end %>
```
---
## JavaScript API
### `getHooks(Components)`
Entry point. Returns a hooks map to pass to `LiveSocket`:
```ts
import { getHooks } from "live_svelte"
import Components from "virtual:live-svelte-components"
const liveSocket = new LiveSocket("/live", Socket, {
hooks: getHooks(Components),
params: { _csrf_token: csrfToken }
})
```
---
### `useLiveSvelte()`
Access the Phoenix hook context from any LiveSvelte-mounted component.
```ts
import { useLiveSvelte } from "live_svelte"
```
```svelte
<script>
import { useLiveSvelte } from "live_svelte"
const { pushEvent, pushEventTo, live } = useLiveSvelte()
function save(data) {
pushEvent("save", data)
}
function saveWithReply(data) {
pushEvent("save", data, (reply) => console.log(reply))
}
</script>
```
**Returns:**
- `live` — raw Phoenix hook context
- `pushEvent(event, payload, callback?)` — push event to LiveView
- `pushEventTo(target, event, payload, callback?)` — push event to specific LiveView
---
### `useLiveEvent(event, callback)`
Subscribe to a server-sent LiveView event. Automatically cleans up on component destroy.
```svelte
<script>
import { useLiveEvent } from "live_svelte"
useLiveEvent("item_added", (payload) => {
console.log("New item:", payload)
})
</script>
```
---
### `useLiveConnection()`
Reactive WebSocket connection state.
```svelte
<script>
import { useLiveConnection } from "live_svelte"
const conn = useLiveConnection()
</script>
{#if !conn.connected}
<div class="banner">Reconnecting...</div>
{/if}
```
**Returns:**
- `connected``boolean`, reactive
---
### `useLiveNavigation()`
Client-side LiveView navigation.
```svelte
<script>
import { useLiveNavigation } from "live_svelte"
const { patch, navigate } = useLiveNavigation()
</script>
<button onclick={() => patch("?page=2")}>Next page</button>
<button onclick={() => navigate("/other")}>Navigate</button>
```
**Returns:**
- `patch(hrefOrParams, opts?)` — patch current LiveView (triggers `handle_params/3`)
- `navigate(href, opts?)` — navigate to a new LiveView
Both accept `{ replace: true }` to use `history.replaceState`.
---
### `useLiveForm(formFn, opts?)`
Reactive form binding with Ecto changeset support. See [Forms and Validation](forms.md) for full documentation.
```ts
import { useLiveForm } from "live_svelte"
```
```svelte
<script>
import { useLiveForm } from "live_svelte"
let { form } = $props()
const { field, fieldArray } = useLiveForm(() => form)
</script>
```
**Parameters:**
- `formFn` — getter function returning the form prop
- `opts?``{ changeEvent?, submitEvent?, debounceInMilliseconds? }`
**Returns:**
- `field(name)` — field descriptor with `name`, `value`, `error`, `phx-debounce`
- `fieldArray(name)` — array field with `fields`, `append`, `prepend`, `remove`
---
### `useLiveUpload(uploadConfig, options)`
File upload integration. See [File Uploads](uploads.md) for full documentation.
```ts
import { useLiveUpload } from "live_svelte"
```
```svelte
<script>
import { useLiveUpload } from "live_svelte"
let { uploads } = $props()
const { showFilePicker, entries, submit, cancel, sync } = useLiveUpload(
uploads.avatar,
{ changeEvent: "validate", submitEvent: "submit" }
)
$effect(() => sync(uploads.avatar))
</script>
```
**Parameters:**
- `uploadConfig` — the upload config object (e.g. `uploads.avatar`), passed directly not as a getter
- `options``{ changeEvent?: string, submitEvent: string }``submitEvent` is required
**Returns:**
- `showFilePicker()` — open file picker dialog
- `addFiles(files)` — enqueue files from `File[]` or `DataTransfer` (drag-drop)
- `entries``Readable<UploadEntry[]>` store — use `$entries` in templates
- `progress``Readable<number>` — overall progress 0100
- `valid``Readable<boolean>` — true when no top-level upload errors
- `submit()` — programmatic form submit
- `cancel(ref?)` — cancel entry by ref string, or all when omitted
- `clear()` — reset file input
- `sync(config)` — merge updated config from server; call in `$effect`
---
### `useEventReply()`
Request-response pattern: push an event and await a reply.
```ts
import { useEventReply } from "live_svelte"
```
```svelte
<script>
import { useEventReply } from "live_svelte"
const { push } = useEventReply()
async function save(data) {
const result = await push("save", data)
console.log("Server replied:", result)
}
</script>
```
**Returns:**
- `push(event, payload)` — returns a `Promise` that resolves with the server reply
The LiveView must reply using `{:reply, payload, socket}` in `handle_event/3`:
```elixir
def handle_event("save", params, socket) do
{:reply, %{status: "ok"}, socket}
end
```
---
### `Link` Component
Client-side navigation component. Svelte equivalent of Phoenix's `<.link>`.
```svelte
<script>
import { Link } from "live_svelte"
</script>
<Link href="/other-page">Go to other page</Link>
<Link href="/other-page" replace={true}>Replace history</Link>
```
---
## Telemetry Events
| Event | Measurements | Metadata | Description |
|-------|-------------|----------|-------------|
| `[:live_svelte, :ssr, :start]` | `%{system_time: integer}` | `%{component: name}` | SSR render begins |
| `[:live_svelte, :ssr, :stop]` | `%{duration_microseconds: integer}` | `%{component: name}` | SSR render completes |
| `[:live_svelte, :ssr, :exception]` | `%{system_time: integer}` | `%{component: name, reason: term}` | SSR render fails |
---
## Configuration Keys
See [Configuration](configuration.md) for full details.
| Key | Default | Description |
|-----|---------|-------------|
| `config :live_svelte, :ssr` | `true` | Global SSR enable/disable |
| `config :live_svelte, :ssr_module` | `LiveSvelte.SSR.NodeJS` | SSR module |
| `config :live_svelte, :json_library` | `LiveSvelte.JSON` | JSON encoder |
| `config :live_svelte, :enable_props_diff` | `true` | Props diffing system |
| `config :live_svelte, :gettext_backend` | `nil` | Gettext for form errors |
| `config :live_svelte, :vite_host` | `"http://localhost:5173"` | Vite dev server URL |

215
guides/basic_usage.md Normal file
View file

@ -0,0 +1,215 @@
# Basic Usage
This guide covers the fundamentals of using LiveSvelte: the `<.svelte>` component, props, events, and the `~V` sigil.
## Your First Component
### 1. Create a Svelte component
Place Svelte files in `assets/svelte/`. LiveSvelte discovers all `*.svelte` files in that directory at compile time.
```svelte
<!-- assets/svelte/Counter.svelte -->
<script>
let { count, live } = $props()
function increment() {
live.pushEvent("increment", {})
}
</script>
<div>
<p>Count: {count}</p>
<button onclick={increment}>Increment</button>
</div>
```
### 2. Use it in a LiveView
```elixir
# lib/my_app_web/live/counter_live.ex
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, :count, 0)}
end
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
def render(assigns) do
~H"""
<.svelte name="Counter" props={%{count: @count}} socket={@socket} />
"""
end
end
```
That's it. When the user clicks the button, `pushEvent("increment", {})` sends the event to `handle_event/3`, the count is incremented, and Svelte re-renders automatically.
## Props
Pass any JSON-serializable map as `props`:
```heex
<.svelte name="UserCard" props={%{name: @user.name, role: @user.role}} socket={@socket} />
```
In the component, receive with `$props()`:
```svelte
<script>
let { name, role } = $props()
</script>
<div>
<h2>{name}</h2>
<span>{role}</span>
</div>
```
### Struct Props
Structs must implement the `LiveSvelte.Encoder` protocol before being passed as props. Use `@derive` for the default implementation:
```elixir
defmodule MyApp.User do
@derive {LiveSvelte.Encoder, only: [:id, :name, :email]}
defstruct [:id, :name, :email, :password_hash]
end
```
The `only:` list controls which fields are exposed. Never derive without `only:` for structs with sensitive fields.
## The `live` Prop
LiveSvelte automatically passes a `live` prop to every mounted component. Use it to communicate with the server:
```svelte
<script>
let { live } = $props()
// Push event to server (fire-and-forget)
function save(data) {
live.pushEvent("save", data)
}
// Push event and receive reply
function saveWithReply(data) {
live.pushEvent("save", data, (reply) => {
console.log("Server replied:", reply)
})
}
// Subscribe to server-sent events
live.handleEvent("flash", ({ message }) => {
alert(message)
})
</script>
```
## Composable Alternative to `live` Prop
Instead of using the `live` prop, you can use composables which work from any component in the tree — no prop drilling:
```svelte
<script>
import { useLiveSvelte, useLiveEvent } from "live_svelte"
const { pushEvent } = useLiveSvelte()
useLiveEvent("flash", ({ message }) => {
alert(message)
})
function save(data) {
pushEvent("save", data)
}
</script>
```
See the [API Reference](api_reference.md) for all composables.
## Component Shorthand with `LiveSvelte.Components`
Add `use LiveSvelte.Components` to your LiveView (or web module) for shorthand component functions:
```elixir
# In web module html_helpers (added by Igniter installer):
import LiveSvelte
use LiveSvelte.Components
```
Then instead of `<.svelte name="Counter" ...>`, use:
```heex
<.Counter count={@count} socket={@socket} />
```
The function names are generated from your `.svelte` filenames. `Counter.svelte``<.Counter>`, `UserCard.svelte``<.UserCard>`.
> #### `socket` is required for SSR {: .info}
>
> Always pass `socket={@socket}` when SSR is enabled. It's used to detect the initial dead render vs. connected live render. You can omit it only when `ssr={false}`.
## Inline Templates with the `~V` Sigil
For small, one-off components, write Svelte templates inline using the `~V` sigil:
```elixir
def render(assigns) do
~V"""
<script>
let { count } = $props()
</script>
<p>Count is {count}</p>
"""
end
```
The sigil writes the template to `assets/svelte/_build/` at compile time and mounts it like any other component. All LiveView assigns are automatically available as props.
> #### Svelte 5 Syntax Required {: .warning}
>
> Always use Svelte 5 runes syntax. Do NOT use Svelte 4 patterns:
>
> | ❌ Svelte 4 | ✅ Svelte 5 |
> |-------------|-------------|
> | `export let count` | `let { count } = $props()` |
> | `let x = 0` (reactive) | `let x = $state(0)` |
> | `$: doubled = x * 2` | `let doubled = $derived(x * 2)` |
> | `<script context="module">` | module-level code in `.js` files |
## Local State
Local component state uses `$state()`:
```svelte
<script>
let { items } = $props()
let filter = $state("")
let filtered = $derived(items.filter(i => i.name.includes(filter)))
</script>
<input bind:value={filter} placeholder="Filter..." />
{#each filtered as item}
<li>{item.name}</li>
{/each}
```
## Component Discovery
LiveSvelte scans `assets/svelte/**/*.svelte` at compile time. Component names in `<.svelte name="...">` are relative paths without the `.svelte` extension:
```
assets/svelte/Counter.svelte → name="Counter"
assets/svelte/forms/UserForm.svelte → name="forms/UserForm"
```
## phx-update="ignore"
LiveSvelte automatically sets `phx-update="ignore"` on the component wrapper div, which prevents LiveView from patching Svelte's DOM after mount. All updates flow through the hook. This is required for correct operation — do not override it.

130
guides/configuration.md Normal file
View file

@ -0,0 +1,130 @@
# Configuration
All LiveSvelte configuration is set via `Application.put_env(:live_svelte, key, value)` in your config files.
## Application Config Keys
| Key | Default | Description |
|-----|---------|-------------|
| `:ssr` | `true` | Enable server-side rendering globally |
| `:ssr_module` | `LiveSvelte.SSR.NodeJS` | SSR module: `NodeJS` or `ViteJS` |
| `:json_library` | `LiveSvelte.JSON` | JSON encoder (e.g. `Jason`) |
| `:enable_props_diff` | `true` | Enable three-tier props diffing system |
| `:gettext_backend` | `nil` | Gettext module for form error translation |
| `:vite_host` | `"http://localhost:5173"` | Vite dev server URL (used by ViteJS SSR mode) |
## Typical Configuration by Environment
### `config/config.exs` (base)
```elixir
config :live_svelte, ssr: true
```
### `config/dev.exs` (development)
```elixir
config :live_svelte,
ssr_module: LiveSvelte.SSR.ViteJS,
vite_host: "http://localhost:5173"
```
### `config/prod.exs` (production)
```elixir
config :live_svelte,
ssr_module: LiveSvelte.SSR.NodeJS,
ssr: true
```
### `config/test.exs` (test)
```elixir
# SSR is off in tests by default (NodeJS not started in test env)
config :live_svelte, ssr: false
```
## Per-Component Attributes
These `<.svelte>` component attributes override global config for individual components:
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `name` | `string` | **required** | Svelte component filename (without `.svelte`) |
| `props` | `map` | `%{}` | Props passed to the component |
| `socket` | `map` | `nil` | LiveView socket — required when `ssr: true` |
| `id` | `string` | auto | Stable DOM id override |
| `key` | `any` | `nil` | Identity key for DOM id in loops |
| `class` | `string` | `nil` | CSS class for the wrapper div |
| `ssr` | `boolean` | `true` | Enable SSR for this component |
| `diff` | `boolean` | `true` | Enable props diffing for this component |
### Examples
```heex
<!-- Disable SSR for a heavy chart component -->
<.svelte name="HeavyChart" props={%{data: @data}} socket={@socket} ssr={false} />
<!-- Disable props diffing (always send full props) -->
<.svelte name="SimpleDisplay" props={%{label: @label}} socket={@socket} diff={false} />
<!-- Stable id for components in loops -->
<.svelte name="Item" props={%{id: item.id, title: item.title}} socket={@socket} key={item.id} />
```
## Vite Plugin Options
Configure the `liveSveltePlugin` in `assets/vite.config.mjs`:
```js
import { liveSveltePlugin } from "live_svelte/vitePlugin"
export default defineConfig({
plugins: [
svelte(),
liveSveltePlugin({
// Options (all optional):
paths: ["assets/svelte"], // Directories to scan for .svelte files
entrypoint: "assets/js/app.js" // Main app entry point
})
]
})
```
### Default Paths
By default, `liveSveltePlugin` discovers Svelte components in:
- `assets/svelte/**/*.svelte`
- `lib/**/*.svelte` (for colocated components next to LiveView modules)
## JSON Library
By default, LiveSvelte uses its own JSON encoder which handles `LiveSvelte.Encoder` protocol automatically. To use `Jason` instead:
```elixir
config :live_svelte, json_library: Jason
```
When using an external JSON library, LiveSvelte still runs all values through `LiveSvelte.Encoder` before passing to the library.
## Props Diffing
Props diffing is enabled by default. To disable globally:
```elixir
config :live_svelte, enable_props_diff: false
```
When disabled, LiveSvelte always sends the full props map on every update. This can be useful for debugging or for very simple UIs where diffing overhead is not worth it.
The three tiers (change tracking → JSON Patch → ID-based list diffing) are all part of the same system and toggle together.
## Gettext Integration
To translate Ecto changeset error messages using your Gettext backend:
```elixir
config :live_svelte, gettext_backend: MyAppWeb.Gettext
```
This affects `useLiveForm` — error messages shown in `field().error` will use translated strings from your `priv/gettext/` directory.

163
guides/deployment.md Normal file
View file

@ -0,0 +1,163 @@
# Deployment
Deploying a LiveSvelte application requires Node.js on the server for SSR (server-side rendering). This guide covers the production build process and deployment considerations.
## Requirements
- **Node.js 19+** on the production server (for `LiveSvelte.SSR.NodeJS`)
- Standard Phoenix/Elixir deployment tooling (releases, Docker, etc.)
## Build Steps
```bash
# 1. Build client bundle and SSR bundle
mix assets.js
# 2. Compile application (copies SSR bundle to _build)
mix compile
# OR in a single release command:
MIX_ENV=prod mix assets.js && MIX_ENV=prod mix release
```
### What `mix assets.js` Does
The `assets.js` alias runs (in order):
1. `npx vite build` — builds the client JavaScript bundle to `priv/static/assets/`
2. `npx vite build --config vite.ssr.config.js` — builds the SSR bundle to `priv/svelte/server.js`
3. `tailwind default` — builds CSS to `priv/static/assets/app.css`
> #### Vite Must Run Before Tailwind {: .warning}
>
> The Vite client build uses `emptyOutDir: true` and clears `priv/static/assets/` first. Always run Vite before Tailwind, or Tailwind's `app.css` will be deleted. The `mix assets.js` alias handles this order correctly.
## NodeJS Supervisor
The Igniter installer adds `NodeJS.Supervisor` to your `application.ex`:
```elixir
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
# ... other children ...
{NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
```
`LiveSvelte.SSR.NodeJS.server_path/0` returns the path to `priv/svelte/server.js`, which is the SSR bundle.
Adjust `pool_size` based on expected SSR load. A pool of 4 workers is a reasonable default.
## Production Config
```elixir
# config/prod.exs
config :live_svelte,
ssr_module: LiveSvelte.SSR.NodeJS,
ssr: true
```
## SSR Bundle
The SSR bundle (`priv/svelte/server.js`) is:
- Built from `assets/vite.ssr.config.js`
- Fully self-contained (all dependencies bundled, `ssr: { noExternal: true }`)
- Required to be present at application start when `ssr_module: LiveSvelte.SSR.NodeJS`
After `mix assets.js`, `mix compile` copies `priv/svelte/server.js` into `_build/`. This copy in `_build/` is what NodeJS.Supervisor actually loads at runtime.
> #### Always Compile After Building SSR Bundle {: .info}
>
> After `mix assets.js`, run `mix compile` so `_build/` gets the updated SSR bundle. In a CI/CD pipeline, ensure both steps run.
## Docker Deployment
Include Node.js in your Docker image:
```dockerfile
# Multi-stage build example
FROM node:20-slim AS assets-builder
WORKDIR /app
COPY assets/ assets/
COPY deps/ deps/
RUN cd assets && npm install
RUN mix assets.js
FROM elixir:1.17-slim AS release-builder
# ... standard Elixir release steps ...
RUN mix release
FROM elixir:1.17-slim
# Include Node.js for SSR
RUN apt-get update && apt-get install -y nodejs npm
COPY --from=release-builder /app/_build/prod/rel/my_app ./
CMD ["/app/bin/my_app", "start"]
```
A simpler approach is to use an image that includes both Elixir and Node.js:
```dockerfile
FROM hexpm/elixir:1.17.3-erlang-27.1.2-debian-bookworm-20241016-slim
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs
```
## Disabling SSR in Production
If you choose not to use SSR (e.g. to avoid Node.js on the server), disable it globally and remove `NodeJS.Supervisor`:
```elixir
# config/prod.exs
config :live_svelte, ssr: false
```
Remove `NodeJS.Supervisor` from `application.ex` children. The `{:nodejs, "~> 3.1"}` dependency can remain in `mix.exs` but won't be used.
## Per-Component SSR Opt-Out
Even with global SSR enabled, you can disable SSR for expensive components to reduce Node.js load:
```heex
<.svelte name="HeavyVisualization" props={%{data: @data}} socket={@socket} ssr={false} />
```
Use SSR primarily for above-the-fold content where first-paint HTML matters.
## Telemetry for Observability
Attach SSR telemetry handlers to monitor production performance:
```elixir
:telemetry.attach_many(
"live-svelte-ssr-metrics",
[
[:live_svelte, :ssr, :stop],
[:live_svelte, :ssr, :exception]
],
fn
[:live_svelte, :ssr, :stop], measurements, _meta, _ ->
MyApp.Metrics.histogram("live_svelte.ssr.duration", measurements.duration_microseconds)
[:live_svelte, :ssr, :exception], _measurements, meta, _ ->
Logger.error("SSR failed: #{inspect(meta.reason)}")
end,
nil
)
```
## Upgrading
When upgrading LiveSvelte versions:
1. Update `{:live_svelte, "~> x.y"}` in `mix.exs`
2. Run `mix deps.get`
3. Check `CHANGELOG.md` for breaking changes
4. Rebuild: `mix assets.js && mix compile`
5. Run tests: `mix test`

212
guides/forms.md Normal file
View file

@ -0,0 +1,212 @@
# Forms and Validation
LiveSvelte provides `useLiveForm` for building reactive forms backed by Ecto changesets with server-side validation.
## Quick Example
**LiveView:**
```elixir
defmodule MyAppWeb.UserFormLive do
use MyAppWeb, :live_view
alias MyApp.Accounts
def mount(_params, _session, socket) do
form = to_form(Accounts.change_user(%Accounts.User{}))
{:ok, assign(socket, form: form)}
end
def handle_event("validate", %{"user" => params}, socket) do
form = params |> Accounts.change_user() |> Map.put(:action, :validate) |> to_form()
{:noreply, assign(socket, form: form)}
end
def handle_event("submit", %{"user" => params}, socket) do
case Accounts.create_user(params) do
{:ok, _user} -> {:noreply, push_navigate(socket, to: "/")}
{:error, changeset} -> {:noreply, assign(socket, form: to_form(changeset))}
end
end
def render(assigns) do
~H"""
<.svelte name="UserForm" props={%{form: @form}} socket={@socket} />
"""
end
end
```
**Svelte Component:**
```svelte
<!-- assets/svelte/UserForm.svelte -->
<script>
import { useLiveForm } from "live_svelte"
let { form } = $props()
const { field } = useLiveForm(() => form)
</script>
<form phx-submit="submit" phx-change="validate">
<div>
<label>Name</label>
<input {...field("name")} />
{#if field("name").error}
<span class="error">{field("name").error}</span>
{/if}
</div>
<div>
<label>Email</label>
<input type="email" {...field("email")} />
{#if field("email").error}
<span class="error">{field("email").error}</span>
{/if}
</div>
<button type="submit">Save</button>
</form>
```
## The `useLiveForm` Composable
```ts
import { useLiveForm } from "live_svelte"
const { field, fieldArray } = useLiveForm(() => form, options?)
```
The first argument is a **getter function** (not the form value directly). This ensures `useLiveForm` always reads the latest reactive prop value.
### Options
```ts
type FormOptions = {
changeEvent?: string // Event name for validation (default: "validate")
submitEvent?: string // Event name for submission (default: "submit")
debounceInMilliseconds?: number // Debounce delay for change events (default: 300)
}
```
## The `field()` Function
`field(name)` returns an object you can spread onto an `<input>` element:
```svelte
<input {...field("email")} />
```
It returns:
- `name` — the HTML input name (matches changeset field)
- `value` — current field value from the changeset
- `error` — error message string (or `null`)
- `phx-debounce` — debounce attribute for change events
You can also access properties individually:
```svelte
<input
name={field("email").name}
value={field("email").value}
class={field("email").error ? "border-red-500" : ""}
/>
{#if field("email").error}
<p>{field("email").error}</p>
{/if}
```
## Nested Fields
Access nested fields with dot notation:
```svelte
<input {...field("address.street")} />
<input {...field("address.city")} />
```
## Dynamic Arrays with `fieldArray()`
For `embeds_many` or `has_many` with nested forms:
```svelte
<script>
import { useLiveForm } from "live_svelte"
let { form } = $props()
const { field, fieldArray } = useLiveForm(() => form)
const skills = fieldArray("skills")
</script>
{#each skills.fields as skillField, i}
<div>
<input {...field(`skills.${i}.name`)} />
<button type="button" onclick={() => skills.remove(i)}>Remove</button>
</div>
{/each}
<button type="button" onclick={() => skills.append({ name: "" })}>Add Skill</button>
```
`fieldArray(path)` returns:
- `fields` — reactive array of field descriptors
- `append(value)` — add an item to the end
- `prepend(value)` — add an item to the start
- `remove(index)` — remove an item by index
## Encoding Changesets as Props
To pass a changeset form as props, use `LiveSvelte.Encoder` for the changeset data. Phoenix's `to_form/1` produces a `Phoenix.HTML.Form` struct that LiveSvelte can encode automatically.
For custom structs used inside the form data, use `@derive`:
```elixir
defmodule MyApp.Address do
@derive {LiveSvelte.Encoder, only: [:street, :city, :zip]}
embedded_schema do
field :street, :string
field :city, :string
field :zip, :string
end
end
```
## TypeScript Types
```ts
import type { Form } from "live_svelte"
// Type your component props
let { form }: { form: Form<{ name: string; email: string }> } = $props()
const { field } = useLiveForm(() => form)
// field("name").value is typed as string
```
## Gettext Integration
If you have a Gettext backend configured, LiveSvelte translates error messages automatically:
```elixir
# config/config.exs
config :live_svelte, gettext_backend: MyAppWeb.Gettext
```
Error messages from changesets will use your Gettext translations.
## Full Form Example with Validation
```elixir
# LiveView
def handle_event("validate", %{"user" => params}, socket) do
changeset =
%User{}
|> User.changeset(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end
```
Setting `action: :validate` on the changeset causes Ecto to include validation errors, which LiveSvelte then passes back to the `field().error` values in the component.

160
guides/installation.md Normal file
View file

@ -0,0 +1,160 @@
# Installation
LiveSvelte uses [Vite](https://vitejs.dev/) for both client and SSR builds, replacing the default `esbuild` setup in Phoenix projects.
## Prerequisites
- **Node.js 19+** — required for SSR (server-side rendering)
- **Elixir 1.17+**
- **Phoenix 1.8+** — required for the Igniter installer
- **Igniter** — the installation scaffolding tool
## Quick Start (Recommended)
### Step 1: Install the Igniter archive
```bash
mix archive.install hex igniter_new
```
### Step 2: Add LiveSvelte to your project
For an **existing** Phoenix 1.8+ project:
```bash
mix igniter.install live_svelte
```
For a **new** project with LiveSvelte pre-installed:
```bash
mix igniter.new my_app --with phx.new --install live_svelte
```
Use the `--bun` flag to use `bun` instead of `npm`:
```bash
mix igniter.install live_svelte --bun
```
### Step 3: Install JS dependencies and build
```bash
cd assets && npm install && cd ..
mix assets.js
mix phx.server
```
Visit `/svelte_demo` to verify the installation with the generated demo component.
## What the Installer Does
Running `mix igniter.install live_svelte` makes the following changes to your project:
**`assets/package.json`** — adds:
- `live_svelte: "file:../deps/live_svelte"` (dependency)
- `svelte: "^5.0.0"` (dev dependency)
- `@sveltejs/vite-plugin-svelte` (dev dependency)
**`assets/vite.config.mjs`** — adds the Svelte plugin and `liveSveltePlugin`:
```js
import { svelte } from "@sveltejs/vite-plugin-svelte"
import { liveSveltePlugin } from "live_svelte/vitePlugin"
// ...
plugins: [svelte(), liveSveltePlugin()],
```
**`assets/vite.ssr.config.js`** — new file for the SSR bundle (Node.js server rendering)
**`assets/js/app.js`** — adds hook wiring:
```js
import { getHooks } from "live_svelte"
import Components from "virtual:live-svelte-components"
const liveSocket = new LiveSocket("/live", Socket, {
hooks: getHooks(Components),
// ...
})
```
**`lib/app_web.ex`** — adds `import LiveSvelte` to `html_helpers`
**`lib/app/application.ex`** — adds NodeJS supervisor for production SSR:
```elixir
{NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]}
```
**`config/config.exs`** — base SSR config:
```elixir
config :live_svelte, ssr: true
```
**`config/dev.exs`** — development SSR via Vite dev server:
```elixir
config :live_svelte,
ssr_module: LiveSvelte.SSR.ViteJS,
vite_host: "http://localhost:5173"
```
**`config/prod.exs`** — production SSR via NodeJS:
```elixir
config :live_svelte,
ssr_module: LiveSvelte.SSR.NodeJS,
ssr: true
```
**`mix.exs`** — adds the `assets.js` alias that runs both Vite builds plus Tailwind:
```elixir
"assets.js": [
"cmd npx vite build",
"cmd npx vite build --config vite.ssr.config.js",
"tailwind default"
]
```
**`assets/svelte/`** — creates the Svelte components directory with a demo component
**`assets/app.css`** — adds `@source "../svelte";` if Tailwind is present
**`.gitignore`** — adds `/assets/svelte/_build/` and `/priv/svelte/`
> #### Phoenix Version Requirement {: .warning}
>
> The Igniter installer requires **Phoenix 1.8+**. The library itself works with Phoenix 1.7+ when installed manually.
## Manual Installation
> #### Manual Installation Not Recommended {: .warning}
>
> Manual installation steps are complex and kept out-of-date as dependencies evolve. We strongly recommend using `mix igniter.install live_svelte` instead.
If you must install manually (e.g. Phoenix < 1.8), the overall steps mirror the LiveVue manual installation process:
1. Add `{:live_svelte, "~> 0.17"}` to `mix.exs` deps
2. Configure Vite with the Svelte plugin and `liveSveltePlugin`
3. Create `vite.ssr.config.js`
4. Wire up `getHooks(Components)` in `app.js`
5. Add `import LiveSvelte` to `html_helpers`
6. Add `NodeJS.Supervisor` to `application.ex`
7. Configure SSR in `config/`
## Disabling SSR
If you don't need server-side rendering, disable it globally:
```elixir
# config/config.exs
config :live_svelte, ssr: false
```
Or per-component:
```heex
<.svelte name="Counter" props={%{count: @count}} socket={@socket} ssr={false} />
```
## Next Steps
- [Basic Usage](basic_usage.md) — your first `<.svelte>` component
- [Configuration](configuration.md) — all config options

52
guides/introduction.md Normal file
View file

@ -0,0 +1,52 @@
# Introduction
LiveSvelte brings end-to-end reactivity between [Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view) and [Svelte 5](https://svelte.dev/) components. Server state flows directly into Svelte components as reactive props, and user interactions in Svelte components push events back to the LiveView process — all over the existing Phoenix WebSocket.
## Three-Layer Architecture
LiveSvelte is built on three layers that work together:
```
LiveView (Elixir) → SvelteHook (Phoenix hook) → Svelte 5 (component)
server assigns reads data attrs reactive props
handle_event/3 pushEvent/handleEvent $props(), $state()
```
1. **LiveView** renders an HTML wrapper div with JSON-encoded props in `data-props`. State lives on the server.
2. **SvelteHook** (a Phoenix LiveView JS hook) mounts and updates Svelte components. It reads `data-props` on mount and applies patches on update.
3. **Svelte 5 component** receives props via `$props()` and re-renders reactively whenever the server sends updates.
## Key Features
- **Full Svelte 5 support**`$props()`, `$state()`, `$derived()`, runes syntax, snippets
- **Server-side rendering (SSR)** — Optional first-paint HTML via NodeJS (production) or ViteJS (development)
- **Efficient props diffing** — Three-tier system: change tracking → JSON Patch → ID-based list diffing
- **Phoenix Streams** — Native support for `stream()` with efficient patch operations
- **Composables**`useLiveSvelte`, `useLiveEvent`, `useLiveConnection`, `useLiveNavigation`, `useLiveForm`, `useLiveUpload`, `useEventReply`
- **TypeScript** — Full type support across Elixir and JavaScript boundaries
- **Igniter installer** — One-command setup with `mix igniter.install live_svelte`
- **Vite** — Development HMR and optimized production builds
## When to Use LiveSvelte
LiveSvelte is a good fit when you want:
- Rich, interactive UI components with real-time server state
- Svelte 5's reactive primitives and component model alongside LiveView's server logic
- Gradual adoption — mix `<.svelte>` components with regular HEEX templates
Plain LiveView may be sufficient when:
- Your UI interactions map naturally to LiveView events without needing local component state
- You don't need Svelte-specific features like `$derived()` or Svelte snippets
## Compatibility
| Dependency | Version |
|------------------|----------------------|
| LiveSvelte | 0.17.4 |
| Svelte | 5.x |
| Phoenix | 1.7+ (1.8+ for Igniter installer) |
| Phoenix LiveView | 1.0+ |
| Elixir | 1.17+ |
| Node.js | 19+ (for SSR) |

163
guides/ssr.md Normal file
View file

@ -0,0 +1,163 @@
# Server-Side Rendering
LiveSvelte supports server-side rendering (SSR) of Svelte components, which provides meaningful HTML on the first paint before the JavaScript bundle loads.
## How SSR Works
When SSR is active, the initial (dead) render calls Node.js to execute Svelte's `render()` function server-side. The result is embedded directly in the HTML response:
1. LiveSvelte calls `SSR.render(component_name, props, slots)`
2. `render()` returns `%{"head" => "<style>...</style>", "html" => "<div>...</div>", "css" => %{"code" => "..."}}`
3. The `head` and CSS styles are included in the page; `html` is placed inside the `data-svelte-target` div
4. When JavaScript loads, the `SvelteHook` hydrates the existing DOM instead of mounting fresh
## SSR Modes
LiveSvelte has two SSR modules for different environments:
### NodeJS Mode (Production)
Uses [`elixir-nodejs`](https://github.com/revelrylabs/elixir-nodejs) to run a pool of Node.js workers that execute the SSR bundle.
```elixir
# config/prod.exs
config :live_svelte,
ssr_module: LiveSvelte.SSR.NodeJS,
ssr: true
```
The SSR bundle is built by:
```bash
mix assets.js # runs: npx vite build --config vite.ssr.config.js
```
This produces `priv/svelte/server.js`, which the NodeJS supervisor loads on application start.
### ViteJS Mode (Development)
Forwards SSR requests to the Vite dev server over HTTP. This provides instant HMR without rebuilding the SSR bundle on every change.
```elixir
# config/dev.exs
config :live_svelte,
ssr_module: LiveSvelte.SSR.ViteJS,
vite_host: "http://localhost:5173"
```
> #### ViteJS Mode Requires Vite Dev Server {: .warning}
>
> `LiveSvelte.SSR.ViteJS` only works when `mix phx.server` is running alongside the Vite dev server started by Phoenix's watchers. If the Vite server is not running, SSR will silently fall back to client-only rendering.
## Configuration
Enable/disable SSR globally:
```elixir
# config/config.exs
config :live_svelte, ssr: true # enabled (default)
config :live_svelte, ssr: false # disabled
```
Select SSR module:
```elixir
config :live_svelte, ssr_module: LiveSvelte.SSR.NodeJS # production (default)
config :live_svelte, ssr_module: LiveSvelte.SSR.ViteJS # development
```
## Per-Component SSR Opt-Out
Disable SSR for a specific component:
```heex
<.svelte name="HeavyChart" props={%{data: @data}} socket={@socket} ssr={false} />
```
Components with `ssr={false}` render a loading slot or nothing on the first paint, then mount client-side normally.
## HMR in Development
When running with `LiveSvelte.SSR.ViteJS`, changes to Svelte files trigger automatic hot module replacement. The `SvelteHook` re-mounts affected components without a full page reload.
Add the `LiveSvelte.Reload` module to your layouts to enable this:
```elixir
# config/dev.exs — added by the Igniter installer
config :live_svelte,
vite_host: "http://localhost:5173"
```
Use `vite_assets/0` in your layout to include Vite's HMR client:
```heex
<!-- In your root layout (dev only) -->
<%= if Application.get_env(:live_svelte, :ssr_module) == LiveSvelte.SSR.ViteJS do %>
<LiveSvelte.Reload.vite_assets path="/assets/js/app.js" />
<% end %>
```
## Loading Slot
Show content while a component is loading (only when `ssr={false}`):
```heex
<.svelte name="SlowChart" props={%{data: @data}} socket={@socket} ssr={false}>
<:loading>
<div class="spinner">Loading chart...</div>
</:loading>
</.svelte>
```
> #### Loading Slot + SSR Incompatible {: .warning}
>
> The `:loading` slot is mutually exclusive with SSR. Using both together produces a compile warning. If SSR is active, the loading slot is ignored.
## Telemetry
LiveSvelte emits telemetry events for SSR operations:
| Event | When |
|-------|------|
| `[:live_svelte, :ssr, :start]` | SSR render begins |
| `[:live_svelte, :ssr, :stop]` | SSR render completes; includes `duration_microseconds` measurement |
| `[:live_svelte, :ssr, :exception]` | SSR render throws an exception |
Attach handlers for observability:
```elixir
:telemetry.attach(
"live-svelte-ssr",
[:live_svelte, :ssr, :stop],
fn _event, measurements, _metadata, _config ->
Logger.debug("SSR render took #{measurements.duration_microseconds}µs")
end,
nil
)
```
## Testing with SSR
SSR is disabled in the test environment by default. To write tests that verify SSR output, enable it per test suite:
```elixir
defmodule MyAppWeb.SsrTest do
use MyAppWeb.ConnCase, async: false # must be async: false
setup do
Application.put_env(:live_svelte, :ssr, true)
on_exit(fn -> Application.put_env(:live_svelte, :ssr, false) end)
:ok
end
test "renders SSR HTML", %{conn: conn} do
html = conn |> get("/counter") |> html_response(200)
assert html =~ "data-ssr=\"true\""
end
end
```
Use `get/2` + `html_response/2` for initial HTML checks — `visit/2` from PhoenixTest connects the socket and transitions past the dead render.
## Production Deployment
See [Deployment](deployment.md) for complete Node.js setup instructions for production.

153
guides/streams.md Normal file
View file

@ -0,0 +1,153 @@
# Phoenix Streams
LiveSvelte has native support for [Phoenix Streams](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream/4), enabling efficient DOM list management without holding full lists in memory on the server.
## Basic Streams
**LiveView:**
```elixir
defmodule MyAppWeb.ItemsLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, stream(socket, :items, MyApp.list_items())}
end
def handle_event("delete", %{"id" => id}, socket) do
item = MyApp.get_item!(id)
MyApp.delete_item!(item)
{:noreply, stream_delete(socket, :items, item)}
end
def render(assigns) do
~H"""
<.svelte name="ItemList" props={%{items: @streams.items}} socket={@socket} />
"""
end
end
```
**Svelte Component:**
```svelte
<!-- assets/svelte/ItemList.svelte -->
<script>
let { items } = $props()
</script>
{#each items as item (item.__dom_id)}
<div id={item.__dom_id}>
<p>{item.name}</p>
</div>
{/each}
```
> #### Use `__dom_id` as the key {: .info}
>
> Always use `item.__dom_id` as the `{#each}` key. LiveSvelte uses this to track item identity for efficient updates.
## Stream Operations
All Phoenix stream operations work automatically:
```elixir
# Insert at the end (default)
socket |> stream_insert(socket, :items, new_item)
# Insert at the beginning
socket |> stream_insert(socket, :items, new_item, at: 0)
# Delete by item (must have :id field)
socket |> stream_delete(socket, :items, item)
# Reset the entire stream
socket |> stream(socket, :items, new_items, reset: true)
```
## Efficient Stream Patches
LiveSvelte sends stream changes as compact JSON Patch operations via `data-streams-diff`, rather than re-sending the full list on every change. This makes stream updates extremely efficient — inserting a single item sends a single operation regardless of list size.
The patch operations used:
| Operation | Description |
|-----------|-------------|
| `upsert` | Insert or update an item at a specific position |
| `remove` | Delete an item by `__dom_id` |
| `replace` | Reset the entire list |
| `limit` | Trim the list to the given max size |
These are applied client-side by the `SvelteHook` before updating the Svelte component's `items` prop.
## Accessing Stream Data in Components
Streams are passed as arrays to Svelte components. Each item has all its original fields plus `__dom_id`:
```svelte
<script>
let { messages } = $props()
</script>
<ul>
{#each messages as message (message.__dom_id)}
<li id={message.__dom_id}>
<strong>{message.user}</strong>: {message.text}
</li>
{/each}
</ul>
```
## Multiple Streams
Pass multiple streams to a single component:
```elixir
def mount(_, _, socket) do
{:ok,
socket
|> stream(:messages, [])
|> stream(:users, [])}
end
def render(assigns) do
~H"""
<.svelte
name="Chat"
props={%{messages: @streams.messages, users: @streams.users}}
socket={@socket}
/>
"""
end
```
```svelte
<script>
let { messages, users } = $props()
</script>
<!-- Both streams update independently and efficiently -->
```
## Encoding Stream Items
Stream items go through `LiveSvelte.Encoder` before being sent to the client. For custom structs, add `@derive`:
```elixir
defmodule MyApp.Message do
@derive {LiveSvelte.Encoder, only: [:id, :user, :text, :inserted_at]}
defstruct [:id, :user, :text, :inserted_at]
end
```
The `@derive` restriction is enforced — fields not in `only:` are excluded even after `__dom_id` is added.
## ID-Based Diffing
For arrays where items have an `:id` field, LiveSvelte uses ID-based list diffing (Tier 3 of the props diffing system). This means:
- Inserting at position 0 sends a single `upsert` op, not N `replace` ops
- Reordering sends minimal operations
- List updates stay efficient regardless of list size
Items must have an `:id` field for ID-based diffing to activate. The `__dom_id` set by Phoenix Streams already guarantees this.

249
guides/testing.md Normal file
View file

@ -0,0 +1,249 @@
# Testing
The LiveSvelte example project has two complementary test layers: fast server-side tests via PhoenixTest, and full-stack browser tests via Wallaby.
## Build Before Testing
> #### Critical: Always Build Before Tests {: .warning}
>
> After any changes to Svelte components or JS files, always run:
>
> ```bash
> cd example_project
> mix assets.js && mix compile
> ```
>
> `mix assets.js` runs Vite builds (client + SSR). `mix compile` copies the updated SSR bundle into `_build/`. Forgetting this step is the most common cause of "my JS changes have no effect" test failures.
## PhoenixTest (Server-Side, Fast)
PhoenixTest tests validate server-side behavior without a browser. They are fast, reliable, and do not require chromedriver.
```bash
cd example_project
mix test --only phoenix_test
```
Tag test modules with `@moduletag :phoenix_test`:
```elixir
defmodule MyAppWeb.CounterTest do
use MyAppWeb.ConnCase, async: true
@moduletag :phoenix_test
import PhoenixTest
test "increments counter", %{conn: conn} do
conn
|> visit("/counter")
|> assert_has("h1", text: "Counter")
|> assert_has("[data-props*='\"count\":0']")
end
end
```
### What PhoenixTest Can Verify
- LiveView renders correct HTML (headings, labels, lists)
- `data-props` contains the expected JSON for Svelte components
- LiveView events update assigns and re-render correctly
- Server-side rendered content
### What PhoenixTest Cannot Verify
- Whether Svelte components use the props they receive
- Client-side rendering (Svelte output)
- Interactions inside Svelte-rendered elements (SSR is off in tests by default)
### Workaround for Svelte-Rendered Elements
Use `unwrap/2` to access the LiveView test process and trigger events directly:
```elixir
session
|> unwrap(fn view ->
Phoenix.LiveViewTest.render_click(view, "increment")
end)
|> assert_has("[data-props*='\"count\":1']")
```
## Wallaby E2E (Browser-Based, Full Stack)
Wallaby tests use chromedriver to run a real browser. They validate the full pipeline: LiveView → SvelteHook → Svelte component.
```bash
cd example_project
mix test --only e2e
```
Tag test modules with `@moduletag :e2e`:
```elixir
defmodule MyAppWeb.CounterE2ETest do
use MyAppWeb.FeatureCase, async: false
@moduletag :e2e
test "Svelte counter increments", %{session: session} do
session
|> visit("/counter")
|> assert_text("Count: 0")
|> click(button("Increment"))
|> assert_text("Count: 1")
end
end
```
### What Wallaby Can Verify
- Svelte components render the correct data from server props
- Client-side interactions (buttons rendered by Svelte, not LiveView)
- Full data flow from server through to Svelte re-renders
- HMR and dynamic updates
### Requirements
Wallaby requires chromedriver installed and available in `PATH`:
```bash
# Check if chromedriver is available
chromedriver --version
# On macOS with Homebrew:
brew install chromedriver
# On Ubuntu/Debian:
sudo apt-get install chromium-driver
```
## Running Both Layers
```bash
# Server-side only (fast, no browser needed)
mix assets.js && mix test --only phoenix_test
# Browser E2E only
mix assets.js && mix test --only e2e
# Everything
mix assets.js && mix test
```
## `LiveSvelte.Test` — Component Introspection
`LiveSvelte.Test` provides helper functions to inspect Svelte component props in server-side tests:
```elixir
import LiveSvelte.Test
# In a PhoenixTest or ConnCase test:
{:ok, view, html} = live(conn, "/counter")
component = get_svelte(html, name: "Counter")
assert component.name == "Counter"
assert component.props["count"] == 0
```
### `get_svelte/1` and `get_svelte/2`
```elixir
# Get first Svelte component in the HTML
component = get_svelte(html)
# Get component by name
component = get_svelte(html, name: "Counter")
# Get component by DOM id
component = get_svelte(html, id: "Counter-1")
# Get from a LiveView directly
{:ok, view, _html} = live(conn, "/counter")
component = get_svelte(view, name: "Counter")
```
The returned map has:
- `name` — component name string
- `id` — DOM id of the component wrapper
- `props` — decoded props map (string keys)
- `slots` — map of slot name → HTML string
- `ssr` — boolean, whether SSR was used
### Example: Asserting Props after Events
```elixir
test "props update after event", %{conn: conn} do
{:ok, view, html} = live(conn, "/counter")
# Initial props
assert get_svelte(html, name: "Counter").props["count"] == 0
# Trigger event
html = render_click(view, "increment")
# Updated props
assert get_svelte(html, name: "Counter").props["count"] == 1
end
```
## Vitest (JavaScript Unit Tests)
JavaScript composables and utilities have unit tests using Vitest:
```bash
cd example_project/assets
npm test # Run tests once
npm run test:watch # Watch mode
```
Test files are colocated with source files (`*.test.ts`).
## Tagging Convention
Use `@moduletag` (not `@tag`) for consistent filtering:
```elixir
# ✅ Correct — applies to ALL tests in the module
@moduletag :phoenix_test
# ❌ Wrong — only applies to the NEXT test
@tag :phoenix_test
```
## Test File Layout
```
example_project/test/example_web/
├── phoenix_test/ # PhoenixTest (server-side)
│ ├── hello_world_test.exs
│ ├── live_struct_test.exs
│ └── ...
└── live/ # Wallaby E2E (browser)
├── live_struct_test.exs
└── ...
```
## SSR Testing
SSR is off in tests by default. To test SSR output:
```elixir
defmodule MyAppWeb.SsrTest do
use MyAppWeb.ConnCase, async: false # SSR state is global — must use async: false
setup do
Application.put_env(:live_svelte, :ssr, true)
on_exit(fn -> Application.put_env(:live_svelte, :ssr, false) end)
:ok
end
test "renders SSR HTML on first request", %{conn: conn} do
# Use get/html_response for dead render — NOT visit/2
html = conn |> get("/counter") |> html_response(200)
assert html =~ ~s(data-ssr="true")
assert html =~ "<div" # SSR produced some HTML
end
end
```
> Use `get/2` + `html_response/2` for SSR checks. `visit/2` from PhoenixTest connects the LiveView socket and transitions past the initial dead render.

218
guides/troubleshooting.md Normal file
View file

@ -0,0 +1,218 @@
# Troubleshooting
Common issues encountered when using LiveSvelte, and how to resolve them.
## "My JS Changes Have No Effect"
**Symptom:** You modified a Svelte component or JavaScript file, but the browser still shows old behavior.
**Cause:** Vite hasn't rebuilt the assets, so the browser is loading stale bundles.
**Fix:**
```bash
cd example_project
mix assets.js && mix compile
```
`mix assets.js` runs Vite builds (client + SSR). `mix compile` copies the updated SSR bundle into `_build/`. Both steps are required after any change to `assets/`.
## SSR Renders Stale HTML
**Symptom:** Server-side rendered HTML shows old component output even after updating Svelte files.
**Cause:** `_build/test/lib/example/priv/svelte/server.js` is a **copy** (not a symlink) of `priv/svelte/server.js`. It is updated by `mix compile`, not by `mix assets.js` alone.
**Fix:**
```bash
mix assets.js && mix compile
```
If you're seeing stale SSR in tests, ensure the `on_exit` cleanup properly resets SSR state.
## Svelte CSS Overwrites Tailwind's `app.css`
**Symptom:** After building, `priv/static/assets/app.css` contains only Svelte component styles, and Tailwind styles are missing.
**Cause:** The Svelte Vite plugin is extracting component CSS to a file, which overwrites the Tailwind output.
**Fix:** Ensure your `vite.config.mjs` passes `css: "injected"` to the Svelte plugin:
```js
import { svelte } from "@sveltejs/vite-plugin-svelte"
export default defineConfig({
plugins: [
svelte({
compilerOptions: {
css: "injected" // ← This is required
}
}),
// ...
]
})
```
This injects Svelte component CSS directly into the JS bundle instead of extracting it to a separate file.
## Component Not Found / `virtual:live-svelte-components` Resolution Fails
**Symptom:** Browser console shows `Error: Failed to resolve module 'virtual:live-svelte-components'` or a specific component name is not found.
**Cause:** The `liveSveltePlugin` is missing from one or both Vite configs.
**Fix:** Ensure `liveSveltePlugin()` is in **both** `vite.config.mjs` (client build) and `vite.ssr.config.js` (SSR build):
```js
// assets/vite.config.mjs
import { liveSveltePlugin } from "live_svelte/vitePlugin"
plugins: [svelte(), liveSveltePlugin()]
// assets/vite.ssr.config.js
import { liveSveltePlugin } from "live_svelte/vitePlugin"
plugins: [svelte(), liveSveltePlugin()]
```
Also verify that your Svelte files are in `assets/svelte/` and have the `.svelte` extension.
## Wallaby E2E Tests Fail
**Symptom:** Wallaby tests fail with a browser connection error or chromedriver not found.
**Cause:** chromedriver is not installed or not in `PATH`.
**Fix:**
```bash
# Check installation
chromedriver --version
# Install on macOS
brew install chromedriver
# Install on Ubuntu/Debian
sudo apt-get install chromium-driver
# Or use a specific version with webdriver-manager
npm install -g webdriver-manager
webdriver-manager update
```
Also ensure `mix assets.js` has been run before E2E tests — the browser needs the built JS to function.
## `mix live_svelte.install` Says "Task Not Found"
**Symptom:** Running `mix live_svelte.install` prints "The task 'live_svelte.install' could not be found".
**Cause:** The `:igniter` dependency is not in your project's deps, or `mix deps.get` hasn't been run.
**Fix:**
```bash
# Ensure igniter is installed
mix archive.install hex igniter_new
# Then install using igniter:
mix igniter.install live_svelte
```
If you added `{:live_svelte, ...}` to `mix.exs` and ran `mix deps.get` manually, also run:
```bash
mix deps.compile
```
## `import LiveSvelte` Missing from html_helpers
**Symptom:** Using `<.svelte>` in a LiveView template produces `function component svelte/1 is undefined`.
**Cause:** `import LiveSvelte` was not added to the web module's `html_helpers`.
**Fix:** Add it manually in `lib/my_app_web.ex`:
```elixir
defp html_helpers do
quote do
# ... existing imports ...
import LiveSvelte
end
end
```
> Note: use `import LiveSvelte`, not `use LiveSvelte`.
## Props Not Reactive After Navigation
**Symptom:** After navigating within the same LiveView, Svelte component props stop updating.
**Cause:** If `phx-update="ignore"` is missing or incorrectly overridden on the component wrapper, LiveView will re-patch Svelte's DOM on updates, breaking reactivity.
**Fix:** Do not add `phx-update` attributes to the `<.svelte>` component call — LiveSvelte sets it automatically on the wrapper div. If you are nesting the component inside a container with `phx-update`, ensure that container is set correctly:
```heex
<!-- ✅ Correct — let LiveSvelte manage its own wrapper -->
<.svelte name="Counter" props={%{count: @count}} socket={@socket} />
<!-- ❌ Wrong — wrapping in a plain div that LiveView re-renders -->
<div>
<.svelte name="Counter" props={%{count: @count}} socket={@socket} />
</div>
```
For containers that wrap multiple components, use `phx-update="ignore"` on the outer container if it should not be re-rendered, or ensure each component has a stable `id`.
## NodeJS.Supervisor Not Starting
**Symptom:** Application fails to start with `NodeJS.Supervisor` error, or SSR silently fails in production.
**Cause:** `ssr_module` is not set to `LiveSvelte.SSR.NodeJS` in production config, or the SSR bundle (`priv/svelte/server.js`) is missing.
**Fix:**
1. Ensure `config/prod.exs` has:
```elixir
config :live_svelte, ssr_module: LiveSvelte.SSR.NodeJS
```
2. Ensure the SSR bundle was built:
```bash
MIX_ENV=prod mix assets.js && MIX_ENV=prod mix compile
```
3. Check that `NodeJS.Supervisor` is in `application.ex`:
```elixir
{NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]}
```
## Svelte 4 Syntax Errors
**Symptom:** Component fails with unexpected syntax errors like `export let` or `<script context="module">`.
**Cause:** The component uses Svelte 4 syntax which is not supported in LiveSvelte's Svelte 5 setup.
**Fix:** Migrate to Svelte 5 runes:
| Svelte 4 | Svelte 5 |
|----------|----------|
| `export let count` | `let { count } = $props()` |
| `let x = 0` (reactive) | `let x = $state(0)` |
| `$: doubled = x * 2` | `let doubled = $derived(x * 2)` |
| `import { onMount } from 'svelte'` | same (unchanged) |
## SSR Not Working in Development
**Symptom:** Components render without SSR HTML even with `ssr: true` in config.
**Cause:** `vite_host` is not reachable or Vite dev server isn't running.
**Fix:** Ensure `mix phx.server` starts the Vite watcher. Check `config/dev.exs`:
```elixir
config :live_svelte,
ssr_module: LiveSvelte.SSR.ViteJS,
vite_host: "http://localhost:5173"
```
And verify your `config/dev.exs` Phoenix watcher runs `vite`:
```elixir
config :my_app, MyAppWeb.Endpoint,
watchers: [
node: ["node_modules/.bin/vite", cd: Path.expand("../assets", __DIR__)]
]
```

224
guides/uploads.md Normal file
View file

@ -0,0 +1,224 @@
# File Uploads
LiveSvelte provides `useLiveUpload` for integrating Phoenix LiveView's file upload system with Svelte components.
## Quick Example
**LiveView:**
```elixir
defmodule MyAppWeb.UploadLive do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:uploaded_files, [])
|> allow_upload(:avatar, accept: ~w(.jpg .png), max_entries: 1)}
end
def handle_event("validate", _params, socket) do
{:noreply, socket}
end
def handle_event("submit", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
dest = Path.join("priv/static/uploads", Path.basename(path))
File.cp!(path, dest)
{:ok, "/uploads/#{Path.basename(dest)}"}
end)
{:noreply,
socket
|> update(:uploaded_files, &(&1 ++ uploaded_files))
|> put_flash(:info, "Uploaded successfully!")}
end
def render(assigns) do
~H"""
<.svelte
name="AvatarUpload"
props={%{uploads: @uploads}}
socket={@socket}
/>
"""
end
end
```
**Svelte Component:**
```svelte
<!-- assets/svelte/AvatarUpload.svelte -->
<script>
import { useLiveUpload } from "live_svelte"
let { uploads } = $props()
const {
showFilePicker,
entries,
submit,
cancel,
clear,
sync
} = useLiveUpload(uploads.avatar, { changeEvent: "validate", submitEvent: "submit" })
// Keep the composable in sync when the server pushes updated upload config
$effect(() => sync(uploads.avatar))
</script>
<div
role="button"
tabindex="0"
onclick={showFilePicker}
onkeydown={(e) => e.key === "Enter" && showFilePicker()}
>
Click to select a file (or drag and drop)
</div>
{#each $entries as entry (entry.ref)}
<div>
<p>{entry.client_name}</p>
<!-- Progress bar -->
<progress value={entry.progress} max="100">{entry.progress}%</progress>
<!-- Validation errors -->
{#each entry.errors as error}
<p class="error">{error}</p>
{/each}
<button type="button" onclick={() => cancel(entry.ref)}>Remove</button>
</div>
{/each}
<button onclick={submit} disabled={$entries.length === 0}>Upload</button>
```
## The `useLiveUpload` Composable
```ts
import { useLiveUpload } from "live_svelte"
const { showFilePicker, entries, submit, cancel, clear, sync } = useLiveUpload(
uploads.avatar,
{ changeEvent: "validate", submitEvent: "submit" }
)
// Sync updated config from server on every render
$effect(() => sync(uploads.avatar))
```
The first argument is the **upload config object** for a specific upload field (e.g., `uploads.avatar`). Pass it directly — not as a getter function.
Call `sync(uploads.avatar)` in a `$effect` to keep the composable up-to-date whenever the server sends an updated config.
> `useLiveUpload` creates a hidden `<form>` and `<input type="file">` internally and appends them to the LiveView element. You do not need to add a form in your Svelte template.
### Options
```ts
interface UploadOptions {
changeEvent?: string // Server event for phx-change (validation). Optional.
submitEvent: string // Server event for phx-submit. REQUIRED.
}
```
## Return Values
| Value | Type | Description |
|-------|------|-------------|
| `showFilePicker()` | `() => void` | Opens the native file picker dialog |
| `addFiles(files)` | `(files: File[] \| DataTransfer) => void` | Enqueue files programmatically (for drag-drop) |
| `entries` | `Readable<UploadEntry[]>` | Reactive store of current upload entries. Use `$entries` in templates. |
| `progress` | `Readable<number>` | Overall upload progress 0100 averaged across all entries |
| `valid` | `Readable<boolean>` | `true` when the upload config has no top-level errors |
| `submit()` | `() => void` | Dispatch a form submit event to trigger Phoenix upload |
| `cancel(ref?)` | `(ref?: string) => void` | Cancel entry by ref string, or all entries when called with no arg |
| `clear()` | `() => void` | Reset the hidden input to clear the file queue |
| `sync(config)` | `(config: UploadConfig) => void` | Merge updated config from server. Call in `$effect`. |
## Upload Entry Fields
Each entry in `entries` has:
| Field | Type | Description |
|-------|------|-------------|
| `ref` | `string` | Unique entry identifier |
| `client_name` | `string` | Original filename |
| `client_size` | `number` | File size in bytes |
| `client_type` | `string` | MIME type |
| `progress` | `number` | Upload progress (0100) |
| `errors` | `string[]` | Validation error messages |
| `valid` | `boolean` | Whether entry passes validation |
| `done` | `boolean` | Whether upload is complete |
| `preflighted` | `boolean` | Whether Phoenix has acknowledged (preflighted) this entry |
## Drag and Drop
```svelte
<script>
import { useLiveUpload } from "live_svelte"
let { uploads } = $props()
const { entries, cancel, sync } = useLiveUpload(uploads.avatar, { submitEvent: "submit" })
$effect(() => sync(uploads.avatar))
let dragOver = $state(false)
</script>
<div
class={dragOver ? "drag-over" : ""}
ondragover={(e) => { e.preventDefault(); dragOver = true }}
ondragleave={() => { dragOver = false }}
ondrop={(e) => {
e.preventDefault()
dragOver = false
// Phoenix LiveView handles the drop via phx-drop-target
}}
phx-drop-target={uploads.avatar?.ref}
>
Drop files here
</div>
```
## Multiple Files
Configure `max_entries` on the LiveView side:
```elixir
allow_upload(:photos, accept: ~w(.jpg .png .gif), max_entries: 5)
```
The `entries` array in the component will reflect all selected files.
## Validation
File validation is configured with `allow_upload/3` options:
```elixir
allow_upload(:avatar,
accept: ~w(.jpg .png .webp),
max_entries: 1,
max_file_size: 10_000_000 # 10 MB
)
```
Validation errors appear in `entry.errors` as human-readable strings.
## Progress Tracking
Upload progress is automatically tracked per entry via `entry.progress` (0100):
```svelte
{#each $entries as entry (entry.ref)}
<div class="upload-item">
<span>{entry.client_name}</span>
<div class="progress-bar" style="width: {entry.progress}%"></div>
{#if entry.done}
<span>✓ Complete</span>
{/if}
</div>
{/each}
```

View file

@ -1,28 +0,0 @@
defmodule LiveSvelte.LiveJson do
use Phoenix.Component
attr(
:live_json_props,
:map,
default: %{},
doc: "LiveJSON props to pass to the svelte component"
)
attr(
:svelte_id,
:string,
required: true,
doc: "Stable DOM id from the parent svelte component"
)
slot(:inner_block)
def live_json(assigns) do
~H"""
<%= if @live_json_props != %{} do %>
<div id={"lj-#{@svelte_id}"} phx-hook="LiveJSON" />
<% end %>
<%= render_slot(@inner_block) %>
"""
end
end

View file

@ -7,7 +7,6 @@ defmodule LiveSvelte do
use Phoenix.Component
import Phoenix.HTML
import LiveSvelte.LiveJson
alias Phoenix.LiveView
alias Phoenix.LiveView.LiveStream
@ -55,11 +54,6 @@ defmodule LiveSvelte do
default: nil,
doc: "LiveView socket, only needed when ssr: true"
attr :live_json_props, :map,
default: %{},
doc: "LiveJson props to pass to the Svelte component",
examples: [%{my_big_data_set: %{some_data: 1}}]
attr :diff, :boolean,
default: true,
doc:
@ -120,11 +114,7 @@ defmodule LiveSvelte do
ssr_code =
if init and dead and ssr_active and assigns.ssr do
try do
props =
Map.merge(
Map.get(assigns, :props, %{}),
Map.get(assigns, :live_json_props, %{})
)
props = Map.get(assigns, :props, %{})
SSR.render(assigns.name, props, slots)
rescue
@ -146,36 +136,31 @@ defmodule LiveSvelte do
|> assign(:streams_diff, streams_diff)
~H"""
<.live_json live_json_props={@live_json_props} svelte_id={@svelte_id}>
<script>
<script>
<%= raw(@ssr_render["head"]) %>
</script>
<div
id={@svelte_id}
data-name={@name}
data-props={json(@props_to_send)}
data-props-diff={json(@props_diff)}
data-streams-diff={json(@streams_diff)}
data-use-diff={to_string(@use_diff)}
data-ssr={@ssr_render != nil}
data-slots={@slots |> Slots.base_encode_64() |> json}
phx-hook="SvelteHook"
phx-update="ignore"
class={@class}
>
<div id={"#{@svelte_id}-target"} data-svelte-target>
<%= raw(@ssr_render["head"]) %>
</script>
<div
id={@svelte_id}
data-name={@name}
data-props={json(@props_to_send)}
data-props-diff={json(@props_diff)}
data-streams-diff={json(@streams_diff)}
data-use-diff={to_string(@use_diff)}
data-ssr={@ssr_render != nil}
data-live-json={
if @init, do: json(@live_json_props), else: @live_json_props |> Map.keys() |> json()
}
data-slots={@slots |> Slots.base_encode_64() |> json}
phx-hook="SvelteHook"
phx-update="ignore"
class={@class}
>
<div id={"#{@svelte_id}-target"} data-svelte-target>
<%= raw(@ssr_render["head"]) %>
<style>
<%= raw(@ssr_render["css"]["code"]) %>
</style>
<%= raw(@ssr_render["html"]) %>
<%= render_slot(@loading) %>
</div>
<style>
<%= raw(@ssr_render["css"]["code"]) %>
</style>
<%= raw(@ssr_render["html"]) %>
<%= render_slot(@loading) %>
</div>
</.live_json>
</div>
"""
end

38
mix.exs
View file

@ -26,12 +26,44 @@ defmodule LiveSvelte.MixProject do
source_ref: "v#{@version}",
source_url: @repo_url,
homepage_url: @repo_url,
main: "LiveSvelte",
main: "readme",
logo: "logo_3.png",
links: %{
"GitHub" => @repo_url,
"Sponsor" => "https://github.com/sponsors/woutdp"
}
},
extras: [
"README.md": [title: "LiveSvelte"],
# Getting Started
"guides/installation.md": [title: "Installation"],
"guides/basic_usage.md": [title: "Basic Usage"],
# Core Usage
"guides/forms.md": [title: "Forms and Validation"],
"guides/uploads.md": [title: "File Uploads"],
"guides/streams.md": [title: "Phoenix Streams"],
"guides/ssr.md": [title: "Server-Side Rendering"],
"guides/configuration.md": [title: "Configuration"],
# Reference
"guides/api_reference.md": [title: "API Reference"],
# Advanced Topics
"guides/introduction.md": [title: "Introduction"],
"guides/testing.md": [title: "Testing"],
"guides/deployment.md": [title: "Deployment"],
# Help & Troubleshooting
"guides/troubleshooting.md": [title: "Troubleshooting"]
],
groups_for_extras: [
"Getting Started": ~r/guides\/(installation|basic_usage)/,
"Core Usage": ~r/guides\/(forms|uploads|streams|ssr|configuration)/,
Reference: ~r/guides\/api_reference/,
"Advanced Topics": ~r/guides\/(introduction|testing|deployment)/,
"Help & Troubleshooting": ~r/guides\/troubleshooting/
]
]
]
end
@ -45,7 +77,7 @@ defmodule LiveSvelte.MixProject do
GitHub: @repo_url
},
files:
~w(assets/copy/tsconfig.json assets/js lib mix.exs package.json .formatter.exs LICENSE.md README.md CHANGELOG.md)
~w(assets/copy/tsconfig.json assets/js guides lib mix.exs package.json .formatter.exs LICENSE.md README.md CHANGELOG.md)
]
end

177
package-lock.json generated
View file

@ -9,38 +9,31 @@
"version": "0.17.4",
"license": "MIT",
"devDependencies": {
"prettier": "3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"svelte": "^5.1.13"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.1",
"svelte": "^5.53.7"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@ -53,16 +46,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
@ -71,9 +54,9 @@
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -81,17 +64,34 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
}
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true,
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
@ -102,20 +102,10 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-typescript": {
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": ">=8.9.0"
}
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@ -132,32 +122,48 @@
"node": ">= 0.4"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/devalue": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
"dev": true,
"license": "MIT"
},
"node_modules/esm-env": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.1.4.tgz",
"integrity": "sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"dev": true,
"license": "MIT"
},
"node_modules/esrap": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz",
"integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15",
"@types/estree": "^1.0.1"
"@jridgewell/sourcemap-codec": "^1.4.15"
}
},
"node_modules/is-reference": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*"
"@types/estree": "^1.0.6"
}
},
"node_modules/locate-character": {
@ -178,9 +184,9 @@
}
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"peer": true,
@ -195,9 +201,9 @@
}
},
"node_modules/prettier-plugin-svelte": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.7.tgz",
"integrity": "sha512-/Dswx/ea0lV34If1eDcG3nulQ63YNr5KPDfMsjbdtpSWOxKKJ7nAc2qlVuYwEvCr4raIuredNoR7K4JCkmTGaQ==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz",
"integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -206,23 +212,26 @@
}
},
"node_modules/svelte": {
"version": "5.1.13",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.13.tgz",
"integrity": "sha512-xVNk8yLsZNfkyqWzVg8+nfU9ewiSjVW0S4qyTxfKa6Y7P5ZBhA+LDsh2cHWIXJQMltikQAk6W3sqGdQZSH58PA==",
"version": "5.53.7",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.7.tgz",
"integrity": "sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"esm-env": "^1.0.0",
"esrap": "^1.2.2",
"is-reference": "^3.0.2",
"clsx": "^2.1.1",
"devalue": "^5.6.3",
"esm-env": "^1.2.1",
"esrap": "^2.2.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"

View file

@ -9,9 +9,9 @@
"format": "npx prettier --write ."
},
"devDependencies": {
"prettier": "3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"svelte": "^5.1.13"
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.1",
"svelte": "^5.53.7"
},
"exports": {
".": {

View file

@ -14,7 +14,6 @@ defmodule LiveSvelte.AutoIdTest do
id: Keyword.get(opts, :id),
key: Keyword.get(opts, :key),
props: Keyword.get(opts, :props, %{}),
live_json_props: %{},
ssr: false,
class: nil,
loading: [],
@ -298,19 +297,9 @@ defmodule LiveSvelte.AutoIdTest do
end
# ---------------------------------------------------------------------------
# Tests — live_json and phx-update (unchanged from before)
# Tests — phx-update
# ---------------------------------------------------------------------------
describe "live_json id derivation" do
test "live_json div id is prefixed with lj- and uses the svelte id" do
html = render_html(base_assigns("Counter", props: %{x: 1}))
# When live_json_props is empty, no lj- div is rendered.
# Verify the component id is still correct.
assert extract_id(html) == "Counter"
end
end
describe "phx-update attribute" do
test "outer hook container has phx-update=ignore to protect Svelte DOM" do
html = render_html(base_assigns("Counter"))

View file

@ -21,7 +21,6 @@ defmodule LiveSvelte.JSONLibraryTest do
socket: nil,
name: "Test",
props: data,
live_json_props: %{},
ssr: false,
class: nil,
loading: [],
@ -58,7 +57,6 @@ defmodule LiveSvelte.JSONLibraryTest do
socket: nil,
name: "Test",
props: data,
live_json_props: %{},
ssr: false,
class: nil,
loading: [],
@ -87,7 +85,6 @@ defmodule LiveSvelte.JSONLibraryTest do
socket: nil,
name: "Legacy",
props: data,
live_json_props: %{},
ssr: false,
class: nil,
loading: [],
@ -120,7 +117,6 @@ defmodule LiveSvelte.JSONLibraryTest do
socket: nil,
name: "StructTest",
props: data,
live_json_props: %{},
ssr: false,
class: nil,
loading: [],

View file

@ -145,7 +145,7 @@ defmodule LiveSvelte.JSONTest do
assert decoded["2"] == "b"
end
test "encodes large maps with integer keys (LiveJson scenario)" do
test "encodes large maps with integer keys" do
data = for i <- 1..100, into: %{}, do: {i, i * 2}
result = JSON.encode!(data)
decoded = :json.decode(result)

View file

@ -10,7 +10,6 @@ defmodule LiveSvelte.PropsDiffTest do
id: nil,
key: nil,
props: Keyword.get(opts, :props, %{}),
live_json_props: %{},
ssr: false,
class: nil,
loading: [],

View file

@ -31,7 +31,6 @@ defmodule LiveSvelte.StreamsTest do
id: nil,
key: nil,
props: %{},
live_json_props: %{},
ssr: false,
class: nil,
loading: [],