portals
Simon Holthausen 2 months ago
parent cfccffd948
commit 2903ba9d8f
No known key found for this signature in database

@ -42,11 +42,17 @@ import { queue_micro_task } from '../task.js';
/**
* All known outlets (and portals waiting for outlets) for a given key.
* `pending` is only used during hydration it contains render functions of
* `{#portal ...}` blocks that were created before their outlet, so that the
* outlet can have them claim their server-rendered content upon initialisation.
* - `outlets` is the reactive source portals depend on
* - `all` is the canonical registry across all 'worlds' (in-flight batches and
* forks). It is the base for the read-modify-writes to `outlets`: outlets add
* and remove themselves as their effects are created and destroyed, so every
* write carries the full union and registrations of other batches are never
* lost. Batch-aware reads of `outlets` still scope each batch to its own view.
* - `pending` it contains render functions of `{#portal ...}` blocks that were
* created before their outlet, so that content is rendered upon initialisation.
* @typedef {{
* outlets: Source<Outlet[]>,
* all: Outlet[],
* pending: Set<(outlet: Outlet) => void>
* }} OutletEntry
*/
@ -68,7 +74,7 @@ function get_outlet_entry(key) {
let entry = outlet_map.get(key);
if (entry === undefined) {
entry = { outlets: source([]), pending: new Set() };
entry = { outlets: source([]), all: [], pending: new Set() };
outlet_map.set(key, entry);
}
@ -151,19 +157,15 @@ export function portal_outlet(node, get_id) {
const entry = get_outlet_entry(id);
internal_set(entry.outlets, [...entry.outlets.v, outlet]);
internal_set(entry.outlets, (entry.all = [...entry.all, outlet]));
// During hydration, portals that were created before this outlet claim
// their server-rendered content now, while the hydration position is known.
// portals that were created before this outlet claim their server-rendered content now
for (const render of entry.pending) {
render(outlet);
}
const unregister = () => {
internal_set(
entry.outlets,
entry.outlets.v.filter((o) => o !== outlet)
);
internal_set(entry.outlets, (entry.all = entry.all.filter((o) => o !== outlet)));
};
return () => {
@ -503,6 +505,7 @@ export function portal(get_target, content) {
}
for (const k of keys) {
// Do the get outside the below if-block so we're notified of changes to any outlets source
var outlets = get(get_outlet_entry(k).outlets);
if (k === key) {
@ -516,7 +519,7 @@ export function portal(get_target, content) {
if (target instanceof Element) {
targets.add(target);
} else if (hydrating) {
// an outlet with our key may appear later during this hydration
// An outlet with our key may appear later during this hydration
// pass (`{#portal ...}` before `{@portal ...}` in the markup).
// Register a callback so it can have us claim our server-rendered
// content at its position.

@ -0,0 +1,19 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
solo: true,
async test({ assert, target }) {
const btn = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `<button>fork</button>`);
btn?.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>fork</button> <div>portaled</div> <div>portaled</div>`
);
}
});

@ -0,0 +1,20 @@
<script>
import { fork } from 'svelte';
let show = $state(false);
</script>
<button onclick={async () => {
fork(() => {
show = true;
}).commit();
}}>fork</button>
{#portal 'target'}
portaled
{/portal}
{#if show}
<div>{@portal 'target'}</div>
<div>{@portal 'target'}</div>
{/if}

@ -0,0 +1,63 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [togglePortalKey, toggleOutletKey, shift, pop, commit] =
target.querySelectorAll('button');
togglePortalKey.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>toggle portalKey</button> <button>toggle outletKey</button> <button>shift</button> <button>pop</button> <button>commit</button> ab'
);
toggleOutletKey.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>toggle portalKey</button> <button>toggle outletKey</button> <button>shift</button> <button>pop</button> <button>commit</button> ab'
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>toggle portalKey</button> <button>toggle outletKey</button> <button>shift</button> <button>pop</button> <button>commit</button> bb hi'
);
shift.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>toggle portalKey</button> <button>toggle outletKey</button> <button>shift</button> <button>pop</button> <button>commit</button> bb hi'
);
commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>toggle portalKey</button> <button>toggle outletKey</button> <button>shift</button> <button>pop</button> <button>commit</button> ba'
);
toggleOutletKey.click();
await tick();
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>toggle portalKey</button> <button>toggle outletKey</button> <button>shift</button> <button>pop</button> <button>commit</button> ba'
);
commit.click();
await tick();
assert.htmlEqual(
target.innerHTML,
'<button>toggle portalKey</button> <button>toggle outletKey</button> <button>shift</button> <button>pop</button> <button>commit</button> bb hi'
);
}
});

@ -0,0 +1,35 @@
<script>
import { fork } from "svelte";
let portalKey = $state('a');
let outletKey = $state('b');
let queued = [];
let first = true;
let f;
function push(v) {
if (first) {
first = false;
return v;
}
return new Promise((resolve) => {
queued.push(() => resolve(v));
});
}
</script>
<button onclick={() => portalKey = portalKey === 'a' ? 'b' : 'a'}>toggle portalKey</button>
<button onclick={() => f = fork(() => outletKey = outletKey === 'a' ? 'b' : 'a')}>toggle outletKey</button>
<button onclick={() => queued.shift()?.()}>shift</button>
<button onclick={() => queued.pop()?.()}>pop</button>
<button onclick={() => f.commit()}>commit</button>
{await push(portalKey + outletKey)}
{#portal portalKey}
hi
{/portal}
{@portal outletKey}
Loading…
Cancel
Save