Raw snippet alternative (#12425)

* feat: add createRawSnippet API

* handle missing hydrate function, improve types

* fix

* tweak types

* beef up test

* build

* types

* oops this was temporary

* typo

* regenerate types

* make mount/render optional, error if missing

* move code to new module

* test hydration

* simpler createRawSnippet API

* regenerate types

* change signature

* docs

* h1 -> node

* allow `setup` to return a teardown function

---------

Co-authored-by: Dominic Gannaway <dg@domgan.com>
pull/12472/head
Rich Harris 4 months ago committed by GitHub
parent 9666215e6d
commit c287bd503d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: add createRawSnippet API

@ -190,3 +190,5 @@ export {
tick,
untrack
} from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';

@ -35,3 +35,5 @@ export function unmount() {
export async function tick() {}
export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js';
export { createRawSnippet } from './internal/server/blocks/snippet.js';

@ -1,12 +1,16 @@
/** @import { Snippet } from 'svelte' */
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { add_snippet_symbol } from '../../../shared/validate.js';
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { branch, block, destroy_effect } from '../../reactivity/effects.js';
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
import {
dev_current_component_function,
set_dev_current_component_function
} from '../../runtime.js';
import { hydrate_node, hydrating } from '../hydration.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
@ -60,3 +64,40 @@ export function wrap_snippet(component, fn) {
}
});
}
/**
* Create a snippet programmatically
* @template {unknown[]} Params
* @param {(...params: Getters<Params>) => {
* render: () => string
* setup?: (element: Element) => void
* }} fn
* @returns {Snippet<Params>}
*/
export function createRawSnippet(fn) {
return add_snippet_symbol(
(/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
var snippet = fn(...params);
/** @type {Element} */
var element;
if (hydrating) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
} else {
var html = snippet.render().trim();
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (fragment.firstChild);
anchor.before(element);
}
const result = snippet.setup?.(element);
assign_nodes(element, element);
if (typeof result === 'function') {
teardown(result);
}
}
);
}

@ -0,0 +1,22 @@
/** @import { Snippet } from 'svelte' */
/** @import { Payload } from '#server' */
/** @import { Getters } from '#shared' */
import { add_snippet_symbol } from '../../shared/validate.js';
/**
* Create a snippet programmatically
* @template {unknown[]} Params
* @param {(...params: Getters<Params>) => {
* render: () => string
* setup?: (element: Element) => void
* }} fn
* @returns {Snippet<Params>}
*/
export function createRawSnippet(fn) {
return add_snippet_symbol((/** @type {Payload} */ payload, /** @type {Params} */ ...args) => {
var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value));
payload.out += fn(...getters)
.render()
.trim();
});
}

@ -7,4 +7,8 @@ export type SourceLocation =
| [line: number, column: number]
| [line: number, column: number, SourceLocation[]];
export type Getters<T> = {
[K in keyof T]: () => T[K];
};
export type Snapshot<T> = ReturnType<typeof $state.snapshot<T>>;

@ -1,3 +1,5 @@
/** @import { TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { is_void } from '../../constants.js';
import * as w from './warnings.js';
import * as e from './errors.js';
@ -6,6 +8,7 @@ const snippet_symbol = Symbol.for('svelte.snippet');
/**
* @param {any} fn
* @returns {import('svelte').Snippet}
*/
export function add_snippet_symbol(fn) {
fn[snippet_symbol] = true;

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
snapshot(target) {
return {
p: target.querySelector('p')
};
}
});

@ -0,0 +1,14 @@
<script>
import { createRawSnippet } from 'svelte';
const snippet = createRawSnippet(() => ({
render: () => `
<p>rendered</p>
`,
setup(p) {
p.textContent = 'hydrated';
}
}));
</script>
{@render snippet()}

@ -0,0 +1,11 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ target, assert, logs }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.deepEqual(logs, ['tearing down']);
}
});

