summaryrefslogtreecommitdiff
path: root/packages/integrations
diff options
context:
space:
mode:
authorGravatar Nate Moore <natemoo-re@users.noreply.github.com> 2022-06-23 10:10:54 -0500
committerGravatar GitHub <noreply@github.com> 2022-06-23 10:10:54 -0500
commit7373d61cdcaedd64bf5fd60521b157cfa4343558 (patch)
treedb7ba617722a58e4b1b6437f1fcabd7f894fd8b1 /packages/integrations
parent19cd962d0b3433ee305d1d277ca4fc3b93593558 (diff)
downloadastro-7373d61cdcaedd64bf5fd60521b157cfa4343558.tar.gz
astro-7373d61cdcaedd64bf5fd60521b157cfa4343558.tar.zst
astro-7373d61cdcaedd64bf5fd60521b157cfa4343558.zip
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>
Diffstat (limited to 'packages/integrations')
-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
19 files changed, 149 insertions, 120 deletions
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 });
},
});