Refactor handling SSR and slots

This commit is contained in:
Wout De Puysseleir 2023-03-08 10:05:35 -08:00
parent 4e3dd6960c
commit 0f5bbfebaa
No known key found for this signature in database
GPG key ID: 3DE9371B50FEC46A
6 changed files with 100 additions and 45 deletions

View file

@ -15,56 +15,62 @@ function base64ToElement(base64) {
return template
}
export const createSlots = (slots, el) => {
function createSlot(content) {
element = base64ToElement(content)
function dataAttributeToJson(attributeName, el) {
const data = el.getAttribute(attributeName)
return data ? JSON.parse(data) : {}
}
function createSlots(slots, ref) {
const createSlot = (slotName, ref) => {
let savedTarget, savedAnchor, savedElement
return () => {
return {
getElement() {
return base64ToElement(dataAttributeToJson('data-slots', ref.el)[slotName])
},
update() {
const element = this.getElement()
detach(savedElement)
insert(savedTarget, element, savedAnchor)
savedElement = element
},
c: noop,
m(target, anchor) {
const element = this.getElement()
savedTarget = target
savedAnchor = anchor
savedElement = element
insert(target, element, anchor);
insert(target, element, anchor)
},
d(detaching) {
if (detaching && element.innerHTML) {
detach(element);
}
if (detaching) detach(savedElement)
},
l: noop,
};
}
}
}
const svelteSlots = {}
for (const slotName in slots) {
svelteSlots[slotName] = [createSlot(slots[slotName])];
svelteSlots[slotName] = [createSlot(slotName, ref)]
}
return svelteSlots
}
function getProps(ref) {
const dataProps = ref.el.getAttribute('data-props')
const props = dataProps ? JSON.parse(dataProps) : {}
return {
...props,
...dataAttributeToJson('data-props', ref.el),
pushEvent: (event, data, callback) => ref.pushEvent(event, data, callback),
$$slots: createSlots({default: ref.el.getAttribute('data-slot-default')}, ref.el),
$$slots: createSlots(dataAttributeToJson('data-slots', ref.el), ref),
$$scope: {}
}
}
function findSlotCtx(component) {
// The default slot always exists if there's a slot set
// even if no slot is set for the explicit default slot
return component.$$.ctx.find(ctxElement => ctxElement.default)
}
@ -88,8 +94,14 @@ const SvelteComponent = {
},
updated() {
// Set the props
this._instance.$set(getProps(this))
findSlotCtx(this._instance).default[0]().update()
// Set the slots
const slotCtx = findSlotCtx(this._instance)
for (const key in slotCtx) {
slotCtx[key][0]().update()
}
},
destroyed() {

View file

@ -1,5 +1,6 @@
module.exports.render = (name, props={}, slots=null) => {
const ssrComponent = require('../../priv/static/assets/server/server.js')[name].default
const $$slots = slots ? {default: () => slots} : {}
slots = Object.fromEntries(Object.entries(slots).map(([k, v]) => [k, () => v]))
const $$slots = slots || {}
return ssrComponent.render(props, {$$slots, context: new Map()})
}

View file

@ -11,6 +11,9 @@ defmodule LiveSvelte do
use Phoenix.LiveComponent
import Phoenix.HTML
alias LiveSvelte.Slots
alias LiveSvelte.SSR
attr(:props, :map, default: %{})
attr(:name, :string)
@ -33,7 +36,7 @@ defmodule LiveSvelte do
id={id(@name)}
data-name={@name}
data-props={json(@props)}
data-slot-default={Base.encode64(get_slot(assigns))}
data-slots={Slots.base_encode_64(@slots) |> json}
phx-update="ignore"
phx-hook="SvelteComponent"
>
@ -45,39 +48,26 @@ defmodule LiveSvelte do
@impl true
def update(assigns, socket) do
slots =
assigns
|> Slots.rendered_slot_map()
|> Slots.js_process()
# Making sure we only render once
ssr_code =
if not connected?(socket) do
props = Map.get(assigns, :props, %{})
slot = get_slot(assigns)
ssr_render(assigns.name, props, slot)
SSR.render(assigns.name, Map.get(assigns, :props, %{}), slots)
end
socket =
socket
|> assign(assigns)
|> assign(:slots, slots)
|> assign(:ssr_render, ssr_code)
{:ok, socket}
end
defp get_slot(assigns) do
~H"""
<%= if assigns[:inner_block] do %>
<%= render_slot(@inner_block) %>
<% end %>
"""
|> Phoenix.HTML.Safe.to_iodata()
|> List.to_string()
|> String.trim()
end
defp ssr_render(name, props, slots \\ nil)
defp ssr_render(name, nil, slots), do: ssr_render(name, %{}, slots)
defp ssr_render(name, props, slots),
do: NodeJS.call!({"svelte/render", "render"}, [name, props, slots])
defp json(props) do
props
|> Jason.encode()

53
lib/slots.ex Normal file
View file

@ -0,0 +1,53 @@
defmodule LiveSvelte.Slots do
import Phoenix.Component
def rendered_slot_map(assigns) do
assigns
|> filter_slots_from_assigns()
|> render_slots()
end
@doc """
Processes the slots for use in JavaScript.
"""
def js_process(assigns) do
assigns
|> Enum.map(fn
{:inner_block, value} -> {:default, value}
key_value -> key_value
end)
|> Enum.into(%{})
end
def base_encode_64(assigns) do
assigns
|> Enum.map(fn {key, value} -> {key, Base.encode64(value)} end)
|> Enum.into(%{})
end
defp filter_slots_from_assigns(assigns) do
assigns
|> Enum.filter(fn
{_key, [%{__slot__: _}]} -> true
_ -> false
end)
|> Enum.into(%{})
end
defp render_slots(assigns) do
Enum.reduce(assigns, %{}, fn
{key, value}, acc -> Map.put(acc, key, render(%{slot: value}))
end)
end
defp render(assigns) do
~H"""
<%= if assigns[:slot] do %>
<%= render_slot(@slot) %>
<% end %>
"""
|> Phoenix.HTML.Safe.to_iodata()
|> List.to_string()
|> String.trim()
end
end

7
lib/ssr.ex Normal file
View file

@ -0,0 +1,7 @@
defmodule LiveSvelte.SSR do
def render(name, props, slots \\ nil)
def render(name, nil, slots), do: render(name, %{}, slots)
def render(name, props, slots),
do: NodeJS.call!({"svelte/render", "render"}, [name, props, slots])
end

View file

@ -1,8 +0,0 @@
defmodule LiveSvelteTest do
use ExUnit.Case
doctest LiveSvelte
test "greets the world" do
assert LiveSvelte.hello() == :world
end
end