Skip to content

Commit

Permalink
feat: proper onDestroy handles
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoDog896 committed Sep 7, 2023
1 parent e73dd55 commit 1715bdd
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 9 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,20 @@ Reparent elements with ease. Svelte non-internal using alternative to [react-rep
</main>
```

## Advantages

- No need to worry about keeping state in sync between components.
- No dependencies on internal svelte APIs, unlike React and Vue alternatives.
- Simple API, with only three exported functions.

## Disadvantages

- Since this library is relatively new, there may be bugs. (Please report them! Every bug report helps!)

## How it works

This library is split into three main concepts:

- `Limbo`, which serves as the "owner" of a component to be teleported.
- `Portal`, which serves as the "receiver" of a component to be teleported, and displays it.
- Teleportation, which transfers borrowship of a component from one `Portal` to another.
Expand All @@ -49,6 +60,6 @@ registry, which maps component instances to what portal ID they belong in. When
is destroyed, it is moved back to `Limbo` and removed from the registry.

In order to move the DOM around, this library extensively uses `<div style="display: contents">`.
The usage of this allows for `svelte-portal` to *ensure* that svelte components
The usage of this allows for `svelte-portal` to _ensure_ that svelte components
have a single root element, which is moved around (in the case of `Limbo`), or
appended to (in the case of `Portal`).
7 changes: 7 additions & 0 deletions src/lib/Limbo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
-->

<script lang="ts">
import { _components } from '$lib/Portal.svelte';
import { onMount } from 'svelte';
export let component: HTMLElement;
onMount(() => {
_components.set(component, { ..._components.get(component), limbo: component });
});
</script>

<!--
Expand Down
41 changes: 35 additions & 6 deletions src/lib/Portal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,65 @@
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
let components = new Map<Container, string>();
/**
* 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: string) {
components.set(component, key);
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">
export let key: string;
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) {
$: 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} />
38 changes: 38 additions & 0 deletions src/routes/examples/basic/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<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>
4 changes: 2 additions & 2 deletions tests/test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { expect, test } from '@playwright/test';

test('index page has expected container headings', async ({ page }) => {
await page.goto('/');
await page.goto('/examples/basic');
await expect(page.getByRole('heading', { name: 'Container A' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Container B' })).toBeVisible();
});

test('componente reparenting works', async ({ page }) => {
await page.goto('/');
await page.goto('/examples/basic');

// precheck: input is in container A
const oldContainerA = page.getByRole('main').locator('div').filter({ hasText: 'Container A' });
Expand Down

0 comments on commit 1715bdd

Please sign in to comment.