Added better navigation between examples projects (#202)

* Added better navigation between examples projects. 
* Fix json date formatter
This commit is contained in:
Denis Donici 2026-02-01 00:14:15 +02:00 committed by GitHub
parent 1f9ebc49d2
commit 6829effefd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 945 additions and 82 deletions

View file

@ -40,3 +40,8 @@ npm-debug.log
# Ignore ssr build for svelte.
/priv/svelte/
# SQLite databases
*.db
*.db-shm
*.db-wal

View file

@ -25,7 +25,7 @@
}
},
"../..": {
"version": "0.15.0-rc.6",
"version": "0.17.0",
"license": "MIT",
"devDependencies": {
"prettier": "3.3.3",
@ -38,7 +38,7 @@
"license": "MIT"
},
"../deps/phoenix": {
"version": "1.7.14",
"version": "1.7.21",
"license": "MIT"
},
"../deps/phoenix_html": {
@ -581,6 +581,7 @@
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -735,6 +736,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@ -1198,6 +1200,7 @@
"integrity": "sha512-MuzIIVRSbc8XxHH7FjkvWqkIcr1BvoMZoR/oFuAJDlh7VSaNJzrB4uJ38GRQa+mWjLXODAMzeDe0xi9GYbGwnw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"css": "^3.0.0",
"debug": "~3.1.0",
@ -1221,6 +1224,7 @@
"integrity": "sha512-56Vd/nwJrljV0w7RCV1A8sB4/yjSbWW5qrGDTAzp7q42OxwqEWT+6obWzDt41tHjIW+C9Fs2ygtejjJrXR+ZPA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
@ -1316,6 +1320,7 @@
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View file

@ -0,0 +1,239 @@
<script>
import { flip } from "svelte/animate"
import {fly, fade} from "svelte/transition"
/**
* @typedef {Object} Note
* @property {string} id
* @property {string} title
* @property {string|null} content
* @property {string} color
* @property {string} inserted_at
*/
/** @type {{notes: Note[], encoder: string, info: string, live: any}} */
let {notes: propNotes = [], encoder = "OTP", info = "", live} = $props()
// Use local reactive state for notes - this helps Svelte track changes for transitions
let notes = $state([])
// Sync props to local state using in-place mutations to preserve $state proxy identity.
// This is critical for animations - replacing the array would cause all items to re-animate.
$effect(() => {
const currentIds = new Set(propNotes.map((p) => p.id));
// 1. Remove deleted items (iterate backwards to avoid index shift issues)
for (let i = notes.length - 1; i >= 0; i--) {
if (!currentIds.has(notes[i].id)) {
notes.splice(i, 1);
}
}
// 2. Update existing items and add new ones in correct order
for (let i = 0; i < propNotes.length; i++) {
const p = propNotes[i];
const existingIndex = notes.findIndex((n) => n.id === p.id);
if (existingIndex !== -1) {
// Update existing item in place (no animation triggered)
notes[existingIndex].title = p.title;
notes[existingIndex].content = p.content;
notes[existingIndex].color = p.color;
notes[existingIndex].inserted_at = p.inserted_at;
// Move to correct position if needed (triggers flip animation)
if (existingIndex !== i) {
const [item] = notes.splice(existingIndex, 1);
notes.splice(i, 0, item);
}
} else {
// Insert new item at correct position (triggers enter animation)
notes.splice(i, 0, { ...p });
}
}
})
let title = $state("")
let content = $state("")
let color = $state("#fef3c7")
const colors = [
{value: "#fef3c7", name: "Amber"},
{value: "#dcfce7", name: "Green"},
{value: "#dbeafe", name: "Blue"},
{value: "#fce7f3", name: "Pink"},
{value: "#f3e8ff", name: "Purple"},
{value: "#fff", name: "White"},
]
function handleSubmit() {
if (!title.trim()) return
live.pushEvent("create_note", {
title: title.trim(),
content: content.trim(),
color,
})
title = ""
content = ""
color = "#fef3c7"
}
/**
* @param {string} id
*/
function handleDelete(id) {
live.pushEvent("delete_note", {id})
}
/**
* @param {string} uuid
*/
function truncateUUID(uuid) {
return uuid ? uuid.substring(0, 8) + "..." : ""
}
/**
* @param {string} dateStr
*/
function formatDate(dateStr) {
if (!dateStr) return ""
const date = new Date(dateStr)
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})
}
</script>
<svelte:head>
<title>Notes ({encoder})</title>
</svelte:head>
<div class="max-w-4xl mx-auto p-4">
<!-- Info Banner -->
<div class="mb-6 p-4 rounded-lg bg-blue-50 border border-blue-200">
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-1 text-xs font-semibold rounded bg-blue-600 text-white">
{encoder} JSON Encoder
</span>
</div>
<p class="text-sm text-blue-800">{info}</p>
</div>
<!-- Create Note Form -->
<form
onsubmit={e => {
e.preventDefault()
handleSubmit()
}}
class="mb-8 p-4 bg-white rounded-lg shadow-sm border"
>
<h2 class="text-lg font-semibold mb-4">Create Note</h2>
<div class="space-y-4">
<div>
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Title *</label>
<input
id="title"
type="text"
bind:value={title}
placeholder="Enter note title"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
required
/>
</div>
<div>
<label for="content" class="block text-sm font-medium text-gray-700 mb-1">Content</label>
<textarea
id="content"
bind:value={content}
placeholder="Enter note content (optional)"
rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
></textarea>
</div>
<div>
<label for="color" class="block text-sm font-medium text-gray-700 mb-2">Color</label>
<div class="flex gap-2 flex-wrap">
{#each colors as c}
<button
aria-label={c.name}
id="color"
type="button"
onclick={() => (color = c.value)}
class="w-8 h-8 rounded-full border-2 transition-transform hover:scale-110"
class:ring-2={color === c.value}
class:ring-offset-2={color === c.value}
class:ring-blue-500={color === c.value}
style="background-color: {c.value}; border-color: {c.value === '#fff' ? '#e5e7eb' : c.value}"
title={c.name}
></button>
{/each}
</div>
</div>
<button type="submit" class="px-4 py-2 bg-zinc-900 text-white rounded-md hover:bg-zinc-700 transition-colors">
Add Note
</button>
</div>
</form>
<!-- Notes Grid
in:fly|global={{ x: -200, duration: 300 }}
out:fade|global={{ duration: 200 }}
-->
<ul class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each notes as note, index (note.id)}
<li
animate:flip={{ delay: 500 }}
role="listitem"
id={`note-${note.id}`}
aria-label={`Note ${index + 1}`}
class="p-4 rounded-lg shadow-sm border transition-shadow hover:shadow-md"
style="background-color: {note.color}"
>
<div class="flex justify-between items-start mb-2">
<h3 class="font-semibold text-gray-900 break-words flex-1 mr-2">{note.title}</h3>
<button
aria-label="Delete note"
onclick={() => handleDelete(note.id)}
class="text-gray-500 hover:text-red-600 transition-colors p-1"
title="Delete note"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
{#if note.content}
<p class="text-sm text-gray-700 mb-3 break-words">{note.content}</p>
{/if}
<div class="flex justify-between items-center text-xs text-gray-500 pt-2 border-t border-gray-200/50">
<span class="font-mono" title={note.id}>ID: {truncateUUID(note.id)}</span>
<span>{formatDate(note.inserted_at)}</span>
</div>
</li>
{:else}
<li class="col-span-full text-center py-12 text-gray-500">
<p class="text-lg">No notes yet</p>
<p class="text-sm">Create your first note above!</p>
</li>
{/each}
</ul>
<!-- Notes Count -->
{#if notes.length > 0}
<div class="mt-6 text-center text-sm text-gray-500">
{notes.length} note{notes.length === 1 ? "" : "s"}
</div>
{/if}
</div>

View file

@ -2,13 +2,10 @@ import Config
# Configure your database
config :example, Example.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "example_dev",
database: Path.expand("../example_dev.db", __DIR__),
pool_size: 5,
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
show_sensitive_data_on_connection_error: true
# For development, we disable any cache and enable
# debugging and code reloading.

View file

@ -6,12 +6,9 @@ import Config
# to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information.
config :example, Example.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "example_test#{System.get_env("MIX_TEST_PARTITION")}",
database: Path.expand("../example_test#{System.get_env("MIX_TEST_PARTITION")}.db", __DIR__),
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: 10
pool_size: 5
# We don't run a server during test. If one is required,
# you can enable the server option below.

View file

@ -11,8 +11,8 @@ defmodule Example.Application do
{NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]},
# Start the Telemetry supervisor
ExampleWeb.Telemetry,
# Start the Ecto repository (actually not used in this example, so skip it)
# Example.Repo,
# Start the Ecto repository
Example.Repo,
# Start the PubSub system
{Phoenix.PubSub, name: Example.PubSub},
# Start Finch

View file

@ -0,0 +1,28 @@
defmodule Example.Note do
@moduledoc """
Ecto schema for notes with UUID primary key.
This schema demonstrates how Ecto structs work with LiveSvelte's
OTP JSON encoder, which automatically converts structs to maps.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
schema "notes" do
field :title, :string
field :content, :string
field :color, :string, default: "#fef3c7"
timestamps(type: :utc_datetime)
end
@doc false
def changeset(note, attrs) do
note
|> cast(attrs, [:title, :content, :color])
|> validate_required([:title])
|> validate_length(:title, max: 100)
|> validate_length(:content, max: 1000)
end
end

View file

@ -0,0 +1,55 @@
defmodule Example.Notes do
@moduledoc """
Context module for managing notes.
"""
import Ecto.Query
alias Example.Repo
alias Example.Note
@doc """
Returns all notes ordered by creation date (newest first).
"""
def list_notes do
Note
|> order_by(desc: :inserted_at)
|> Repo.all()
end
@doc """
Gets a single note by ID.
Raises `Ecto.NoResultsError` if the note does not exist.
"""
def get_note!(id), do: Repo.get!(Note, id)
@doc """
Creates a new note.
"""
def create_note(attrs \\ %{}) do
%Note{}
|> Note.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates an existing note.
"""
def update_note(%Note{} = note, attrs) do
note
|> Note.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a note.
"""
def delete_note(%Note{} = note) do
Repo.delete(note)
end
@doc """
Returns a changeset for tracking note changes.
"""
def change_note(%Note{} = note, attrs \\ %{}) do
Note.changeset(note, attrs)
end
end

View file

@ -1,5 +1,5 @@
defmodule Example.Repo do
use Ecto.Repo,
otp_app: :example,
adapter: Ecto.Adapters.Postgres
adapter: Ecto.Adapters.SQLite3
end

View file

@ -686,4 +686,139 @@ defmodule ExampleWeb.CoreComponents do
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
@doc """
Renders a navigation link with active state styling.
## Examples
<.nav_link href={~p"/home"} current_path={@current_path}>Home</.nav_link>
"""
attr :href, :string, required: true
attr :current_path, :string, required: true
attr :class, :string, default: nil
slot :inner_block, required: true
def nav_link(assigns) do
~H"""
<a
href={@href}
class={[
"block px-3 py-2 rounded-md text-sm font-medium transition-colors",
if(@current_path == @href,
do: "bg-zinc-900 text-white",
else: "text-zinc-700 hover:bg-zinc-100 hover:text-zinc-900"
),
@class
]}
>
<%= render_slot(@inner_block) %>
</a>
"""
end
@doc """
Renders a mobile navigation panel with slide-out sidebar.
## Examples
<.mobile_nav id="mobile-nav" current_path={@current_path}>
<:section title="Basics">
<.nav_link href={~p"/hello"} current_path={@current_path}>Hello</.nav_link>
</:section>
</.mobile_nav>
"""
attr :id, :string, required: true
attr :current_path, :string, required: true
slot :section, required: true do
attr :title, :string, required: true
end
def mobile_nav(assigns) do
~H"""
<div id={@id} class="relative z-50 lg:hidden" role="dialog" aria-modal="true">
<div
id={"#{@id}-backdrop"}
class="fixed inset-0 bg-zinc-900/80 opacity-0 transition-opacity duration-300 ease-linear hidden"
phx-click={hide_mobile_nav(@id)}
/>
<div
id={"#{@id}-container"}
class="fixed inset-0 flex -translate-x-full transition-transform duration-300 ease-in-out"
>
<div class="relative mr-16 flex w-full max-w-xs flex-1">
<div class="absolute left-full top-0 flex w-16 justify-center pt-5">
<button
type="button"
class="-m-2.5 p-2.5 text-white"
phx-click={hide_mobile_nav(@id)}
>
<span class="sr-only">Close sidebar</span>
<.icon name="hero-x-mark" class="h-6 w-6" />
</button>
</div>
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-4">
<div class="flex h-16 shrink-0 items-center border-b border-zinc-200">
<a href="/" class="flex items-center gap-2">
<svg viewBox="0 0 71 48" class="h-6" aria-hidden="true">
<path
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
fill="#FD4F00"
/>
</svg>
<span class="text-sm font-semibold">LiveSvelte</span>
</a>
</div>
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
<%= for section <- @section do %>
<li>
<div class="text-xs font-semibold leading-6 text-zinc-400 uppercase tracking-wider">
<%= section.title %>
</div>
<ul role="list" class="-mx-2 mt-2 space-y-1">
<%= render_slot(section) %>
</ul>
</li>
<% end %>
</ul>
</nav>
</div>
</div>
</div>
</div>
"""
end
def show_mobile_nav(js \\ %JS{}, id) do
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id}-backdrop",
transition: {"transition-opacity ease-linear duration-300", "opacity-0", "opacity-100"}
)
|> JS.remove_class("-translate-x-full",
to: "##{id}-container",
transition: {"transition ease-in-out duration-300 transform", "-translate-x-full", "translate-x-0"}
)
|> JS.add_class("overflow-hidden", to: "body")
end
def hide_mobile_nav(js \\ %JS{}, id) do
js
|> JS.add_class("-translate-x-full",
to: "##{id}-container",
transition: {"transition ease-in-out duration-300 transform", "translate-x-0", "-translate-x-full"}
)
|> JS.hide(
to: "##{id}-backdrop",
transition: {"transition-opacity ease-linear duration-300", "opacity-100", "opacity-0"}
)
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
end
end

View file

@ -2,4 +2,101 @@ defmodule ExampleWeb.Layouts do
use ExampleWeb, :html
embed_templates "layouts/*"
# Single source of truth for main nav (sidebar + desktop). Layout has route helpers (~p).
def nav_groups do
[
%{
label: "Basics",
links: [
%{label: "Hello World", to: ~p"/hello-world"},
%{label: "Lodash", to: ~p"/lodash"},
%{label: "Struct Props", to: ~p"/live-struct"}
]
},
%{
label: "Interactive",
links: [
%{label: "Counter", to: ~p"/live-simple-counter"},
%{label: "Lights", to: ~p"/live-lights"},
%{label: "Sigil", to: ~p"/live-sigil"},
%{label: "Plus/Minus (Static)", to: ~p"/plus-minus-svelte"},
%{label: "Plus/Minus (Live)", to: ~p"/live-plus-minus"},
%{label: "Hybrid Counter", to: ~p"/live-plus-minus-hybrid"}
]
},
%{
label: "Data",
links: [
%{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: "Slots",
links: [
%{label: "Simple Slots", to: ~p"/live-slots-simple"},
%{label: "Dynamic Slots", to: ~p"/live-slots-dynamic"}
]
},
%{
label: "Advanced",
links: [
%{label: "Client Loading", to: ~p"/live-client-side-loading"}
]
},
%{
label: "Ecto",
links: [
%{label: "Notes (OTP)", to: ~p"/live-notes-otp"}
]
}
]
end
def nav_sidebar_items(assigns) do
assigns = assign(assigns, :nav_groups, nav_groups())
~H"""
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
<li :for={group <- @nav_groups}>
<div class="text-xs font-semibold leading-6 text-zinc-400 uppercase tracking-wider">
<%= group.label %>
</div>
<ul role="list" class="-mx-2 mt-2 space-y-1">
<li :for={link <- group.links}>
<a href={link.to} class="block px-3 py-2 rounded-md text-sm font-medium text-zinc-700 hover:bg-zinc-100">
<%= link.label %>
</a>
</li>
</ul>
</li>
</ul>
</nav>
"""
end
def nav_desktop_dropdowns(assigns) do
assigns = assign(assigns, :nav_groups, nav_groups())
~H"""
<nav class="hidden lg:flex lg:items-center lg:gap-1">
<div :for={group <- @nav_groups} class="relative group">
<button type="button" class="px-3 py-2 text-sm font-medium text-zinc-700 hover:text-zinc-900 rounded-md hover:bg-zinc-100">
<%= group.label %>
</button>
<div class="absolute left-0 mt-1 w-48 bg-white rounded-md shadow-lg border border-zinc-200 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-150 z-50">
<div class="py-1">
<a :for={link <- group.links} href={link.to} class="block px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-100">
<%= link.label %>
</a>
</div>
</div>
</div>
</nav>
"""
end
end

View file

@ -1,57 +1,92 @@
<header class="px-4 sm:px-6 lg:px-8 h-[70px]">
<div class="flex items-center justify-between border-b border-zinc-100 py-3">
<div class="flex items-center gap-4">
<a href="/">
<svg viewBox="0 0 71 48" class="h-6" aria-hidden="true">
<path
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
fill="#FD4F00"
/>
</svg>
</a>
<%!-- Mobile Navigation Sidebar --%>
<div id="mobile-nav" class="relative z-50 lg:hidden hidden" role="dialog" aria-modal="true">
<div
id="mobile-nav-backdrop"
class="fixed inset-0 bg-zinc-900/80 opacity-0 transition-opacity duration-300 ease-linear"
phx-click={ExampleWeb.CoreComponents.hide_mobile_nav("mobile-nav")}
/>
<p class="rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6 text-brand">
LiveSvelte Examples
</p>
<div
id="mobile-nav-container"
class="fixed inset-0 flex -translate-x-full transition-transform duration-300 ease-in-out"
>
<div class="relative mr-16 flex w-full max-w-xs flex-1">
<div class="absolute left-full top-0 flex w-16 justify-center pt-5">
<button
type="button"
class="-m-2.5 p-2.5 text-white"
phx-click={ExampleWeb.CoreComponents.hide_mobile_nav("mobile-nav")}
>
<span class="sr-only">Close sidebar</span>
<.icon name="hero-x-mark" class="h-6 w-6" />
</button>
</div>
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-4">
<div class="flex h-16 shrink-0 items-center border-b border-zinc-200">
<a href="/" class="flex items-center gap-2">
<svg viewBox="0 0 71 48" class="h-6" aria-hidden="true">
<path
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
fill="#FD4F00"
/>
</svg>
<span class="text-sm font-semibold">LiveSvelte</span>
</a>
</div>
<.nav_sidebar_items />
</div>
</div>
</div>
</div>
<div class="flex">
<%= for {path, text} <- [
{~p"/hello-world", "1"},
{~p"/lodash", "2"},
{~p"/live-struct", "3"},
{~p"/live-simple-counter", "4"},
{~p"/live-lights", "5"},
{~p"/live-sigil", "6"},
{~p"/plus-minus-svelte", "7"},
{~p"/live-plus-minus", "8"},
{~p"/live-plus-minus-hybrid", "9"},
{~p"/live-log-list", "10"},
{~p"/live-breaking-news", "11"},
{~p"/live-chat", "12"},
{~p"/live-json", "13"},
{~p"/live-slots-simple", "14"},
{~p"/live-slots-dynamic", "15"},
{~p"/live-client-side-loading", "16"}
] do %>
<a href={path} class="font-semibold leading-6 text-zinc-900 hover:text-zinc-700 hover:underline p-2">
<%= text %>
<header class="sticky top-0 z-40 bg-white border-b border-zinc-200">
<div class="px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<%!-- Logo and mobile menu button --%>
<div class="flex items-center gap-4">
<%!-- Mobile menu button --%>
<button
type="button"
class="lg:hidden -m-2.5 p-2.5 text-zinc-700"
phx-click={ExampleWeb.CoreComponents.show_mobile_nav("mobile-nav")}
>
<span class="sr-only">Open sidebar</span>
<.icon name="hero-bars-3" class="h-6 w-6" />
</button>
<a href="/" class="flex items-center gap-2">
<svg viewBox="0 0 71 48" class="h-6" aria-hidden="true">
<path
d="m26.371 33.477-.552-.1c-3.92-.729-6.397-3.1-7.57-6.829-.733-2.324.597-4.035 3.035-4.148 1.995-.092 3.362 1.055 4.57 2.39 1.557 1.72 2.984 3.558 4.514 5.305 2.202 2.515 4.797 4.134 8.347 3.634 3.183-.448 5.958-1.725 8.371-3.828.363-.316.761-.592 1.144-.886l-.241-.284c-2.027.63-4.093.841-6.205.735-3.195-.16-6.24-.828-8.964-2.582-2.486-1.601-4.319-3.746-5.19-6.611-.704-2.315.736-3.934 3.135-3.6.948.133 1.746.56 2.463 1.165.583.493 1.143 1.015 1.738 1.493 2.8 2.25 6.712 2.375 10.265-.068-5.842-.026-9.817-3.24-13.308-7.313-1.366-1.594-2.7-3.216-4.095-4.785-2.698-3.036-5.692-5.71-9.79-6.623C12.8-.623 7.745.14 2.893 2.361 1.926 2.804.997 3.319 0 4.149c.494 0 .763.006 1.032 0 2.446-.064 4.28 1.023 5.602 3.024.962 1.457 1.415 3.104 1.761 4.798.513 2.515.247 5.078.544 7.605.761 6.494 4.08 11.026 10.26 13.346 2.267.852 4.591 1.135 7.172.555ZM10.751 3.852c-.976.246-1.756-.148-2.56-.962 1.377-.343 2.592-.476 3.897-.528-.107.848-.607 1.306-1.336 1.49Zm32.002 37.924c-.085-.626-.62-.901-1.04-1.228-1.857-1.446-4.03-1.958-6.333-2-1.375-.026-2.735-.128-4.031-.61-.595-.22-1.26-.505-1.244-1.272.015-.78.693-1 1.31-1.184.505-.15 1.026-.247 1.6-.382-1.46-.936-2.886-1.065-4.787-.3-2.993 1.202-5.943 1.06-8.926-.017-1.684-.608-3.179-1.563-4.735-2.408l-.043.03a2.96 2.96 0 0 0 .04-.029c-.038-.117-.107-.12-.197-.054l.122.107c1.29 2.115 3.034 3.817 5.004 5.271 3.793 2.8 7.936 4.471 12.784 3.73A66.714 66.714 0 0 1 37 40.877c1.98-.16 3.866.398 5.753.899Zm-9.14-30.345c-.105-.076-.206-.266-.42-.069 1.745 2.36 3.985 4.098 6.683 5.193 4.354 1.767 8.773 2.07 13.293.51 3.51-1.21 6.033-.028 7.343 3.38.19-3.955-2.137-6.837-5.843-7.401-2.084-.318-4.01.373-5.962.94-5.434 1.575-10.485.798-15.094-2.553Zm27.085 15.425c.708.059 1.416.123 2.124.185-1.6-1.405-3.55-1.517-5.523-1.404-3.003.17-5.167 1.903-7.14 3.972-1.739 1.824-3.31 3.87-5.903 4.604.043.078.054.117.066.117.35.005.699.021 1.047.005 3.768-.17 7.317-.965 10.14-3.7.89-.86 1.685-1.817 2.544-2.71.716-.746 1.584-1.159 2.645-1.07Zm-8.753-4.67c-2.812.246-5.254 1.409-7.548 2.943-1.766 1.18-3.654 1.738-5.776 1.37-.374-.066-.75-.114-1.124-.17l-.013.156c.135.07.265.151.405.207.354.14.702.308 1.07.395 4.083.971 7.992.474 11.516-1.803 2.221-1.435 4.521-1.707 7.013-1.336.252.038.503.083.756.107.234.022.479.255.795.003-2.179-1.574-4.526-2.096-7.094-1.872Zm-10.049-9.544c1.475.051 2.943-.142 4.486-1.059-.452.04-.643.04-.827.076-2.126.424-4.033-.04-5.733-1.383-.623-.493-1.257-.974-1.889-1.457-2.503-1.914-5.374-2.555-8.514-2.5.05.154.054.26.108.315 3.417 3.455 7.371 5.836 12.369 6.008Zm24.727 17.731c-2.114-2.097-4.952-2.367-7.578-.537 1.738.078 3.043.632 4.101 1.728.374.388.763.768 1.182 1.106 1.6 1.29 4.311 1.352 5.896.155-1.861-.726-1.861-.726-3.601-2.452Zm-21.058 16.06c-1.858-3.46-4.981-4.24-8.59-4.008a9.667 9.667 0 0 1 2.977 1.39c.84.586 1.547 1.311 2.243 2.055 1.38 1.473 3.534 2.376 4.962 2.07-.656-.412-1.238-.848-1.592-1.507Zm17.29-19.32c0-.023.001-.045.003-.068l-.006.006.006-.006-.036-.004.021.018.012.053Zm-20 14.744a7.61 7.61 0 0 0-.072-.041.127.127 0 0 0 .015.043c.005.008.038 0 .058-.002Zm-.072-.041-.008-.034-.008.01.008-.01-.022-.006.005.026.024.014Z"
fill="#FD4F00"
/>
</svg>
</a>
<% end %>
</div>
<div class="flex items-center gap-4">
<a
href="https://github.com/woutdp/live_svelte"
target="_blank"
class="text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
GitHub
</a>
<span class="hidden sm:block rounded-full bg-brand/5 px-2 text-[0.8125rem] font-medium leading-6 text-brand">
LiveSvelte Examples
</span>
</div>
<%!-- Desktop Navigation --%>
<.nav_desktop_dropdowns />
<%!-- GitHub Link --%>
<div class="flex items-center gap-4">
<a
href="https://github.com/woutdp/live_svelte"
target="_blank"
class="text-[0.8125rem] font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
>
GitHub
</a>
</div>
</div>
</div>
</header>
<main class="p-0 sm:py-20 sm:px-6 lg:px-8 h-[calc(100vh_-_70px)]">
<main class="p-4 sm:py-8 sm:px-6 lg:px-8 min-h-[calc(100vh-65px)]">
<%= @inner_content %>
</main>

View file

@ -1 +1,149 @@
<div class="max-w-4xl mx-auto">
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-zinc-900 mb-4">LiveSvelte Examples</h1>
<p class="text-lg text-zinc-600">
Explore these examples to learn how to integrate Svelte components with Phoenix LiveView.
</p>
</div>
<div class="grid gap-8 md:grid-cols-2">
<%!-- Basics --%>
<div class="bg-white rounded-lg border border-zinc-200 p-6">
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-sm">1</span>
Basics
</h2>
<ul class="space-y-2">
<li>
<a href={~p"/hello-world"} class="text-blue-600 hover:text-blue-800 hover:underline">Hello World</a>
<span class="text-zinc-500 text-sm">- Simple component rendering</span>
</li>
<li>
<a href={~p"/lodash"} class="text-blue-600 hover:text-blue-800 hover:underline">Lodash</a>
<span class="text-zinc-500 text-sm">- Using npm packages</span>
</li>
<li>
<a href={~p"/live-struct"} class="text-blue-600 hover:text-blue-800 hover:underline">Struct Props</a>
<span class="text-zinc-500 text-sm">- Passing Elixir structs as props</span>
</li>
</ul>
</div>
<%!-- Interactive --%>
<div class="bg-white rounded-lg border border-zinc-200 p-6">
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-green-100 text-green-600 flex items-center justify-center text-sm">2</span>
Interactive
</h2>
<ul class="space-y-2">
<li>
<a href={~p"/live-simple-counter"} class="text-blue-600 hover:text-blue-800 hover:underline">Counter</a>
<span class="text-zinc-500 text-sm">- Server + client state</span>
</li>
<li>
<a href={~p"/live-lights"} class="text-blue-600 hover:text-blue-800 hover:underline">Lights</a>
<span class="text-zinc-500 text-sm">- Multiple components sharing state</span>
</li>
<li>
<a href={~p"/live-sigil"} class="text-blue-600 hover:text-blue-800 hover:underline">Sigil</a>
<span class="text-zinc-500 text-sm">- Inline Svelte with ~V sigil</span>
</li>
<li>
<a href={~p"/plus-minus-svelte"} class="text-blue-600 hover:text-blue-800 hover:underline">Plus/Minus (Static)</a>
<span class="text-zinc-500 text-sm">- Dead view integration</span>
</li>
<li>
<a href={~p"/live-plus-minus"} class="text-blue-600 hover:text-blue-800 hover:underline">Plus/Minus (Live)</a>
<span class="text-zinc-500 text-sm">- LiveView integration</span>
</li>
<li>
<a href={~p"/live-plus-minus-hybrid"} class="text-blue-600 hover:text-blue-800 hover:underline">Hybrid Counter</a>
<span class="text-zinc-500 text-sm">- Client + server events</span>
</li>
</ul>
</div>
<%!-- Data --%>
<div class="bg-white rounded-lg border border-zinc-200 p-6">
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-purple-100 text-purple-600 flex items-center justify-center text-sm">3</span>
Data
</h2>
<ul class="space-y-2">
<li>
<a href={~p"/live-log-list"} class="text-blue-600 hover:text-blue-800 hover:underline">Log List</a>
<span class="text-zinc-500 text-sm">- Dynamic list updates</span>
</li>
<li>
<a href={~p"/live-breaking-news"} class="text-blue-600 hover:text-blue-800 hover:underline">Breaking News</a>
<span class="text-zinc-500 text-sm">- Real-time updates with ~V sigil</span>
</li>
<li>
<a href={~p"/live-chat"} class="text-blue-600 hover:text-blue-800 hover:underline">Chat</a>
<span class="text-zinc-500 text-sm">- PubSub + pushEvent</span>
</li>
<li>
<a href={~p"/live-json"} class="text-blue-600 hover:text-blue-800 hover:underline">LiveJSON</a>
<span class="text-zinc-500 text-sm">- Efficient JSON diffing</span>
</li>
</ul>
</div>
<%!-- Slots --%>
<div class="bg-white rounded-lg border border-zinc-200 p-6">
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-orange-100 text-orange-600 flex items-center justify-center text-sm">4</span>
Slots
</h2>
<ul class="space-y-2">
<li>
<a href={~p"/live-slots-simple"} class="text-blue-600 hover:text-blue-800 hover:underline">Simple Slots</a>
<span class="text-zinc-500 text-sm">- Basic slot usage</span>
</li>
<li>
<a href={~p"/live-slots-dynamic"} class="text-blue-600 hover:text-blue-800 hover:underline">Dynamic Slots</a>
<span class="text-zinc-500 text-sm">- Named slots with dynamic content</span>
</li>
</ul>
</div>
<%!-- Advanced --%>
<div class="bg-white rounded-lg border border-zinc-200 p-6">
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-red-100 text-red-600 flex items-center justify-center text-sm">5</span>
Advanced
</h2>
<ul class="space-y-2">
<li>
<a href={~p"/live-client-side-loading"} class="text-blue-600 hover:text-blue-800 hover:underline">Client Loading</a>
<span class="text-zinc-500 text-sm">- Loading states and SSR</span>
</li>
</ul>
</div>
<%!-- Ecto --%>
<div class="bg-white rounded-lg border border-zinc-200 p-6">
<h2 class="text-lg font-semibold text-zinc-900 mb-4 flex items-center gap-2">
<span class="w-8 h-8 rounded-full bg-teal-100 text-teal-600 flex items-center justify-center text-sm">6</span>
Ecto
</h2>
<ul class="space-y-2">
<li>
<a href={~p"/live-notes-otp"} class="text-blue-600 hover:text-blue-800 hover:underline">Notes (OTP JSON)</a>
<span class="text-zinc-500 text-sm">- Default OTP encoder with SQLite</span>
</li>
<li>
<a href={~p"/live-notes-jason"} class="text-blue-600 hover:text-blue-800 hover:underline">Notes (Jason)</a>
<span class="text-zinc-500 text-sm">- Jason-compatible approach</span>
</li>
</ul>
</div>
</div>
<div class="mt-12 text-center text-zinc-500 text-sm">
<p>
View the source code on
<a href="https://github.com/woutdp/live_svelte" target="_blank" class="text-blue-600 hover:underline">GitHub</a>
</p>
</div>
</div>

View file

@ -0,0 +1,58 @@
defmodule ExampleWeb.LiveNotesOtp do
@moduledoc """
LiveView demonstrating Ecto structs with OTP JSON encoder.
The OTP JSON encoder (LiveSvelte.JSON) automatically converts Ecto structs
to maps, stripping the __struct__ key. This means you can pass Ecto schemas
directly to Svelte components without any additional configuration.
This is the default encoder for LiveSvelte since v0.17.0.
"""
use ExampleWeb, :live_view
alias Example.Notes
@info """
Using OTP JSON encoder (default since v0.17.0). Ecto structs are automatically
converted to maps.
"""
def render(assigns) do
~H"""
<.svelte
name="NotesApp"
props={%{
notes: @notes,
encoder: "OTP",
info: @info
}}
socket={@socket}
/>
"""
end
def mount(_params, _session, socket) do
{:ok, assign(socket, notes: Notes.list_notes(), info: @info)}
end
def handle_event("create_note", params, socket) do
case Notes.create_note(params) do
{:ok, _note} ->
{:noreply, assign(socket, :notes, Notes.list_notes())}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to create note")}
end
end
def handle_event("delete_note", %{"id" => id}, socket) do
note = Notes.get_note!(id)
case Notes.delete_note(note) do
{:ok, _note} ->
{:noreply, assign(socket, :notes, Notes.list_notes())}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to delete note")}
end
end
end

View file

@ -35,6 +35,8 @@ defmodule ExampleWeb.Router do
live "/live-slots-simple", LiveSlotsSimple
live "/live-slots-dynamic", LiveSlotsDynamic
live "/live-client-side-loading", LiveClientSideLoading
# Ecto Examples
live "/live-notes-otp", LiveNotesOtp
# not referenced in app.html.heex:
live "/live-composition", LiveComposition
end

View file

@ -32,10 +32,9 @@ defmodule Example.MixProject do
# Type `mix help deps` for examples and options.
defp deps do
[
{:ecto_sql, "~> 3.6"},
{:ecto_sql, "~> 3.12"},
{:finch, "~> 0.13"},
{:gettext, "~> 0.20"},
{:jason, "~> 1.2"},
{:json_diff_ex, "~> 0.6", override: true},
{:live_json, "~> 0.4.5"},
{:live_svelte, path: ".."},
@ -45,7 +44,7 @@ defmodule Example.MixProject do
{:phoenix_live_dashboard, "~> 0.8"},
{:phoenix_live_view, "~> 0.19"},
{:plug_cowboy, "~> 2.5"},
{:postgrex, ">= 0.0.0"},
{:ecto_sqlite3, "~> 0.17"},
{:swoosh, "~> 1.3"},
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},

View file

@ -1,15 +1,17 @@
%{
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
"db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"},
"cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"ecto": {:hex, :ecto, "3.9.5", "9f0aa7ae44a1577b651c98791c6988cd1b69b21bc724e3fd67090b97f7604263", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d4f3115d8cbacdc0bfa4b742865459fb1371d0715515842a1fb17fe31920b74c"},
"ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"},
"esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"},
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
"ecto_sql": {:hex, :ecto_sql, "3.13.4", "b6e9d07557ddba62508a9ce4a484989a5bb5e9a048ae0e695f6d93f095c25d60", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b38cf0749ca4d1c5a8bcbff79bbe15446861ca12a61f9fba604486cb6b62a14"},
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.22.0", "edab2d0f701b7dd05dcf7e2d97769c106aff62b5cfddc000d1dd6f46b9cbd8c3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5af9e031bffcc5da0b7bca90c271a7b1e7c04a93fecf7f6cd35bc1b1921a64bd"},
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"expo": {:hex, :expo, "0.4.0", "bbe4bf455e2eb2ebd2f1e7d83530ce50fb9990eb88fc47855c515bfdf1c6626f", [:mix], [], "hexpm", "a8ed1683ec8b7c7fa53fd7a41b2c6935f539168a6bb0616d7fd6b58a36f3abf2"},
"exqlite": {:hex, :exqlite, "0.34.0", "ebca3570eb4c4eb4345d76c8e44ce31a62de7b24a54fd118164480f2954bd540", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "bcdc58879a0db5e08cd5f6fbe07a0692ceffaaaa617eab46b506137edf0a2742"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.14.0", "619bfdee18fc135190bf590356c4bf5d5f71f916adb12aec94caa3fa9267a4bc", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5459acaf18c4fdb47a8c22fb3baff5d8173106217c8e56c5ba0b93e66501a8dd"},
"floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"},
@ -19,7 +21,7 @@
"json_diff_ex": {:hex, :json_diff_ex, "0.6.7", "679eb6df8fb59b061434798ecf641f510e8dc7ae9d6ca22e593457bfe3d51a5e", [:mix], [], "hexpm", "7da6360cfb8aea96513d67c1a6401e1bb1cf2c988e2e81cfb67224fd05187043"},
"jsonpatch": {:hex, :jsonpatch, "0.13.1", "fd32eae78e2a7d9a2f40ee2468d22b75676b5d49bb5f6f929204123b15e3f254", [:make, :mix], [], "hexpm", "b3a29d2a3d56149e50fd5a8e19f5dd82c7f85491871b5598405cc7fb97de8f0b"},
"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.3.1", "81c7c632c1425772b498a445cb37a4f664116c031d6a0a4d0602100187452be6", [:mix], [{:esbuild, "~> 0.5", [hex: :esbuild, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:nodejs, "~> 2.0", [hex: :nodejs, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "7e0353e57af92bcbaa142e38a26012a8aaa818cd53f5d33f627283fe7ba825d6"},
"live_svelte": {:hex, :live_svelte, "0.17.0", "953603f1358cceb8e893ad41c1e5243dba5bd95c57b5a8162164f593bcf499f8", [: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", "c60055b3acc9ee56258589b98b3b0a5e2a3b5bec4298af5f3936384ff4c4a44d"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
"nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"},
@ -31,13 +33,12 @@
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.0", "0b3158b5b198aa444473c91d23d79f52fb077e807ffad80dacf88ce078fa8df2", [: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", [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.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "87785a54474fed91a67a1227a741097eb1a42c2e49d3c0d098b588af65cd410d"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, 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]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"},
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.5", "261f21b67aea8162239b2d6d3b4c31efde4daa22a20d80b19c2c0f21b34b270e", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20884bf58a90ff5a5663420f5d2c368e9e15ed1ad5e911daf0916ea3c57f77ac"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"swoosh": {:hex, :swoosh, "1.9.1", "0a5d7bf9954eb41d7e55525bc0940379982b090abbaef67cd8e1fd2ed7f8ca1a", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76dffff3ffcab80f249d5937a592eaef7cc49ac6f4cdd27e622868326ed6371e"},
@ -46,5 +47,5 @@
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
}

View file

@ -0,0 +1,13 @@
defmodule Example.Repo.Migrations.CreateNotes do
use Ecto.Migration
def change do
create table(:notes, primary_key: false) do
add :id, :binary_id, primary_key: true
add :title, :string, null: false
add :content, :text
add :color, :string, default: "#fef3c7"
timestamps(type: :utc_datetime)
end
end
end

View file

@ -11,6 +11,8 @@ defmodule LiveSvelte.JSON do
- Automatically converts structs to maps
- Converts all map keys to strings (matching Jason behavior)
- Handles nested data structures
- Converts DateTime/NaiveDateTime/Date/Time to ISO 8601 strings
- Strips Ecto schema metadata (`__meta__` field) automatically
## Usage
@ -22,9 +24,9 @@ defmodule LiveSvelte.JSON do
## SSR Compatibility Note
When using server-side rendering, the NodeJS worker uses Jason internally
to serialize data to the Node.js process. This module is designed to produce
Jason-compatible output, ensuring consistency between SSR and client-side
hydration.
to serialize data to the Node.js process. The `prepare/1` function is used
to convert Elixir terms to JSON-compatible values before passing to NodeJS,
ensuring consistency between SSR and client-side hydration.
"""
@ -51,6 +53,31 @@ defmodule LiveSvelte.JSON do
|> IO.iodata_to_binary()
end
@doc """
Prepares an Elixir term for JSON serialization.
This function recursively converts Elixir terms to JSON-compatible values:
- Structs become maps (with `__struct__` key stripped)
- Ecto schemas have `__meta__` field stripped
- DateTime/NaiveDateTime/Date/Time become ISO 8601 strings
- Atoms become strings
- nil becomes :null (for Erlang's :json module)
This is useful for preparing data before passing to external JSON encoders
(like the NodeJS worker which uses Jason internally).
## Examples
iex> LiveSvelte.JSON.prepare(%DateTime{} = dt)
"2026-01-31T20:31:10Z"
iex> LiveSvelte.JSON.prepare(%{notes: [%MyApp.Note{title: "Hello"}]})
%{"notes" => [%{"title" => "Hello"}]}
"""
@spec prepare(term()) :: term()
def prepare(term), do: prepare_term(term)
# Recursively prepare terms for JSON encoding.
# Converts structs to maps, nil to null, and handles nested structures.
@ -66,6 +93,22 @@ defmodule LiveSvelte.JSON do
Atom.to_string(atom)
end
# DateTime/NaiveDateTime/Date/Time become ISO 8601 strings
# These must come before the generic struct handler
defp prepare_term(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
defp prepare_term(%NaiveDateTime{} = dt), do: NaiveDateTime.to_iso8601(dt)
defp prepare_term(%Date{} = d), do: Date.to_iso8601(d)
defp prepare_term(%Time{} = t), do: Time.to_iso8601(t)
# Ecto schema structs - strip __meta__ field
# Must come before the generic struct handler
defp prepare_term(%{__struct__: _, __meta__: _} = struct) do
struct
|> Map.from_struct()
|> Map.delete(:__meta__)
|> prepare_term()
end
# Structs become maps (strip __struct__ key)
defp prepare_term(%_{} = struct) do
struct

View file

@ -3,8 +3,14 @@ defmodule LiveSvelte.SSR.NodeJS do
@behaviour LiveSvelte.SSR
def render(name, props, slots) do
# Prepare props and slots for JSON serialization before passing to NodeJS.
# This converts structs to maps, DateTime to ISO 8601 strings, and strips
# Ecto metadata (__meta__). Required because NodeJS.call! uses Jason internally.
prepared_props = LiveSvelte.JSON.prepare(props)
prepared_slots = LiveSvelte.JSON.prepare(slots)
try do
NodeJS.call!({"server", "render"}, [name, props, slots], binary: true)
NodeJS.call!({"server", "render"}, [name, prepared_props, prepared_slots], binary: true)
catch
:exit, {:noproc, _} ->
message = """