@ -0,0 +1,18 @@
<script>
import { createRawSnippet } from 'svelte';
let show = $state(true);
const snippet = createRawSnippet(() => ({
render: () => `<hr>`,
setup(p) {
return () => console.log('tearing down')
}
}));
</script>
<button onclick={() => show = !show}>click</button>
{#if show}
{@render snippet()}
{/if}

@ -0,0 +1,17 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true // Render in dev mode to check that the validation error is not thrown
},
html: `<button>click</button><p>clicks: 0</p>`,
test({ target, assert }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<button>click</button><p>clicks: 1</p>`);
}
});

@ -0,0 +1,20 @@
<script>
import { createRawSnippet } from 'svelte';
let count = $state(0);
const hello = createRawSnippet((count) => ({
render: () => `
<p>clicks: ${count()}</p>
`,
setup(p) {
$effect(() => {
p.textContent = `clicks: ${count()}`
});
}
}));
</script>
<button onclick={() => count += 1}>click</button>
{@render hello(count)}

@ -365,6 +365,13 @@ declare module 'svelte' {
export function flushSync(fn?: (() => void) | undefined): void;
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;
/**
* Create a snippet programmatically
* */
export function createRawSnippet<Params extends unknown[]>(fn: (...params: Getters<Params>) => {
render: () => string;
setup?: (element: Element) => void;
}): Snippet<Params>;
/**
* Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component.
* Transitions will play during the initial render unless the `intro` option is set to `false`.
@ -450,6 +457,9 @@ declare module 'svelte' {
* https://svelte.dev/docs/svelte#getallcontexts
* */
export function getAllContexts<T extends Map<any, any> = Map<any, any>>(): T;
type Getters<T> = {
[K in keyof T]: () => T[K];
};
export {};
}

@ -256,6 +256,10 @@ We can tighten things up further by declaring a generic, so that `data` and `row
</script>
```
## Creating snippets programmatically
In advanced scenarios, you may need to create a snippet programmatically. For this, you can use [`createRawSnippet`](/docs/imports#svelte-createrawsnippet)
## Snippets and slots
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/special-elements#slot). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5.

@ -93,6 +93,37 @@ To prevent something from being treated as an `$effect`/`$derived` dependency, u
</script>
```
### `createRawSnippet`
An advanced API designed for people building frameworks that integrate with Svelte, `createRawSnippet` allows you to create [snippets](/docs/snippets) programmatically for use with `{@render ...}` tags:
```js
import { createRawSnippet } from 'svelte';
const greet = createRawSnippet((name) => {
return {
render: () => `
<h1>Hello ${name()}!</h1>
`,
setup: (node) => {
$effect(() => {
node.textContent = `Hello ${name()}!`;
});
}
};
});
```
The `render` function is called during server-side rendering, or during `mount` (but not during `hydrate`, because it already ran on the server), and must return HTML representing a single element.
The `setup` function is called during `mount` or `hydrate` with that same element as its sole argument. It is responsible for ensuring that the DOM is updated when the arguments change their value — in this example, when `name` changes:
```svelte
{@render greet(name)}
```
If `setup` returns a function, it will be called when the snippet is unmounted. If the snippet is fully static, you can omit the `setup` function altogether.
## `svelte/reactivity`
Svelte provides reactive `SvelteMap`, `SvelteSet`, `SvelteDate` and `SvelteURL` classes. These can be imported from `svelte/reactivity` and used just like their native counterparts. [Demo:](https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE32QwUrEMBBAf2XMpQrb9t7tFrx7UjxZYWM6NYFkEpJJ16X03yWK9OQeZ3iPecwqZmMxie5tFSQdik48hiAOgq-hDGlByygOIvkcVdn0SUUTeBhpZOOCjwwrvPxgr89PsMEcvYPqV2wjSsVmMXytjiMVR3lKDDlaOAHhZVfvK80cUte2-CVdsNgo79ogWVcPx5H6dj9M_V1dg9KSPjEBe2CNCZumgboeRuoNhczwYWjqFmkzntYcbROiZ6-83f5HtE9c3nADKUF_yEi9jnvQxVgLOUySEc464nwGSRMsRiEsGJO8mVeEbRAH4fxkZoOT6Dhm3N63b9_bGfOlAQAA)

Loading…
Cancel
Save