From 0f5bbfebaabbc8759368e6ff655a34a249be1e97 Mon Sep 17 00:00:00 2001 From: Wout De Puysseleir Date: Wed, 8 Mar 2023 10:05:35 -0800 Subject: [PATCH] Refactor handling SSR and slots --- assets/js/hooks.js | 42 ++++++++++++++++++++----------- assets/svelte/render.js | 3 ++- lib/live_component.ex | 32 ++++++++--------------- lib/slots.ex | 53 +++++++++++++++++++++++++++++++++++++++ lib/ssr.ex | 7 ++++++ test/svelte_view_test.exs | 8 ------ 6 files changed, 100 insertions(+), 45 deletions(-) create mode 100644 lib/slots.ex create mode 100644 lib/ssr.ex delete mode 100644 test/svelte_view_test.exs diff --git a/assets/js/hooks.js b/assets/js/hooks.js index 1747844..1859c57 100644 --- a/assets/js/hooks.js +++ b/assets/js/hooks.js @@ -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() { diff --git a/assets/svelte/render.js b/assets/svelte/render.js index bb6d556..fab12eb 100644 --- a/assets/svelte/render.js +++ b/assets/svelte/render.js @@ -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()}) } diff --git a/lib/live_component.ex b/lib/live_component.ex index c5a5397..93e1707 100644 --- a/lib/live_component.ex +++ b/lib/live_component.ex @@ -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() diff --git a/lib/slots.ex b/lib/slots.ex new file mode 100644 index 0000000..a48a19f --- /dev/null +++ b/lib/slots.ex @@ -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 diff --git a/lib/ssr.ex b/lib/ssr.ex new file mode 100644 index 0000000..5ce6938 --- /dev/null +++ b/lib/ssr.ex @@ -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 diff --git a/test/svelte_view_test.exs b/test/svelte_view_test.exs deleted file mode 100644 index dea4b9c..0000000 --- a/test/svelte_view_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule LiveSvelteTest do - use ExUnit.Case - doctest LiveSvelte - - test "greets the world" do - assert LiveSvelte.hello() == :world - end -end