diff --git a/.changeset/fresh-zoos-burn.md b/.changeset/fresh-zoos-burn.md new file mode 100644 index 000000000000..6b68a0272656 --- /dev/null +++ b/.changeset/fresh-zoos-burn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add createRawSnippet API diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 37fdb9cf6c65..24f8d99a1203 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -190,3 +190,5 @@ export { tick, untrack } from './internal/client/runtime.js'; + +export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 5bcb13e5f2ef..6cea28c9f30a 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.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/index.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index c0fc350ce45c..31b02a153f14 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -6,7 +6,8 @@ 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 { assign_nodes } from '../template.js'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -60,3 +61,29 @@ export function wrap_snippet(component, fn) { } }); } + +/** + * Create a snippet imperatively using mount, hyrdate and render functions. + * @param {{ + * mount: (...params: any[]) => Element, + * hydrate?: (element: Element, ...params: any[]) => void, + * render: (...params: any[]) => string + * }} options + */ +export function createRawSnippet({ mount, hydrate }) { + var snippet_fn = (/** @type {TemplateNode} */ anchor, /** @type {any[]} */ ...params) => { + var element; + if (hydrating) { + element = hydrate_node; + hydrate_next(); + if (hydrate !== undefined) hydrate(/** @type {Element} */ (element), ...params); + } else { + element = mount(...params); + anchor.before(element); + } + assign_nodes(element, element); + }; + add_snippet_symbol(snippet_fn); + + return snippet_fn; +} diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 2cb741ad95ec..40dee1b6cdde 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -13,7 +13,7 @@ import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; -import { validate_store } from '../shared/validate.js'; +import { add_snippet_symbol, validate_store } from '../shared/validate.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -155,6 +155,22 @@ export function head(payload, fn) { head_payload.out += BLOCK_CLOSE; } +/** + * Create a snippet imperatively using mount, hyrdate and render functions. + * @param {{ + * mount: (...params: any[]) => Element, + * hydrate?: (element: Element, ...params: any[]) => void, + * render: (...params: any[]) => string + * }} options + */ +export function createRawSnippet({ render }) { + const snippet_fn = (/** @type {Payload} */ payload, /** @type {any[]} */ ...args) => { + payload.out += render(...args); + }; + add_snippet_symbol(snippet_fn); + return snippet_fn; +} + /** * @template V * @param {string} name diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_config.js new file mode 100644 index 000000000000..a22776d7e7f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/_config.js @@ -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: `
0
`, + + test({ assert, target }) { + const [b1] = target.querySelectorAll('button'); + + b1?.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, `
1
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/main.svelte new file mode 100644 index 000000000000..6568abd987e9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw-args/main.svelte @@ -0,0 +1,32 @@ + + +
+ {@render snippet(count)} +
+ diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js new file mode 100644 index 000000000000..129a5734028c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true // Render in dev mode to check that the validation error is not thrown + }, + html: `

hello world

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte new file mode 100644 index 000000000000..6a1dd3915ab2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-raw/main.svelte @@ -0,0 +1,16 @@ + + +{@render hello()} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index dff48ce8a187..4f93359e92db 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -365,12 +365,20 @@ declare module 'svelte' { export function flushSync(fn?: (() => void) | undefined): void; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Create a snippet imperatively using mount, hyrdate and render functions. + * */ + export function createRawSnippet({ mount, hydrate }: { + mount: (...params: any[]) => Element; + hydrate?: (element: Element, ...params: any[]) => void; + render: (...params: any[]) => string; + }): (anchor: TemplateNode, ...params: any[]) => void; /** * 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`. * * */ - export function mount, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { + function mount_1, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: Document | Element | ShadowRoot; anchor?: Node; props?: Props; @@ -389,7 +397,7 @@ declare module 'svelte' { * Hydrates a component on the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * */ - export function hydrate, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { + function hydrate_1, Exports extends Record>(component: ComponentType> | Component, options: {} extends Props ? { target: Document | Element | ShadowRoot; props?: Props; events?: Record any>; @@ -450,8 +458,9 @@ declare module 'svelte' { * https://svelte.dev/docs/svelte#getallcontexts * */ export function getAllContexts = Map>(): T; + type TemplateNode = Text | Element | Comment; - export {}; + export { hydrate_1 as hydrate, mount_1 as mount }; } declare module 'svelte/action' {