diff options
author | 2022-06-23 10:10:54 -0500 | |
---|---|---|
committer | 2022-06-23 10:10:54 -0500 | |
commit | 7373d61cdcaedd64bf5fd60521b157cfa4343558 (patch) | |
tree | db7ba617722a58e4b1b6437f1fcabd7f894fd8b1 /packages/integrations | |
parent | 19cd962d0b3433ee305d1d277ca4fc3b93593558 (diff) | |
download | astro-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.js | 16 | ||||
-rw-r--r-- | packages/integrations/preact/client.js | 5 | ||||
-rw-r--r-- | packages/integrations/preact/server.js | 13 | ||||
-rw-r--r-- | packages/integrations/preact/static-html.js | 4 | ||||
-rw-r--r-- | packages/integrations/react/client-v17.js | 5 | ||||
-rw-r--r-- | packages/integrations/react/client.js | 5 | ||||
-rw-r--r-- | packages/integrations/react/server-v17.js | 15 | ||||
-rw-r--r-- | packages/integrations/react/server.js | 15 | ||||
-rw-r--r-- | packages/integrations/react/static-html.js | 5 | ||||
-rw-r--r-- | packages/integrations/solid/client.js | 38 | ||||
-rw-r--r-- | packages/integrations/solid/server.js | 29 | ||||
-rw-r--r-- | packages/integrations/solid/static-html.js | 12 | ||||
-rw-r--r-- | packages/integrations/svelte/Wrapper.svelte | 21 | ||||
-rw-r--r-- | packages/integrations/svelte/Wrapper.svelte.ssr.js | 19 | ||||
-rw-r--r-- | packages/integrations/svelte/client.js | 36 | ||||
-rw-r--r-- | packages/integrations/svelte/server.js | 14 | ||||
-rw-r--r-- | packages/integrations/vue/client.js | 6 | ||||
-rw-r--r-- | packages/integrations/vue/server.js | 6 | ||||
-rw-r--r-- | packages/integrations/vue/static-html.js | 5 |
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 }); }, }); |