Skip to content

Raw snippet alternative #12425

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-zoos-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: add createRawSnippet API
2 changes: 2 additions & 0 deletions packages/svelte/src/index-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,5 @@ export {
tick,
untrack
} from './internal/client/runtime.js';

export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';
2 changes: 2 additions & 0 deletions packages/svelte/src/index-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
45 changes: 43 additions & 2 deletions packages/svelte/src/internal/client/dom/blocks/snippet.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);
}
}
);
}
22 changes: 22 additions & 0 deletions packages/svelte/src/internal/server/blocks/snippet.js
Original file line number Diff line number Diff line change
@@ -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();
});
}
4 changes: 4 additions & 0 deletions packages/svelte/src/internal/shared/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>>;
3 changes: 3 additions & 0 deletions packages/svelte/src/internal/shared/validate.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { test } from '../../test';

export default test({
snapshot(target) {
return {
p: target.querySelector('p')
};
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!--[--><p>hydrated</p><!--]-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script>
import { createRawSnippet } from 'svelte';

const snippet = createRawSnippet(() => ({
render: () => `
<p>rendered</p>
`,
setup(p) {
p.textContent = 'hydrated';
}
}));
</script>

{@render snippet()}
Original file line number Diff line number Diff line change
@@ -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']);
}
});
Original file line number Diff line number Diff line change
@@ -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}
17 changes: 17 additions & 0 deletions packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js
Original file line number Diff line number Diff line change
@@ -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>`);
}
});
Original file line number Diff line number Diff line change
@@ -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)}
10 changes: 10 additions & 0 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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 {};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading