You can't hint to Svelte that it will reuse the instance, but you can make a borrowing system that will safely move a component to other containers. I've packaged this into a library, svelte-reparent
.
By making an original container, Limbo
, and renderers, Portals
, with the ability to teleport
between those components, we can also:
- Attach the lifetime to
Limbo
(safely allowing a space to go back to)
- Add destruction hooks to
Portal
that safely move it back to Limbo
Since Svelte doesn't have a Virtual DOM, it's significantly easier to move elements:
Limbo.svelte
- our container
<!--
Limbo is a place to initialize the element,
and serves as a hidden space to keep track of nodes.
Since Limbo owns the lifecycle of the current element,
the moment that Limbo gets destroyed, it will destroy its child element
that it *thinks* it owns. Because of this, it is quite safe to destroy and reinitialize
a Limbo component without causing unintended side effects to the DOM.
-->
<script lang="ts">
import { _components } from '$lib/Portal.svelte';
import { onMount } from 'svelte';
export let component: HTMLElement;
let container: HTMLDivElement;
// Register the component and its limbo
onMount(() => _components.set(component, { ..._components.get(component), limbo: container }));
</script>
<!--
We don't want to render this component,
but we use it as the initial holder before teleporting it.
This allows us to have a safe fallback
for when a Portal gets destroyed.
We also wrap it to guarantee that `component` is a DOM component,
since we can't guarantee that all svelte components only have 1 root node.
-->
<div style="display: none;" bind:this={container}>
<div style="display: contents;" bind:this={component}><slot /></div>
</div>
Portal.svelte
<script context="module" lang="ts">
import { writable } from 'svelte/store';
type Container = HTMLElement;
type Key = string | number | symbol;
/**
* Universal map to keep track of what portal a component wants to be in,
* as well as its original limbo owner.
*
* DON'T MODIFY EXTERNALLY!
* Doing so is **undefined behavior**.
*/
export let _components = new Map<
Container,
{
limbo?: HTMLElement;
key?: Key;
}
>();
// dirty tracker - a Map isn't reactive, so we need to coerce Svelte to re-render
let dirty = writable(Symbol());
export async function teleport(component: Container, key: Key) {
_components.set(component, { ..._components.get(component), key });
// trigger a re-render
dirty.set(Symbol());
}
</script>
<script lang="ts">
import { onDestroy } from 'svelte';
export let key: Key;
export let component: Container;
/*
- component may be nil before mount
- listen to dirty to force a re-render
*/
$: if (component && $dirty && _components.get(component)?.key == key) {
// appendChild forces a move, not a copy - we can safely use this as the DOM
// handles ownership of the node for us
container.appendChild(component);
}
let container: HTMLDivElement;
onDestroy(() => {
// check if we own the component
const { limbo, key: localKey } = _components.get(component) || {};
if (localKey !== key) return;
_components.delete(component);
// move the component back to the limbo till it gets re-mounted
limbo?.appendChild(component);
// trigger a re-render
dirty.set(Symbol());
});
</script>
<div style="display: contents;" bind:this={container} />
+page.svelte (example)
:
<script lang="ts">
import { onMount } from 'svelte';
import { Portal, Limbo, teleport } from '$lib';
let component: HTMLElement;
function send(label: string) {
return () => {
teleport(component, label);
};
}
onMount((): void => send('a')());
</script>
<main>
<Limbo bind:component>
<input placeholder="Enter unkept state" />
</Limbo>
<div class="container">
<h1>Container A</h1>
<Portal key="a" {component} />
<button on:click={send('a')}>Move Component Here</button>
</div>
<div class="container">
<h1>Container B</h1>
<Portal key="b" {component} />
<button on:click={send('b')}>Move Component Here</button>
</div>
</main>
<style>
.container {
border: 1px solid black;
margin: 1rem;
padding: 1rem;
}
</style>
Uploaded Example