Skip to content

Commit

Permalink
feat: add outro option to unmount (#14540)
Browse files Browse the repository at this point in the history
* feat: add outro option to unmount

* unused

* regenerate

* revert

* changeset

* create separate component_root effect

* docs

* return a promise

* remove from map immediately
  • Loading branch information
Rich-Harris authored Dec 14, 2024
1 parent f0af633 commit b0e3c5b
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-jeans-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: add `outro` option to `unmount`
11 changes: 7 additions & 4 deletions documentation/docs/06-runtime/04-imperative-component-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,22 @@ Note that unlike calling `new App(...)` in Svelte 4, things like effects (includ

## `unmount`

Unmounts a component created with [`mount`](#mount) or [`hydrate`](#hydrate):
Unmounts a component that was previously created with [`mount`](#mount) or [`hydrate`](#hydrate).

If `options.outro` is `true`, [transitions](transition) will play before the component is removed from the DOM:

```js
// @errors: 1109
import { mount, unmount } from 'svelte';
import App from './App.svelte';

const app = mount(App, {...});
const app = mount(App, { target: document.body });

// later
unmount(app);
unmount(app, { outro: true });
```

Returns a `Promise` that resolves after transitions have completed if `options.outro` is true, or immediately otherwise.

## `render`

Only available on the server and when compiling with the `server` option. Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app:
Expand Down
24 changes: 24 additions & 0 deletions packages/svelte/src/internal/client/reactivity/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,35 @@ export function inspect_effect(fn) {
*/
export function effect_root(fn) {
const effect = create_effect(ROOT_EFFECT, fn, true);

return () => {
destroy_effect(effect);
};
}

/**
* An effect root whose children can transition out
* @param {() => void} fn
* @returns {(options?: { outro?: boolean }) => Promise<void>}
*/
export function component_root(fn) {
const effect = create_effect(ROOT_EFFECT, fn, true);

return (options = {}) => {
return new Promise((fulfil) => {
if (options.outro) {
pause_effect(effect, () => {
destroy_effect(effect);
fulfil(undefined);
});
} else {
destroy_effect(effect);
fulfil(undefined);
}
});
};
}

/**
* @param {() => void | (() => void)} fn
* @returns {Effect}
Expand Down
33 changes: 27 additions & 6 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from './dom/operations.js';
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
import { push, pop, component_context, active_effect } from './runtime.js';
import { effect_root, branch } from './reactivity/effects.js';
import { component_root, branch } from './reactivity/effects.js';
import {
hydrate_next,
hydrate_node,
Expand Down Expand Up @@ -204,7 +204,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
// @ts-expect-error will be defined because the render effect runs synchronously
var component = undefined;

var unmount = effect_root(() => {
var unmount = component_root(() => {
var anchor_node = anchor ?? target.appendChild(create_text());

branch(() => {
Expand Down Expand Up @@ -252,7 +252,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
}

root_event_handles.delete(event_handle);
mounted_components.delete(component);

if (anchor_node !== anchor) {
anchor_node.parentNode?.removeChild(anchor_node);
}
Expand All @@ -271,14 +271,35 @@ let mounted_components = new WeakMap();

/**
* Unmounts a component that was previously mounted using `mount` or `hydrate`.
*
* If `options.outro` is `true`, [transitions](https://svelte.dev/docs/svelte/transition) will play before the component is removed from the DOM.
*
* Returns a `Promise` that resolves after transitions have completed if `options.outro` is true, or immediately otherwise.
*
* ```js
* import { mount, unmount } from 'svelte';
* import App from './App.svelte';
*
* const app = mount(App, { target: document.body });
*
* // later...
* unmount(app, { outro: true });
* ```
* @param {Record<string, any>} component
* @param {{ outro?: boolean }} [options]
* @returns {Promise<void>}
*/
export function unmount(component) {
export function unmount(component, options) {
const fn = mounted_components.get(component);

if (fn) {
fn();
} else if (DEV) {
mounted_components.delete(component);
return fn(options);
}

if (DEV) {
w.lifecycle_double_unmount();
}

return Promise.resolve();
}
18 changes: 17 additions & 1 deletion packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,8 +448,24 @@ declare module 'svelte' {
}): Exports;
/**
* Unmounts a component that was previously mounted using `mount` or `hydrate`.
*
* If `options.outro` is `true`, [transitions](https://svelte.dev/docs/svelte/transition) will play before the component is removed from the DOM.
*
* Returns a `Promise` that resolves after transitions have completed if `options.outro` is true, or immediately otherwise.
*
* ```js
* import { mount, unmount } from 'svelte';
* import App from './App.svelte';
*
* const app = mount(App, { target: document.body });
*
* // later...
* unmount(app, { outro: true });
* ```
* */
export function unmount(component: Record<string, any>): void;
export function unmount(component: Record<string, any>, options?: {
outro?: boolean;
} | undefined): Promise<void>;
/**
* Returns a promise that resolves once any pending state changes have been applied.
* */
Expand Down

0 comments on commit b0e3c5b

Please sign in to comment.