mirror of
https://github.com/woutdp/live_svelte
synced 2026-05-24 09:28:21 +00:00
216 lines
5.4 KiB
Markdown
216 lines
5.4 KiB
Markdown
|
|
# 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.
|