Enable named slots in renderers (#3652)
* feat: pass all slots to renderers * refactor: pass `slots` as top-level props * test: add named slot test for frameworks * fix: nested hydration, slots that are not initially rendered * test: add nested-recursive e2e test * fix: render unmatched custom element children * chore: update lockfile * fix: unrendered slots for client:only * fix(lit): ensure lit integration uses new slots API * chore: add changeset * chore: add changesets * fix: lit slots * feat: convert dash-case or snake_case slots to camelCase for JSX * feat: remove tmpl special logic * test: add slot components-in-markdown test * refactor: prefer Object.entries.map() to for/of loop Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
parent
19cd962d0b
commit
7373d61cdc
60 changed files with 826 additions and 156 deletions
7
.changeset/clever-pumpkins-begin.md
Normal file
7
.changeset/clever-pumpkins-begin.md
Normal file
|
@ -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.
|
29
.changeset/lovely-bulldogs-admire.md
Normal file
29
.changeset/lovely-bulldogs-admire.md
Normal file
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
7
.changeset/mean-ears-mate.md
Normal file
7
.changeset/mean-ears-mate.md
Normal file
|
@ -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.
|
8
.changeset/tough-ants-rest.md
Normal file
8
.changeset/tough-ants-rest.md
Normal file
|
@ -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.
|
|
@ -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()],
|
||||
});
|
24
packages/astro/e2e/fixtures/nested-recursive/package.json
Normal file
24
packages/astro/e2e/fixtures/nested-recursive/package.json
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
96
packages/astro/e2e/nested-recursive.test.js
Normal file
96
packages/astro/e2e/nested-recursive.test.js
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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; }',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
15
packages/astro/test/fixtures/lit-element/src/pages/slots.astro
vendored
Normal file
15
packages/astro/test/fixtures/lit-element/src/pages/slots.astro
vendored
Normal file
|
@ -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>
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
9
packages/astro/test/fixtures/slots-preact/src/pages/markdown.md
vendored
Normal file
9
packages/astro/test/fixtures/slots-preact/src/pages/markdown.md
vendored
Normal file
|
@ -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>
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
9
packages/astro/test/fixtures/slots-react/src/pages/markdown.md
vendored
Normal file
9
packages/astro/test/fixtures/slots-react/src/pages/markdown.md
vendored
Normal file
|
@ -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>
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
9
packages/astro/test/fixtures/slots-solid/src/pages/markdown.md
vendored
Normal file
9
packages/astro/test/fixtures/slots-solid/src/pages/markdown.md
vendored
Normal file
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
9
packages/astro/test/fixtures/slots-svelte/src/pages/markdown.md
vendored
Normal file
9
packages/astro/test/fixtures/slots-svelte/src/pages/markdown.md
vendored
Normal file
|
@ -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>
|
|
@ -8,6 +8,8 @@
|
|||
<slot>
|
||||
<h1>Fallback</h1>
|
||||
</slot>
|
||||
<slot name="named" />
|
||||
<slot name="dash-case"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
9
packages/astro/test/fixtures/slots-vue/src/pages/markdown.md
vendored
Normal file
9
packages/astro/test/fixtures/slots-vue/src/pages/markdown.md
vendored
Normal file
|
@ -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>
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
})
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
})
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
})
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
})
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
})
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
@ -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 } });
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
|
@ -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;
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
@ -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) });
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
2
packages/webapi/mod.d.ts
vendored
2
packages/webapi/mod.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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:*
|
||||
|
|
Loading…
Reference in a new issue