summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/clever-pumpkins-begin.md7
-rw-r--r--.changeset/lovely-bulldogs-admire.md29
-rw-r--r--.changeset/mean-ears-mate.md7
-rw-r--r--.changeset/tough-ants-rest.md8
-rw-r--r--packages/astro/e2e/fixtures/nested-recursive/astro.config.mjs12
-rw-r--r--packages/astro/e2e/fixtures/nested-recursive/package.json24
-rw-r--r--packages/astro/e2e/fixtures/nested-recursive/src/components/PreactCounter.tsx17
-rw-r--r--packages/astro/e2e/fixtures/nested-recursive/src/components/ReactCounter.jsx17
-rw-r--r--packages/astro/e2e/fixtures/nested-recursive/src/components/SolidCounter.tsx17
-rw-r--r--packages/astro/e2e/fixtures/nested-recursive/src/components/SvelteCounter.svelte29
-rw-r--r--packages/astro/e2e/fixtures/nested-recursive/src/components/VueCounter.vue34
-rw-r--r--packages/astro/e2e/fixtures/nested-recursive/src/pages/index.astro28
-rw-r--r--packages/astro/e2e/nested-recursive.test.js96
-rw-r--r--packages/astro/src/@types/astro.ts2
-rw-r--r--packages/astro/src/runtime/server/astro-island.ts27
-rw-r--r--packages/astro/src/runtime/server/index.ts51
-rw-r--r--packages/astro/test/fixtures/lit-element/src/components/my-element.js4
-rw-r--r--packages/astro/test/fixtures/lit-element/src/pages/slots.astro15
-rw-r--r--packages/astro/test/fixtures/slots-preact/src/components/Counter.jsx4
-rw-r--r--packages/astro/test/fixtures/slots-preact/src/pages/index.astro2
-rw-r--r--packages/astro/test/fixtures/slots-preact/src/pages/markdown.md9
-rw-r--r--packages/astro/test/fixtures/slots-react/src/components/Counter.jsx4
-rw-r--r--packages/astro/test/fixtures/slots-react/src/pages/index.astro2
-rw-r--r--packages/astro/test/fixtures/slots-react/src/pages/markdown.md9
-rw-r--r--packages/astro/test/fixtures/slots-solid/src/components/Counter.jsx4
-rw-r--r--packages/astro/test/fixtures/slots-solid/src/pages/index.astro2
-rw-r--r--packages/astro/test/fixtures/slots-solid/src/pages/markdown.md9
-rw-r--r--packages/astro/test/fixtures/slots-svelte/src/components/Counter.svelte4
-rw-r--r--packages/astro/test/fixtures/slots-svelte/src/pages/index.astro8
-rw-r--r--packages/astro/test/fixtures/slots-svelte/src/pages/markdown.md9
-rw-r--r--packages/astro/test/fixtures/slots-vue/src/components/Counter.vue2
-rw-r--r--packages/astro/test/fixtures/slots-vue/src/pages/index.astro2
-rw-r--r--packages/astro/test/fixtures/slots-vue/src/pages/markdown.md9
-rw-r--r--packages/astro/test/lit-element.test.js19
-rw-r--r--packages/astro/test/slots-preact.test.js32
-rw-r--r--packages/astro/test/slots-react.test.js32
-rw-r--r--packages/astro/test/slots-solid.test.js32
-rw-r--r--packages/astro/test/slots-svelte.test.js34
-rw-r--r--packages/astro/test/slots-vue.test.js32
-rw-r--r--packages/integrations/lit/server.js16
-rw-r--r--packages/integrations/preact/client.js5
-rw-r--r--packages/integrations/preact/server.js13
-rw-r--r--packages/integrations/preact/static-html.js4
-rw-r--r--packages/integrations/react/client-v17.js5
-rw-r--r--packages/integrations/react/client.js5
-rw-r--r--packages/integrations/react/server-v17.js15
-rw-r--r--packages/integrations/react/server.js15
-rw-r--r--packages/integrations/react/static-html.js5
-rw-r--r--packages/integrations/solid/client.js38
-rw-r--r--packages/integrations/solid/server.js29
-rw-r--r--packages/integrations/solid/static-html.js12
-rw-r--r--packages/integrations/svelte/Wrapper.svelte21
-rw-r--r--packages/integrations/svelte/Wrapper.svelte.ssr.js19
-rw-r--r--packages/integrations/svelte/client.js36
-rw-r--r--packages/integrations/svelte/server.js14
-rw-r--r--packages/integrations/vue/client.js6
-rw-r--r--packages/integrations/vue/server.js6
-rw-r--r--packages/integrations/vue/static-html.js5
-rw-r--r--packages/webapi/mod.d.ts2
-rw-r--r--pnpm-lock.yaml29
60 files changed, 827 insertions, 157 deletions
diff --git a/.changeset/clever-pumpkins-begin.md b/.changeset/clever-pumpkins-begin.md
new file mode 100644
index 000000000..26b9e5f18
--- /dev/null
+++ b/.changeset/clever-pumpkins-begin.md
@@ -0,0 +1,7 @@
+---
+'@astrojs/lit': minor
+---
+
+Adds support for passing named slots from `.astro` => Lit components.
+
+All slots are treated as Light DOM content.
diff --git a/.changeset/lovely-bulldogs-admire.md b/.changeset/lovely-bulldogs-admire.md
new file mode 100644
index 000000000..74888cb27
--- /dev/null
+++ b/.changeset/lovely-bulldogs-admire.md
@@ -0,0 +1,29 @@
+---
+'@astrojs/preact': minor
+'@astrojs/react': minor
+'@astrojs/solid-js': minor
+---
+
+Add support for passing named slots from `.astro` => framework components.
+
+Each `slot` is be passed as a top-level prop. For example:
+
+```jsx
+// From .astro
+<Component>
+ <h2 slot="title">Hello world!</h2>
+ <h2 slot="slot-with-dash">Dash</h2>
+ <div>Default</div>
+</Component>
+
+// For .jsx
+export default function Component({ title, slotWithDash, children }) {
+ return (
+ <>
+ <div id="title">{title}</div>
+ <div id="slot-with-dash">{slotWithDash}</div>
+ <div id="main">{children}</div>
+ </>
+ )
+}
+```
diff --git a/.changeset/mean-ears-mate.md b/.changeset/mean-ears-mate.md
new file mode 100644
index 000000000..6bb538e4e
--- /dev/null
+++ b/.changeset/mean-ears-mate.md
@@ -0,0 +1,7 @@
+---
+'astro': patch
+---
+
+Add renderer support for passing named slots to framework components.
+
+**BREAKING**: integrations using the `addRenderer()` API are now passed all named slots via `Record<string, string>` rather than `string`. Previously only the default slot was passed.
diff --git a/.changeset/tough-ants-rest.md b/.changeset/tough-ants-rest.md
new file mode 100644
index 000000000..f95ae30c2
--- /dev/null
+++ b/.changeset/tough-ants-rest.md
@@ -0,0 +1,8 @@
+---
+'@astrojs/svelte': minor
+'@astrojs/vue': minor
+---
+
+Adds support for passing named slots from `.astro` => framework components.
+
+Inside your components, use the built-in `slot` API as you normally would.
diff --git a/packages/astro/e2e/fixtures/nested-recursive/astro.config.mjs b/packages/astro/e2e/fixtures/nested-recursive/astro.config.mjs
new file mode 100644
index 000000000..4b50887cd
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-recursive/astro.config.mjs
@@ -0,0 +1,12 @@
+import { defineConfig } from 'astro/config';
+import preact from '@astrojs/preact';
+import react from '@astrojs/react';
+import svelte from '@astrojs/svelte';
+import vue from '@astrojs/vue';
+import solid from '@astrojs/solid-js';
+
+// https://astro.build/config
+export default defineConfig({
+ // Enable many frameworks to support all different kinds of components.
+ integrations: [preact(), react(), svelte(), vue(), solid()],
+});
diff --git a/packages/astro/e2e/fixtures/nested-recursive/package.json b/packages/astro/e2e/fixtures/nested-recursive/package.json
new file mode 100644
index 000000000..3376ef596
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-recursive/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@e2e/nested-recursive",
+ "version": "0.0.0",
+ "private": true,
+ "devDependencies": {
+ "@astrojs/preact": "workspace:*",
+ "@astrojs/react": "workspace:*",
+ "@astrojs/solid-js": "workspace:*",
+ "@astrojs/svelte": "workspace:*",
+ "@astrojs/vue": "workspace:*",
+ "astro": "workspace:*"
+ },
+ "dependencies": {
+ "preact": "^10.7.3",
+ "react": "^18.1.0",
+ "react-dom": "^18.1.0",
+ "solid-js": "^1.4.3",
+ "svelte": "^3.48.0",
+ "vue": "^3.2.36"
+ },
+ "scripts": {
+ "dev": "astro dev"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/nested-recursive/src/components/PreactCounter.tsx b/packages/astro/e2e/fixtures/nested-recursive/src/components/PreactCounter.tsx
new file mode 100644
index 000000000..32200f41f
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-recursive/src/components/PreactCounter.tsx
@@ -0,0 +1,17 @@
+import { useState } from 'preact/hooks';
+
+/** a counter written in Preact */
+export default function PreactCounter({ children, id }) {
+ const [count, setCount] = useState(0);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+ <div id={id} class="counter">
+ <button class="decrement" onClick={subtract}>-</button>
+ <pre id={`${id}-count`}>{count}</pre>
+ <button id={`${id}-increment`} class="increment" onClick={add}>+</button>
+ <div class="children">{children}</div>
+ </div>
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-recursive/src/components/ReactCounter.jsx b/packages/astro/e2e/fixtures/nested-recursive/src/components/ReactCounter.jsx
new file mode 100644
index 000000000..6b3a1de5f
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-recursive/src/components/ReactCounter.jsx
@@ -0,0 +1,17 @@
+import { useState } from 'react';
+
+/** a counter written in React */
+export default function ReactCounter({ children, id }) {
+ const [count, setCount] = useState(0);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+ <div id={id} className="counter">
+ <button className="decrement" onClick={subtract}>-</button>
+ <pre id={`${id}-count`}>{count}</pre>
+ <button id={`${id}-increment`} className="increment" onClick={add}>+</button>
+ <div className="children">{children}</div>
+ </div>
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-recursive/src/components/SolidCounter.tsx b/packages/astro/e2e/fixtures/nested-recursive/src/components/SolidCounter.tsx
new file mode 100644
index 000000000..afabe43b9
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-recursive/src/components/SolidCounter.tsx
@@ -0,0 +1,17 @@
+import { createSignal } from 'solid-js';
+
+/** a counter written with Solid */
+export default function SolidCounter({ children, id }) {
+ const [count, setCount] = createSignal(0);
+ const add = () => setCount(count() + 1);
+ const subtract = () => setCount(count() - 1);
+
+ return (
+ <div id={id} class="counter">
+ <button class="decrement" onClick={subtract}>-</button>
+ <pre id={`${id}-count`}>{count()}</pre>
+ <button id={`${id}-increment`} class="increment" onClick={add}>+</button>
+ <div class="children">{children}</div>
+ </div>
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-recursive/src/components/SvelteCounter.svelte b/packages/astro/e2e/fixtures/nested-recursive/src/components/SvelteCounter.svelte
new file mode 100644
index 000000000..733f58076
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-recursive/src/components/SvelteCounter.svelte
@@ -0,0 +1,29 @@
+
+<script>
+ export let id;
+ let children;
+ let count = 0;
+
+ function add() {
+ count += 1;
+ }
+
+ function subtract() {
+ count -= 1;
+ }
+</script>
+
+<div {id} class="counter">
+ <button class="decrement" on:click={subtract}>-</button>
+ <pre id={`${id}-count`}>{ count }</pre>
+ <button id={`${id}-increment`} class="increment" on:click={add}>+</button>
+ <div class="children">
+ <slot />
+ </div>
+</div>
+
+<style>
+ .counter {
+ background: white;
+ }
+</style>
diff --git a/packages/astro/e2e/fixtures/nested-recursive/src/components/VueCounter.vue b/packages/astro/e2e/fixtures/nested-recursive/src/components/VueCounter.vue
new file mode 100644
index 000000000..d404cc965
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-recursive/src/components/VueCounter.vue
@@ -0,0 +1,34 @@
+<template>
+ <div :id="id" class="counter">
+ <button class="decrement" @click="subtract()">-</button>
+ <pre :id="`${id}-count`">{{ count }}</pre>
+ <button :id="`${id}-increment`" class="increment" @click="add()">+</button>
+ <div class="children">
+ <slot />
+ </div>
+ </div>
+</template>
+
+<script>
+import { ref } from 'vue';
+export default {
+ props: {
+ id: {
+ type: String,
+ required: true
+ }
+ },
+ setup(props) {
+ const count = ref(0);
+ const add = () => (count.value = count.value + 1);
+ const subtract = () => (count.value = count.value - 1);
+
+ return {
+ id: props.id,
+ count,
+ add,
+ subtract,
+ };
+ },
+};
+</script>
diff --git a/packages/astro/e2e/fixtures/nested-recursive/src/pages/index.astro b/packages/astro/e2e/fixtures/nested-recursive/src/pages/index.astro
new file mode 100644
index 000000000..685c7fb5e
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-recursive/src/pages/index.astro
@@ -0,0 +1,28 @@
+---
+import ReactCounter from '../components/ReactCounter.jsx';
+import PreactCounter from '../components/PreactCounter.tsx';
+import SolidCounter from '../components/SolidCounter.tsx';
+import VueCounter from '../components/VueCounter.vue';
+import SvelteCounter from '../components/SvelteCounter.svelte';
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
+ </head>
+ <body>
+ <main>
+ <ReactCounter id="react-counter" client:idle>
+ <PreactCounter id="preact-counter" client:idle>
+ <SolidCounter id="solid-counter" client:idle>
+ <SvelteCounter id="svelte-counter" client:idle>
+ <VueCounter id="vue-counter" client:idle />
+ </SvelteCounter>
+ </SolidCounter>
+ </PreactCounter>
+ </ReactCounter>
+ </main>
+ </body>
+</html>
diff --git a/packages/astro/e2e/nested-recursive.test.js b/packages/astro/e2e/nested-recursive.test.js
new file mode 100644
index 000000000..ae981189a
--- /dev/null
+++ b/packages/astro/e2e/nested-recursive.test.js
@@ -0,0 +1,96 @@
+import { test as base, expect } from '@playwright/test';
+import { loadFixture } from './test-utils.js';
+
+const test = base.extend({
+ astro: async ({}, use) => {
+ const fixture = await loadFixture({ root: './fixtures/nested-recursive/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Recursive Nested Frameworks', () => {
+ test('React counter', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const counter = await page.locator('#react-counter');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = await counter.locator('#react-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#react-counter-increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('Preact counter', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const counter = await page.locator('#preact-counter');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = await counter.locator('#preact-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#preact-counter-increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('Solid counter', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const counter = await page.locator('#solid-counter');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = await counter.locator('#solid-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#solid-counter-increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('Vue counter', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const counter = await page.locator('#vue-counter');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = await counter.locator('#vue-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#vue-counter-increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+
+ test('Svelte counter', async ({ astro, page }) => {
+ await page.goto('/');
+
+ const counter = await page.locator('#svelte-counter');
+ await expect(counter, 'component is visible').toBeVisible();
+
+ const count = await counter.locator('#svelte-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#svelte-counter-increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+});
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index dc2af7db3..d2ef92365 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -737,7 +737,7 @@ export interface AstroConfig extends z.output<typeof AstroConfigSchema> {
export type AsyncRendererComponentFn<U> = (
Component: any,
props: any,
- children: string | undefined,
+ slots: Record<string, string>,
metadata?: AstroComponentMetadata
) => Promise<U>;
diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts
index a067c9a57..10c69aa8e 100644
--- a/packages/astro/src/runtime/server/astro-island.ts
+++ b/packages/astro/src/runtime/server/astro-island.ts
@@ -64,23 +64,24 @@ declare const Astro: {
if (!this.hydrator || this.parentElement?.closest('astro-island[ssr]')) {
return;
}
- let innerHTML: string | null = null;
- let fragment = this.querySelector('astro-fragment');
- if (fragment == null && this.hasAttribute('tmpl')) {
- // If there is no child fragment, check to see if there is a template.
- // This happens if children were passed but the client component did not render any.
- let template = this.querySelector('template[data-astro-template]');
- if (template) {
- innerHTML = template.innerHTML;
- template.remove();
- }
- } else if (fragment) {
- innerHTML = fragment.innerHTML;
+ const slotted = this.querySelectorAll('astro-slot');
+ const slots: Record<string, string> = {};
+ // Always check to see if there are templates.
+ // This happens if slots were passed but the client component did not render them.
+ const templates = this.querySelectorAll('template[data-astro-template]');
+ for (const template of templates) {
+ if (!template.closest(this.tagName)?.isSameNode(this)) continue;
+ slots[template.getAttribute('data-astro-template') || 'default'] = template.innerHTML;
+ template.remove();
+ }
+ for (const slot of slotted) {
+ if (!slot.closest(this.tagName)?.isSameNode(this)) continue;
+ slots[slot.getAttribute('name') || 'default'] = slot.innerHTML;
}
const props = this.hasAttribute('props')
? JSON.parse(this.getAttribute('props')!, reviver)
: {};
- this.hydrator(this)(this.Component, props, innerHTML, {
+ this.hydrator(this)(this.Component, props, slots, {
client: this.getAttribute('client'),
});
this.removeAttribute('ssr');
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 6c7b12699..1b78d7171 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -208,7 +208,16 @@ Did you mean to add ${formatList(probableRendererNames.map((r) => '`' + r + '`')
throw new Error(message);
}
- const children = await renderSlot(result, slots?.default);
+ const children: Record<string, string> = {};
+ if (slots) {
+ await Promise.all(
+ Object.entries(slots).map(([key, value]) =>
+ renderSlot(result, value as string).then((output) => {
+ children[key] = output;
+ })
+ )
+ );
+ }
// Call the renderers `check` hook to see if any claim this component.
let renderer: SSRLoadedRenderer | undefined;
if (metadata.hydrate !== 'only') {
@@ -307,11 +316,12 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
// This is a custom element without a renderer. Because of that, render it
// as a string and the user is responsible for adding a script tag for the component definition.
if (!html && typeof Component === 'string') {
+ const childSlots = Object.values(children).join('');
html = await renderAstroComponent(
await render`<${Component}${internalSpreadAttributes(props)}${markHTMLString(
- (children == null || children == '') && voidElementNames.test(Component)
+ childSlots === '' && voidElementNames.test(Component)
? `/>`
- : `>${children == null ? '' : children}</${Component}>`
+ : `>${childSlots}</${Component}>`
)}`
);
}
@@ -320,7 +330,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
if (isPage) {
return html;
}
- return markHTMLString(html.replace(/\<\/?astro-fragment\>/g, ''));
+ return markHTMLString(html.replace(/\<\/?astro-slot\>/g, ''));
}
// Include componentExport name, componentUrl, and props in hash to dedupe identical islands
@@ -336,13 +346,30 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
);
result._metadata.needsHydrationStyles = true;
- // Render a template if no fragment is provided.
- const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html);
- const template = needsAstroTemplate ? `<template data-astro-template>${children}</template>` : '';
-
- if (needsAstroTemplate) {
- island.props.tmpl = '';
- }
+ // Render template if not all astro fragments are provided.
+ let unrenderedSlots: string[] = [];
+ if (html) {
+ if (Object.keys(children).length > 0) {
+ for (const key of Object.keys(children)) {
+ if (!html.includes(key === 'default' ? `<astro-slot>` : `<astro-slot name="${key}">`)) {
+ unrenderedSlots.push(key);
+ }
+ }
+ }
+ } else {
+ unrenderedSlots = Object.keys(children);
+ }
+ const template =
+ unrenderedSlots.length > 0
+ ? unrenderedSlots
+ .map(
+ (key) =>
+ `<template data-astro-template${key !== 'default' ? `="${key}"` : ''}>${
+ children[key]
+ }</template>`
+ )
+ .join('')
+ : '';
island.children = `${html ?? ''}${template}`;
@@ -652,7 +679,7 @@ export async function renderHead(result: SSRResult): Promise<string> {
styles.push(
renderElement('style', {
props: {},
- children: 'astro-island, astro-fragment { display: contents; }',
+ children: 'astro-island, astro-slot { display: contents; }',
})
);
}
diff --git a/packages/astro/test/fixtures/lit-element/src/components/my-element.js b/packages/astro/test/fixtures/lit-element/src/components/my-element.js
index b2cf72dea..e946924cf 100644
--- a/packages/astro/test/fixtures/lit-element/src/components/my-element.js
+++ b/packages/astro/test/fixtures/lit-element/src/components/my-element.js
@@ -29,6 +29,10 @@ export class MyElement extends LitElement {
<div id="str">${this.str}</div>
<div id="data">data: ${this.obj.data}</div>
<div id="win">${typeofwindow}</div>
+
+ <!-- Slots -->
+ <div id="default"><slot /></div>
+ <div id="named"><slot name="named" /></div>
`;
}
}
diff --git a/packages/astro/test/fixtures/lit-element/src/pages/slots.astro b/packages/astro/test/fixtures/lit-element/src/pages/slots.astro
new file mode 100644
index 000000000..b8fc4963c
--- /dev/null
+++ b/packages/astro/test/fixtures/lit-element/src/pages/slots.astro
@@ -0,0 +1,15 @@
+---
+import {MyElement} from '../components/my-element.js';
+---
+
+<html>
+<head>
+ <title>LitElement | Slot</title>
+</head>
+<body>
+ <MyElement>
+ <div>default</div>
+ <div slot="named">named</div>
+ </MyElement>
+</body>
+</html>
diff --git a/packages/astro/test/fixtures/slots-preact/src/components/Counter.jsx b/packages/astro/test/fixtures/slots-preact/src/components/Counter.jsx
index cc11b9ee3..16d2a95b9 100644
--- a/packages/astro/test/fixtures/slots-preact/src/components/Counter.jsx
+++ b/packages/astro/test/fixtures/slots-preact/src/components/Counter.jsx
@@ -1,7 +1,7 @@
import { h, Fragment } from 'preact';
import { useState } from 'preact/hooks'
-export default function Counter({ children, count: initialCount, case: id }) {
+export default function Counter({ named, dashCase, children, count: initialCount, case: id }) {
const [count, setCount] = useState(initialCount);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);
@@ -15,6 +15,8 @@ export default function Counter({ children, count: initialCount, case: id }) {
</div>
<div id={id} className="counter-message">
{children || <h1>Fallback</h1>}
+ {named}
+ {dashCase}
</div>
</>
);
diff --git a/packages/astro/test/fixtures/slots-preact/src/pages/index.astro b/packages/astro/test/fixtures/slots-preact/src/pages/index.astro
index f8f101e73..b2b039566 100644
--- a/packages/astro/test/fixtures/slots-preact/src/pages/index.astro
+++ b/packages/astro/test/fixtures/slots-preact/src/pages/index.astro
@@ -8,4 +8,6 @@ import Counter from '../components/Counter.jsx'
<Counter case="false" client:visible>{false}</Counter>
<Counter case="string" client:visible>{''}</Counter>
<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
+ <Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
+ <Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>
</main>
diff --git a/packages/astro/test/fixtures/slots-preact/src/pages/markdown.md b/packages/astro/test/fixtures/slots-preact/src/pages/markdown.md
new file mode 100644
index 000000000..f86720fea
--- /dev/null
+++ b/packages/astro/test/fixtures/slots-preact/src/pages/markdown.md
@@ -0,0 +1,9 @@
+---
+setup: import Counter from '../components/Counter.jsx'
+---
+
+# Slots: Preact
+
+<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
+<Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
+<Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>
diff --git a/packages/astro/test/fixtures/slots-react/src/components/Counter.jsx b/packages/astro/test/fixtures/slots-react/src/components/Counter.jsx
index 93f267ca4..733cc47cc 100644
--- a/packages/astro/test/fixtures/slots-react/src/components/Counter.jsx
+++ b/packages/astro/test/fixtures/slots-react/src/components/Counter.jsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
-export default function Counter({ children, count: initialCount, case: id }) {
+export default function Counter({ named, dashCase, children, count: initialCount, case: id }) {
const [count, setCount] = useState(initialCount);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);
@@ -14,6 +14,8 @@ export default function Counter({ children, count: initialCount, case: id }) {
</div>
<div id={id} className="counter-message">
{children || <h1>Fallback</h1>}
+ {named}
+ {dashCase}
</div>
</>
);
diff --git a/packages/astro/test/fixtures/slots-react/src/pages/index.astro b/packages/astro/test/fixtures/slots-react/src/pages/index.astro
index f8f101e73..b2b039566 100644
--- a/packages/astro/test/fixtures/slots-react/src/pages/index.astro
+++ b/packages/astro/test/fixtures/slots-react/src/pages/index.astro
@@ -8,4 +8,6 @@ import Counter from '../components/Counter.jsx'
<Counter case="false" client:visible>{false}</Counter>
<Counter case="string" client:visible>{''}</Counter>
<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
+ <Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
+ <Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>
</main>
diff --git a/packages/astro/test/fixtures/slots-react/src/pages/markdown.md b/packages/astro/test/fixtures/slots-react/src/pages/markdown.md
new file mode 100644
index 000000000..308450506
--- /dev/null
+++ b/packages/astro/test/fixtures/slots-react/src/pages/markdown.md
@@ -0,0 +1,9 @@
+---
+setup: import Counter from '../components/Counter.jsx'
+---
+
+# Slots: React
+
+<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
+<Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
+<Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>
diff --git a/packages/astro/test/fixtures/slots-solid/src/components/Counter.jsx b/packages/astro/test/fixtures/slots-solid/src/components/Counter.jsx
index 6a585b8e3..4b9c63c66 100644
--- a/packages/astro/test/fixtures/slots-solid/src/components/Counter.jsx
+++ b/packages/astro/test/fixtures/slots-solid/src/components/Counter.jsx
@@ -1,6 +1,6 @@
import { createSignal } from 'solid-js';
-export default function Counter({ children, count: initialCount, case: id }) {
+export default function Counter({ named, dashCase, children, count: initialCount, case: id }) {
const [count] = createSignal(0);
return (
<>
@@ -9,6 +9,8 @@ export default function Counter({ children, count: initialCount, case: id }) {
</div>
<div id={id} className="counter-message">
{children || <h1>Fallback</h1>}
+ {named}
+ {dashCase}
</div>
</>
);
diff --git a/packages/astro/test/fixtures/slots-solid/src/pages/index.astro b/packages/astro/test/fixtures/slots-solid/src/pages/index.astro
index f8f101e73..b2b039566 100644
--- a/packages/astro/test/fixtures/slots-solid/src/pages/index.astro
+++ b/packages/astro/test/fixtures/slots-solid/src/pages/index.astro
@@ -8,4 +8,6 @@ import Counter from '../components/Counter.jsx'
<Counter case="false" client:visible>{false}</Counter>
<Counter case="string" client:visible>{''}</Counter>
<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
+ <Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
+ <Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>
</main>
diff --git a/packages/astro/test/fixtures/slots-solid/src/pages/markdown.md b/packages/astro/test/fixtures/slots-solid/src/pages/markdown.md
new file mode 100644
index 000000000..d9bc2dabd
--- /dev/null
+++ b/packages/astro/test/fixtures/slots-solid/src/pages/markdown.md
@@ -0,0 +1,9 @@
+---
+setup: import Counter from '../components/Counter.jsx'
+---
+
+# Slots: Solid
+
+<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
+<Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
+<Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>
diff --git a/packages/astro/test/fixtures/slots-svelte/src/components/Counter.svelte b/packages/astro/test/fixtures/slots-svelte/src/components/Counter.svelte
index 11901049e..24f4e734e 100644
--- a/packages/astro/test/fixtures/slots-svelte/src/components/Counter.svelte
+++ b/packages/astro/test/fixtures/slots-svelte/src/components/Counter.svelte
@@ -17,9 +17,7 @@
<button on:click={add}>+</button>
</div>
<div id={id}>
- <slot>
- <h1 id="fallback">Fallback</h1>
- </slot>
+ <slot><h1 id="fallback">Fallback</h1></slot><slot name="named" /><slot name="dash-case" />
</div>
<style>
diff --git a/packages/astro/test/fixtures/slots-svelte/src/pages/index.astro b/packages/astro/test/fixtures/slots-svelte/src/pages/index.astro
index bc25c17c7..72b0bc107 100644
--- a/packages/astro/test/fixtures/slots-svelte/src/pages/index.astro
+++ b/packages/astro/test/fixtures/slots-svelte/src/pages/index.astro
@@ -4,8 +4,10 @@ import Counter from '../components/Counter.svelte'
<main>
<Counter id="default-self-closing" client:visible/>
<Counter id="default-empty" client:visible></Counter>
- <Counter case="zero" client:visible>{0}</Counter>
- <Counter case="false" client:visible>{false}</Counter>
- <Counter case="string" client:visible>{''}</Counter>
+ <Counter id="zero" client:visible>{0}</Counter>
+ <Counter id="false" client:visible>{false}</Counter>
+ <Counter id="string" client:visible>{''}</Counter>
<Counter id="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
+ <Counter id="named" client:visible><h1 slot="named"> / Named</h1></Counter>
+ <Counter id="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>
</main>
diff --git a/packages/astro/test/fixtures/slots-svelte/src/pages/markdown.md b/packages/astro/test/fixtures/slots-svelte/src/pages/markdown.md
new file mode 100644
index 000000000..e5e415921
--- /dev/null
+++ b/packages/astro/test/fixtures/slots-svelte/src/pages/markdown.md
@@ -0,0 +1,9 @@
+---
+setup: import Counter from '../components/Counter.svelte'
+---
+
+# Slots: Svelte
+
+<Counter id="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
+<Counter id="named" client:visible><h1 slot="named"> / Named</h1></Counter>
+<Counter id="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>
diff --git a/packages/astro/test/fixtures/slots-vue/src/components/Counter.vue b/packages/astro/test/fixtures/slots-vue/src/components/Counter.vue
index 304511560..cd7ce75fe 100644
--- a/packages/astro/test/fixtures/slots-vue/src/components/Counter.vue
+++ b/packages/astro/test/fixtures/slots-vue/src/components/Counter.vue
@@ -8,6 +8,8 @@
<slot>
<h1>Fallback</h1>
</slot>
+ <slot name="named" />
+ <slot name="dash-case"></slot>
</div>
</template>
diff --git a/packages/astro/test/fixtures/slots-vue/src/pages/index.astro b/packages/astro/test/fixtures/slots-vue/src/pages/index.astro
index 65ca26726..ffc272267 100644
--- a/packages/astro/test/fixtures/slots-vue/src/pages/index.astro
+++ b/packages/astro/test/fixtures/slots-vue/src/pages/index.astro
@@ -8,4 +8,6 @@ import Counter from '../components/Counter.vue'
<Counter case="false" client:visible>{false}</Counter>
<Counter case="string" client:visible>{''}</Counter>
<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
+ <Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
+ <Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>
</main>
diff --git a/packages/astro/test/fixtures/slots-vue/src/pages/markdown.md b/packages/astro/test/fixtures/slots-vue/src/pages/markdown.md
new file mode 100644
index 000000000..6a08515f2
--- /dev/null
+++ b/packages/astro/test/fixtures/slots-vue/src/pages/markdown.md
@@ -0,0 +1,9 @@
+---
+setup: import Counter from '../components/Counter.vue'
+---
+
+# Slots: Vue
+
+<Counter case="content" client:visible><h1 id="slotted">Hello world!</h1></Counter>
+<Counter case="named" client:visible><h1 slot="named"> / Named</h1></Counter>
+<Counter case="dash-case" client:visible><h1 slot="dash-case"> / Dash Case</h1></Counter>
diff --git a/packages/astro/test/lit-element.test.js b/packages/astro/test/lit-element.test.js
index b9d5f53c2..350248ab6 100644
--- a/packages/astro/test/lit-element.test.js
+++ b/packages/astro/test/lit-element.test.js
@@ -61,4 +61,23 @@ describe('LitElement test', function () {
expect($('my-element').attr('reflected-str')).to.equal('default reflected string');
expect($('my-element').attr('reflected-str-prop')).to.equal('initialized reflected');
});
+
+ it('Correctly passes child slots', async () => {
+ // @lit-labs/ssr/ requires Node 13.9 or higher
+ if (NODE_VERSION < 13.9) {
+ return;
+ }
+ const html = await fixture.readFile('/slots/index.html');
+ const $ = cheerio.load(html);
+
+ expect($('my-element').length).to.equal(1);
+
+ const [defaultSlot, namedSlot] = $('template').siblings().toArray();
+
+ // has default slot content in lightdom
+ expect($(defaultSlot).text()).to.equal('default');
+
+ // has named slot content in lightdom
+ expect($(namedSlot).text()).to.equal('named');
+ });
});
diff --git a/packages/astro/test/slots-preact.test.js b/packages/astro/test/slots-preact.test.js
index c86a25fb7..b655638b2 100644
--- a/packages/astro/test/slots-preact.test.js
+++ b/packages/astro/test/slots-preact.test.js
@@ -21,4 +21,36 @@ describe('Slots: Preact', () => {
expect($('#string').text().trim()).to.equal('');
expect($('#content').text().trim()).to.equal('Hello world!');
});
+
+ it('Renders named slot', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('#named').text().trim()).to.equal('Fallback / Named');
+ })
+
+ it('Converts dash-case slot to camelCase', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
+ })
+
+ describe('For Markdown Pages', () => {
+ it('Renders default slot', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#content').text().trim()).to.equal('Hello world!');
+ });
+
+ it('Renders named slot', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#named').text().trim()).to.equal('Fallback / Named');
+ })
+
+ it('Converts dash-case slot to camelCase', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
+ })
+ })
});
diff --git a/packages/astro/test/slots-react.test.js b/packages/astro/test/slots-react.test.js
index da6953142..e84b02519 100644
--- a/packages/astro/test/slots-react.test.js
+++ b/packages/astro/test/slots-react.test.js
@@ -21,4 +21,36 @@ describe('Slots: React', () => {
expect($('#string').text().trim()).to.equal('');
expect($('#content').text().trim()).to.equal('Hello world!');
});
+
+ it('Renders named slot', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('#named').text().trim()).to.equal('Fallback / Named');
+ })
+
+ it('Converts dash-case slot to camelCase', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
+ })
+
+ describe('For Markdown Pages', () => {
+ it('Renders default slot', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#content').text().trim()).to.equal('Hello world!');
+ });
+
+ it('Renders named slot', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#named').text().trim()).to.equal('Fallback / Named');
+ })
+
+ it('Converts dash-case slot to camelCase', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
+ })
+ })
});
diff --git a/packages/astro/test/slots-solid.test.js b/packages/astro/test/slots-solid.test.js
index d7659f033..faff85efc 100644
--- a/packages/astro/test/slots-solid.test.js
+++ b/packages/astro/test/slots-solid.test.js
@@ -21,4 +21,36 @@ describe('Slots: Solid', () => {
expect($('#string').text().trim()).to.equal('');
expect($('#content').text().trim()).to.equal('Hello world!');
});
+
+ it('Renders named slot', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('#named').text().trim()).to.equal('Fallback / Named');
+ })
+
+ it('Converts dash-case slot to camelCase', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
+ })
+
+ describe('For Markdown Pages', () => {
+ it('Renders default slot', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#content').text().trim()).to.equal('Hello world!');
+ });
+
+ it('Renders named slot', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#named').text().trim()).to.equal('Fallback / Named');
+ })
+
+ it('Converts dash-case slot to camelCase', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
+ })
+ })
});
diff --git a/packages/astro/test/slots-svelte.test.js b/packages/astro/test/slots-svelte.test.js
index 0bbbae25a..9beab3281 100644
--- a/packages/astro/test/slots-svelte.test.js
+++ b/packages/astro/test/slots-svelte.test.js
@@ -16,9 +16,41 @@ describe('Slots: Svelte', () => {
expect($('#default-self-closing').text().trim()).to.equal('Fallback');
expect($('#default-empty').text().trim()).to.equal('Fallback');
- expect($('#zero').text().trim()).to.equal('');
+ expect($('#zero').text().trim()).to.equal('0');
expect($('#false').text().trim()).to.equal('');
expect($('#string').text().trim()).to.equal('');
expect($('#content').text().trim()).to.equal('Hello world!');
});
+
+ it('Renders named slot', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('#named').text().trim()).to.equal('Fallback / Named');
+ })
+
+ it('Preserves dash-case slot', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
+ })
+
+ describe('For Markdown Pages', () => {
+ it('Renders default slot', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#content').text().trim()).to.equal('Hello world!');
+ });
+
+ it('Renders named slot', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#named').text().trim()).to.equal('Fallback / Named');
+ })
+
+ it('Converts dash-case slot to camelCase', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
+ })
+ })
});
diff --git a/packages/astro/test/slots-vue.test.js b/packages/astro/test/slots-vue.test.js
index 2b0dbc743..dd2b612fe 100644
--- a/packages/astro/test/slots-vue.test.js
+++ b/packages/astro/test/slots-vue.test.js
@@ -21,4 +21,36 @@ describe('Slots: Vue', () => {
expect($('#string').text().trim()).to.equal('');
expect($('#content').text().trim()).to.equal('Hello world!');
});
+
+ it('Renders named slot', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('#named').text().trim()).to.equal('Fallback / Named');
+ })
+
+ it('Preserves dash-case slot', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+ expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
+ })
+
+ describe('For Markdown Pages', () => {
+ it('Renders default slot', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#content').text().trim()).to.equal('Hello world!');
+ });
+
+ it('Renders named slot', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#named').text().trim()).to.equal('Fallback / Named');
+ })
+
+ it('Converts dash-case slot to camelCase', async () => {
+ const html = await fixture.readFile('/markdown/index.html');
+ const $ = cheerio.load(html);
+ expect($('#dash-case').text().trim()).to.equal('Fallback / Dash Case');
+ })
+ })
});
diff --git a/packages/integrations/lit/server.js b/packages/integrations/lit/server.js
index bc8995061..2f9076672 100644
--- a/packages/integrations/lit/server.js
+++ b/packages/integrations/lit/server.js
@@ -26,7 +26,7 @@ async function check(Component, _props, _children) {
return !!(await isLitElement(Component));
}
-function* render(Component, attrs, children) {
+function* render(Component, attrs, slots) {
let tagName = Component;
if (typeof tagName !== 'string') {
tagName = Component[Symbol.for('tagName')];
@@ -57,15 +57,23 @@ function* render(Component, attrs, children) {
yield* shadowContents;
yield '</template>';
}
- yield children || ''; // don’t print “undefined” as string
+ if (slots) {
+ for (const [slot, value] of Object.entries(slots)) {
+ if (slot === 'default') {
+ yield `<astro-slot>${value || ''}</astro-slot>`;
+ } else {
+ yield `<astro-slot slot="${slot}">${value || ''}</astro-slot>`;
+ }
+ }
+ }
yield `</${tagName}>`;
}
-async function renderToStaticMarkup(Component, props, children) {
+async function renderToStaticMarkup(Component, props, slots) {
let tagName = Component;
let out = '';
- for (let chunk of render(tagName, props, children)) {
+ for (let chunk of render(tagName, props, slots)) {
out += chunk;
}
diff --git a/packages/integrations/preact/client.js b/packages/integrations/preact/client.js
index 12c5666df..e2f4ca803 100644
--- a/packages/integrations/preact/client.js
+++ b/packages/integrations/preact/client.js
@@ -1,8 +1,11 @@
import { h, render } from 'preact';
import StaticHtml from './static-html.js';
-export default (element) => (Component, props, children) => {
+export default (element) => (Component, props, { default: children, ...slotted }) => {
if (!element.hasAttribute('ssr')) return;
+ for (const [key, value] of Object.entries(slotted)) {
+ props[key] = h(StaticHtml, { value, name: key });
+ }
render(
h(Component, props, children != null ? h(StaticHtml, { value: children }) : children),
element
diff --git a/packages/integrations/preact/server.js b/packages/integrations/preact/server.js
index 0729f42e9..31f980aa3 100644
--- a/packages/integrations/preact/server.js
+++ b/packages/integrations/preact/server.js
@@ -2,6 +2,8 @@ import { h, Component as BaseComponent } from 'preact';
import render from 'preact-render-to-string';
import StaticHtml from './static-html.js';
+const slotName = str => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
+
function check(Component, props, children) {
if (typeof Component !== 'function') return false;
@@ -24,9 +26,16 @@ function check(Component, props, children) {
}
}
-function renderToStaticMarkup(Component, props, children) {
+function renderToStaticMarkup(Component, props, { default: children, ...slotted }) {
+ const slots = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ const name = slotName(key);
+ slots[name] = h(StaticHtml, { value, name });
+ }
+ // Note: create newProps to avoid mutating `props` before they are serialized
+ const newProps = { ...props, ...slots }
const html = render(
- h(Component, props, children != null ? h(StaticHtml, { value: children }) : children)
+ h(Component, newProps, children != null ? h(StaticHtml, { value: children }) : children)
);
return { html };
}
diff --git a/packages/integrations/preact/static-html.js b/packages/integrations/preact/static-html.js
index 9af8002a7..7e964ef06 100644
--- a/packages/integrations/preact/static-html.js
+++ b/packages/integrations/preact/static-html.js
@@ -7,9 +7,9 @@ import { h } from 'preact';
* As a bonus, we can signal to Preact that this subtree is
* entirely static and will never change via `shouldComponentUpdate`.
*/
-const StaticHtml = ({ value }) => {
+const StaticHtml = ({ value, name }) => {
if (!value) return null;
- return h('astro-fragment', { dangerouslySetInnerHTML: { __html: value } });
+ return h('astro-slot', { name, dangerouslySetInnerHTML: { __html: value } });
};
/**
diff --git a/packages/integrations/react/client-v17.js b/packages/integrations/react/client-v17.js
index 1dce34709..443109603 100644
--- a/packages/integrations/react/client-v17.js
+++ b/packages/integrations/react/client-v17.js
@@ -3,7 +3,10 @@ import { render, hydrate } from 'react-dom';
import StaticHtml from './static-html.js';
export default (element) =>
- (Component, props, children, { client }) => {
+ (Component, props, { default: children, ...slotted }, { client }) => {
+ for (const [key, value] of Object.entries(slotted)) {
+ props[key] = createElement(StaticHtml, { value, name: key });
+ }
const componentEl = createElement(
Component,
props,
diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js
index 2828d2cbe..b41d7845a 100644
--- a/packages/integrations/react/client.js
+++ b/packages/integrations/react/client.js
@@ -11,8 +11,11 @@ function isAlreadyHydrated(element) {
}
export default (element) =>
- (Component, props, children, { client }) => {
+ (Component, props, { default: children, ...slotted }, { client }) => {
if (!element.hasAttribute('ssr')) return;
+ for (const [key, value] of Object.entries(slotted)) {
+ props[key] = createElement(StaticHtml, { value, name: key });
+ }
const componentEl = createElement(
Component,
props,
diff --git a/packages/integrations/react/server-v17.js b/packages/integrations/react/server-v17.js
index b48d7b6f4..5d747a832 100644
--- a/packages/integrations/react/server-v17.js
+++ b/packages/integrations/react/server-v17.js
@@ -2,6 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom/server.js';
import StaticHtml from './static-html.js';
+const slotName = str => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
const reactTypeof = Symbol.for('react.element');
function errorIsComingFromPreactComponent(err) {
@@ -50,12 +51,20 @@ function check(Component, props, children) {
return isReactComponent;
}
-function renderToStaticMarkup(Component, props, children, metadata) {
+function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) {
delete props['class'];
- const vnode = React.createElement(Component, {
+ const slots = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ const name = slotName(key);
+ slots[name] = React.createElement(StaticHtml, { value, name });
+ }
+ // Note: create newProps to avoid mutating `props` before they are serialized
+ const newProps = {
...props,
+ ...slots,
children: children != null ? React.createElement(StaticHtml, { value: children }) : undefined,
- });
+ }
+ const vnode = React.createElement(Component, newProps);
let html;
if (metadata && metadata.hydrate) {
html = ReactDOM.renderToString(vnode);
diff --git a/packages/integrations/react/server.js b/packages/integrations/react/server.js
index 776901563..cda839a1c 100644
--- a/packages/integrations/react/server.js
+++ b/packages/integrations/react/server.js
@@ -2,6 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom/server';
import StaticHtml from './static-html.js';
+const slotName = str => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
const reactTypeof = Symbol.for('react.element');
function errorIsComingFromPreactComponent(err) {
@@ -56,12 +57,20 @@ async function getNodeWritable() {
return Writable;
}
-async function renderToStaticMarkup(Component, props, children, metadata) {
+async function renderToStaticMarkup(Component, props, { default: children, ...slotted }, metadata) {
delete props['class'];
- const vnode = React.createElement(Component, {
+ const slots = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ const name = slotName(key);
+ slots[name] = React.createElement(StaticHtml, { value, name });
+ }
+ // Note: create newProps to avoid mutating `props` before they are serialized
+ const newProps = {
...props,
+ ...slots,
children: children != null ? React.createElement(StaticHtml, { value: children }) : undefined,
- });
+ }
+ const vnode = React.createElement(Component, newProps);
let html;
if (metadata && metadata.hydrate) {
html = ReactDOM.renderToString(vnode);
diff --git a/packages/integrations/react/static-html.js b/packages/integrations/react/static-html.js
index ecd76ae9b..9589aaed8 100644
--- a/packages/integrations/react/static-html.js
+++ b/packages/integrations/react/static-html.js
@@ -7,9 +7,10 @@ import { createElement as h } from 'react';
* As a bonus, we can signal to React that this subtree is
* entirely static and will never change via `shouldComponentUpdate`.
*/
-const StaticHtml = ({ value }) => {
+const StaticHtml = ({ value, name }) => {
if (!value) return null;
- return h('astro-fragment', {
+ return h('astro-slot', {
+ name,
suppressHydrationWarning: true,
dangerouslySetInnerHTML: { __html: value },
});
diff --git a/packages/integrations/solid/client.js b/packages/integrations/solid/client.js
index 867d951c6..ceb7546d2 100644
--- a/packages/integrations/solid/client.js
+++ b/packages/integrations/solid/client.js
@@ -2,7 +2,7 @@ import { sharedConfig } from 'solid-js';
import { hydrate, render, createComponent } from 'solid-js/web';
export default (element) =>
- (Component, props, childHTML, { client }) => {
+ (Component, props, slotted, { client }) => {
// Prepare global object expected by Solid's hydration logic
if (!window._$HY) {
window._$HY = { events: [], completed: new WeakSet(), r: {} };
@@ -11,26 +11,30 @@ export default (element) =>
const fn = client === 'only' ? render : hydrate;
- // Perform actual hydration
- let children;
+ let _slots = {};
+ if (Object.keys(slotted).length > 0) {
+ // hydrating
+ if (sharedConfig.context) {
+ element.querySelectorAll('astro-slot').forEach((slot) => {
+ _slots[slot.getAttribute('name') || 'default'] = slot.cloneNode(true);
+ });
+ } else {
+ for (const [key, value] of Object.entries(slotted)) {
+ _slots[key] = document.createElement('astro-slot');
+ if (key !== 'default') _slots[key].setAttribute('name', key);
+ _slots[key].innerHTML = value;
+ }
+ }
+ }
+
+ const { default: children, ...slots } = _slots;
+
fn(
() =>
createComponent(Component, {
...props,
- get children() {
- if (childHTML != null) {
- // hydrating
- if (sharedConfig.context) {
- children = element.querySelector('astro-fragment');
- }
-
- if (children == null) {
- children = document.createElement('astro-fragment');
- children.innerHTML = childHTML;
- }
- }
- return children;
- },
+ ...slots,
+ children
}),
element
);
diff --git a/packages/integrations/solid/server.js b/packages/integrations/solid/server.js
index dc4f88227..92b614012 100644
--- a/packages/integrations/solid/server.js
+++ b/packages/integrations/solid/server.js
@@ -1,23 +1,28 @@
import { renderToString, ssr, createComponent } from 'solid-js/web';
+const slotName = str => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
+
function check(Component, props, children) {
if (typeof Component !== 'function') return false;
const { html } = renderToStaticMarkup(Component, props, children);
return typeof html === 'string';
}
-function renderToStaticMarkup(Component, props, children) {
- const html = renderToString(() =>
- createComponent(Component, {
- ...props,
- // In Solid SSR mode, `ssr` creates the expected structure for `children`.
- // In Solid client mode, `ssr` is just a stub.
- children: children != null ? ssr(`<astro-fragment>${children}</astro-fragment>`) : children,
- })
- );
- return {
- html: html,
- };
+function renderToStaticMarkup(Component, props, { default: children, ...slotted }) {
+ const slots = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ const name = slotName(key);
+ slots[name] = ssr(`<astro-slot name="${name}">${value}</astro-slot>`);
+ }
+ // Note: create newProps to avoid mutating `props` before they are serialized
+ const newProps = {
+ ...props,
+ ...slots,
+ // In Solid SSR mode, `ssr` creates the expected structure for `children`.
+ children: children != null ? ssr(`<astro-slot>${children}</astro-slot>`) : children,
+ }
+ const html = renderToString(() => createComponent(Component, newProps));
+ return { html }
}
export default {
diff --git a/packages/integrations/solid/static-html.js b/packages/integrations/solid/static-html.js
deleted file mode 100644
index 9f969eac9..000000000
--- a/packages/integrations/solid/static-html.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { ssr } from 'solid-js/web';
-
-/**
- * Astro passes `children` as a string of HTML, so we need
- * a wrapper `astro-fragment` to render that content as VNodes.
- */
-const StaticHtml = ({ innerHTML }) => {
- if (!innerHTML) return null;
- return ssr(`<astro-fragment>${innerHTML}</astro-fragment>`);
-};
-
-export default StaticHtml;
diff --git a/packages/integrations/svelte/Wrapper.svelte b/packages/integrations/svelte/Wrapper.svelte
deleted file mode 100644
index c1ee77d91..000000000
--- a/packages/integrations/svelte/Wrapper.svelte
+++ /dev/null
@@ -1,21 +0,0 @@
-<script>
-/**
- * Why do we need a wrapper component?
- *
- * Astro passes `children` as a string of HTML, so we need
- * a way to render that content.
- *
- * Rather than passing a magical prop which needs special
- * handling, using this wrapper allows Svelte users to just
- * use `<slot />` like they would for any other component.
- */
-const { __astro_component: Component, __astro_children, ...props } = $$props;
-</script>
-
-<svelte:component this={Component} {...props}>
- {#if __astro_children != null}
- <astro-fragment>
- {@html __astro_children}
- </astro-fragment>
- {/if}
-</svelte:component>
diff --git a/packages/integrations/svelte/Wrapper.svelte.ssr.js b/packages/integrations/svelte/Wrapper.svelte.ssr.js
deleted file mode 100644
index e6a4781a7..000000000
--- a/packages/integrations/svelte/Wrapper.svelte.ssr.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/* App.svelte generated by Svelte v3.38.2 */
-import { create_ssr_component, missing_component, validate_component } from 'svelte/internal';
-
-const App = create_ssr_component(($$result, $$props, $$bindings, slots) => {
- const { __astro_component: Component, __astro_children, ...props } = $$props;
- const children = {};
- if (__astro_children != null) {
- children.default = () => `<astro-fragment>${__astro_children}</astro-fragment>`;
- }
-
- return `${validate_component(Component || missing_component, 'svelte:component').$$render(
- $$result,
- Object.assign(props),
- {},
- children
- )}`;
-});
-
-export default App;
diff --git a/packages/integrations/svelte/client.js b/packages/integrations/svelte/client.js
index 3f401b544..36a8e8de1 100644
--- a/packages/integrations/svelte/client.js
+++ b/packages/integrations/svelte/client.js
@@ -1,15 +1,43 @@
-import SvelteWrapper from './Wrapper.svelte';
+const noop = () => {};
export default (target) => {
- return (component, props, children, { client }) => {
+ return (Component, props, slotted, { client }) => {
if (!target.hasAttribute('ssr')) return;
delete props['class'];
+ const slots = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ slots[key] = createSlotDefinition(key, value);
+ }
try {
- new SvelteWrapper({
+ new Component({
target,
- props: { __astro_component: component, __astro_children: children, ...props },
+ props: {
+ ...props,
+ $$slots: slots,
+ $$scope: { ctx: [] }
+ },
hydrate: client !== 'only',
+ $$inline: true,
});
} catch (e) {}
};
};
+
+function createSlotDefinition(key, children) {
+ return [
+ () => ({
+ // mount
+ m(target) {
+ target.insertAdjacentHTML('beforeend', `<astro-slot${key === 'default' ? '' : ` name="${key}"`}>${children}</astro-slot>`)
+ },
+ // create
+ c: noop,
+ // hydrate
+ l: noop,
+ // destroy
+ d: noop,
+ }),
+ noop,
+ noop,
+ ]
+}
diff --git a/packages/integrations/svelte/server.js b/packages/integrations/svelte/server.js
index 3c989cd5a..7a5610b4a 100644
--- a/packages/integrations/svelte/server.js
+++ b/packages/integrations/svelte/server.js
@@ -1,15 +1,13 @@
-import SvelteWrapper from './Wrapper.svelte.ssr.js';
-
function check(Component) {
return Component['render'] && Component['$$render'];
}
-async function renderToStaticMarkup(Component, props, children) {
- const { html } = SvelteWrapper.render({
- __astro_component: Component,
- __astro_children: children,
- ...props,
- });
+async function renderToStaticMarkup(Component, props, slotted) {
+ const slots = {};
+ for (const [key, value] of Object.entries(slotted)) {
+ slots[key] = () => `<astro-slot${key === 'default' ? '' : ` name="${key}"`}>${value}</astro-slot>`;
+ }
+ const { html } = Component.render(props, { $$slots: slots });
return { html };
}
diff --git a/packages/integrations/vue/client.js b/packages/integrations/vue/client.js
index c6206fe51..648a69658 100644
--- a/packages/integrations/vue/client.js
+++ b/packages/integrations/vue/client.js
@@ -2,15 +2,15 @@ import { h, createSSRApp, createApp } from 'vue';
import StaticHtml from './static-html.js';
export default (element) =>
- (Component, props, children, { client }) => {
+ (Component, props, slotted, { client }) => {
delete props['class'];
if (!element.hasAttribute('ssr')) return;
// Expose name on host component for Vue devtools
const name = Component.name ? `${Component.name} Host` : undefined;
const slots = {};
- if (children != null) {
- slots.default = () => h(StaticHtml, { value: children });
+ for (const [key, value] of Object.entries(slotted)) {
+ slots[key] = () => h(StaticHtml, { value, name: key === 'default' ? undefined : key });
}
if (client === 'only') {
const app = createApp({ name, render: () => h(Component, props, slots) });
diff --git a/packages/integrations/vue/server.js b/packages/integrations/vue/server.js
index 1ae2b757b..883aa4de0 100644
--- a/packages/integrations/vue/server.js
+++ b/packages/integrations/vue/server.js
@@ -6,10 +6,10 @@ function check(Component) {
return !!Component['ssrRender'];
}
-async function renderToStaticMarkup(Component, props, children) {
+async function renderToStaticMarkup(Component, props, slotted) {
const slots = {};
- if (children != null) {
- slots.default = () => h(StaticHtml, { value: children });
+ for (const [key, value] of Object.entries(slotted)) {
+ slots[key] = () => h(StaticHtml, { value, name: key === 'default' ? undefined : key });
}
const app = createSSRApp({ render: () => h(Component, props, slots) });
const html = await renderToString(app);
diff --git a/packages/integrations/vue/static-html.js b/packages/integrations/vue/static-html.js
index ff1459b6f..a7f09eace 100644
--- a/packages/integrations/vue/static-html.js
+++ b/packages/integrations/vue/static-html.js
@@ -9,10 +9,11 @@ import { h, defineComponent } from 'vue';
const StaticHtml = defineComponent({
props: {
value: String,
+ name: String,
},
- setup({ value }) {
+ setup({ name, value }) {
if (!value) return () => null;
- return () => h('astro-fragment', { innerHTML: value });
+ return () => h('astro-slot', { name, innerHTML: value });
},
});
diff --git a/packages/webapi/mod.d.ts b/packages/webapi/mod.d.ts
index a3c49dc5c..b385e82a5 100644
--- a/packages/webapi/mod.d.ts
+++ b/packages/webapi/mod.d.ts
@@ -1,5 +1,5 @@
export { pathToPosix } from './lib/utils';
-export { AbortController, AbortSignal, alert, atob, Blob, btoa, ByteLengthQueuingStrategy, cancelAnimationFrame, cancelIdleCallback, CanvasRenderingContext2D, CharacterData, clearTimeout, Comment, CountQueuingStrategy, CSSStyleSheet, CustomElementRegistry, CustomEvent, Document, DocumentFragment, DOMException, Element, Event, EventTarget, fetch, File, FormData, Headers, HTMLBodyElement, HTMLCanvasElement, HTMLDivElement, HTMLDocument, HTMLElement, HTMLHeadElement, HTMLHtmlElement, HTMLImageElement, HTMLSpanElement, HTMLStyleElement, HTMLTemplateElement, HTMLUnknownElement, Image, ImageData, IntersectionObserver, MediaQueryList, MutationObserver, Node, NodeFilter, NodeIterator, OffscreenCanvas, ReadableByteStreamController, ReadableStream, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, Request, requestAnimationFrame, requestIdleCallback, ResizeObserver, Response, setTimeout, ShadowRoot, structuredClone, StyleSheet, Text, TransformStream, TreeWalker, URLPattern, Window, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter } from './mod.js';
+export { AbortController, AbortSignal, alert, atob, Blob, btoa, ByteLengthQueuingStrategy, cancelAnimationFrame, cancelIdleCallback, CanvasRenderingContext2D, CharacterData, clearTimeout, Comment, CountQueuingStrategy, CSSStyleSheet, CustomElementRegistry, CustomEvent, Document, DocumentFragment, DOMException, Element, Event, EventTarget, fetch, File, FormData, Headers, HTMLBodyElement, HTMLCanvasElement, HTMLDivElement, HTMLDocument, HTMLElement, HTMLHeadElement, HTMLHtmlElement, HTMLImageElement, HTMLSpanElement, HTMLStyleElement, HTMLTemplateElement, HTMLUnknownElement, Image, ImageData, IntersectionObserver, MediaQueryList, MutationObserver, Node, NodeFilter, NodeIterator, OffscreenCanvas, ReadableByteStreamController, ReadableStream, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, Request, requestAnimationFrame, requestIdleCallback, ResizeObserver, Response, setTimeout, ShadowRoot, structuredClone, StyleSheet, Text, TransformStream, TreeWalker, URLPattern, Window, WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter, } from './mod.js';
export declare const polyfill: {
(target: any, options?: PolyfillOptions): any;
internals(target: any, name: string): any;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 520b204a4..65317d88a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -888,6 +888,35 @@ importers:
'@astrojs/vue': link:../../../../integrations/vue
astro: link:../../..
+ packages/astro/e2e/fixtures/nested-recursive:
+ specifiers:
+ '@astrojs/preact': workspace:*
+ '@astrojs/react': workspace:*
+ '@astrojs/solid-js': workspace:*
+ '@astrojs/svelte': workspace:*
+ '@astrojs/vue': workspace:*
+ astro: workspace:*
+ preact: ^10.7.3
+ react: ^18.1.0
+ react-dom: ^18.1.0
+ solid-js: ^1.4.3
+ svelte: ^3.48.0
+ vue: ^3.2.36
+ dependencies:
+ preact: 10.7.3
+ react: 18.1.0
+ react-dom: 18.1.0_react@18.1.0
+ solid-js: 1.4.3
+ svelte: 3.48.0
+ vue: 3.2.37
+ devDependencies:
+ '@astrojs/preact': link:../../../../integrations/preact
+ '@astrojs/react': link:../../../../integrations/react
+ '@astrojs/solid-js': link:../../../../integrations/solid
+ '@astrojs/svelte': link:../../../../integrations/svelte
+ '@astrojs/vue': link:../../../../integrations/vue
+ astro: link:../../..
+
packages/astro/e2e/fixtures/nested-styles:
specifiers:
astro: workspace:*