diff options
author | 2022-02-16 10:11:54 -0500 | |
---|---|---|
committer | 2022-02-16 10:11:54 -0500 | |
commit | 102161761de629fe1bfee7d151d4956c57ea2f42 (patch) | |
tree | dbfe0f25bef0e49fe489f3a3c30873c99269191f | |
parent | 19d548f400dc200ddd0e682520899a862c4d668a (diff) | |
download | astro-102161761de629fe1bfee7d151d4956c57ea2f42.tar.gz astro-102161761de629fe1bfee7d151d4956c57ea2f42.tar.zst astro-102161761de629fe1bfee7d151d4956c57ea2f42.zip |
Pass children to client components even if they do not render them (#2588)
* Pass children to client components even if they do not render them
* Handle when no children are provided
* Adds a changeset
* Use roots directly i guess
* Use an attribute to signal that the template is needed
-rw-r--r-- | .changeset/great-suns-pump.md | 5 | ||||
-rw-r--r-- | packages/astro/src/@types/astro.ts | 1 | ||||
-rw-r--r-- | packages/astro/src/runtime/client/idle.ts | 20 | ||||
-rw-r--r-- | packages/astro/src/runtime/client/load.ts | 22 | ||||
-rw-r--r-- | packages/astro/src/runtime/client/media.ts | 18 | ||||
-rw-r--r-- | packages/astro/src/runtime/client/only.ts | 20 | ||||
-rw-r--r-- | packages/astro/src/runtime/client/visible.ts | 20 | ||||
-rw-r--r-- | packages/astro/src/runtime/server/hydration.ts | 2 | ||||
-rw-r--r-- | packages/astro/src/runtime/server/index.ts | 5 | ||||
-rw-r--r-- | packages/astro/test/astro-children.test.js | 20 | ||||
-rw-r--r-- | packages/astro/test/fixtures/astro-children/src/components/NoRender.jsx | 5 | ||||
-rw-r--r-- | packages/astro/test/fixtures/astro-children/src/pages/no-render.astro | 22 |
12 files changed, 149 insertions, 11 deletions
diff --git a/.changeset/great-suns-pump.md b/.changeset/great-suns-pump.md new file mode 100644 index 000000000..0d4a2541e --- /dev/null +++ b/.changeset/great-suns-pump.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix for passing children to client component when the component does not render them diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index d93563352..c2708ab1e 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -213,6 +213,7 @@ export type GetStaticPathsResultKeyed = GetStaticPathsResult & { }; export interface HydrateOptions { + name: string; value?: string; } diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts index c3914cfbe..6a5bf15e1 100644 --- a/packages/astro/src/runtime/client/idle.ts +++ b/packages/astro/src/runtime/client/idle.ts @@ -4,10 +4,26 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; * Hydrate this component as soon as the main thread is free * (or after a short delay, if `requestIdleCallback`) isn't supported */ -export default async function onIdle(astroId: string, _options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { +export default async function onIdle(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { const cb = async () => { const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); - const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null; + if(roots.length === 0) { + throw new Error(`Unable to find the root for the component ${options.name}`); + } + + let innerHTML: string | null = null; + let fragment = roots[0].querySelector(`astro-fragment`); + if(fragment == null && roots[0].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 = roots[0].querySelector(`template[data-astro-template]`); + if(template) { + innerHTML = template.innerHTML; + template.remove(); + } + } else if(fragment) { + innerHTML = fragment.innerHTML; + } const hydrate = await getHydrateCallback(); for (const root of roots) { diff --git a/packages/astro/src/runtime/client/load.ts b/packages/astro/src/runtime/client/load.ts index c3fae489e..73bb441cf 100644 --- a/packages/astro/src/runtime/client/load.ts +++ b/packages/astro/src/runtime/client/load.ts @@ -3,9 +3,27 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; /** * Hydrate this component immediately */ -export default async function onLoad(astroId: string, _options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { +export default async function onLoad(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); - const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null; + if(roots.length === 0) { + throw new Error(`Unable to find the root for the component ${options.name}`); + } + + let innerHTML: string | null = null; + let fragment = roots[0].querySelector(`astro-fragment`); + if(fragment == null && roots[0].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 = roots[0].querySelector(`template[data-astro-template]`); + if(template) { + innerHTML = template.innerHTML; + template.remove(); + } + } else if(fragment) { + innerHTML = fragment.innerHTML; + } + + //const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null; const hydrate = await getHydrateCallback(); for (const root of roots) { diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts index ef2f65260..f5ae240c1 100644 --- a/packages/astro/src/runtime/client/media.ts +++ b/packages/astro/src/runtime/client/media.ts @@ -5,7 +5,23 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; */ export default async function onMedia(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); - const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null; + if(roots.length === 0) { + throw new Error(`Unable to find the root for the component ${options.name}`); + } + + let innerHTML: string | null = null; + let fragment = roots[0].querySelector(`astro-fragment`); + if(fragment == null && roots[0].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 = roots[0].querySelector(`template[data-astro-template]`); + if(template) { + innerHTML = template.innerHTML; + template.remove(); + } + } else if(fragment) { + innerHTML = fragment.innerHTML; + } const cb = async () => { const hydrate = await getHydrateCallback(); diff --git a/packages/astro/src/runtime/client/only.ts b/packages/astro/src/runtime/client/only.ts index c3fae489e..cc4efb99d 100644 --- a/packages/astro/src/runtime/client/only.ts +++ b/packages/astro/src/runtime/client/only.ts @@ -3,9 +3,25 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; /** * Hydrate this component immediately */ -export default async function onLoad(astroId: string, _options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { +export default async function onLoad(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); - const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null; + if(roots.length === 0) { + throw new Error(`Unable to find the root for the component ${options.name}`); + } + + let innerHTML: string | null = null; + let fragment = roots[0].querySelector(`astro-fragment`); + if(fragment == null && roots[0].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 = roots[0].querySelector(`template[data-astro-template]`); + if(template) { + innerHTML = template.innerHTML; + template.remove(); + } + } else if(fragment) { + innerHTML = fragment.innerHTML; + } const hydrate = await getHydrateCallback(); for (const root of roots) { diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts index e06aabab4..101551ec0 100644 --- a/packages/astro/src/runtime/client/visible.ts +++ b/packages/astro/src/runtime/client/visible.ts @@ -5,9 +5,25 @@ import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro'; * We target the children because `astro-root` is set to `display: contents` * which doesn't work with IntersectionObserver */ -export default async function onVisible(astroId: string, _options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { +export default async function onVisible(astroId: string, options: HydrateOptions, getHydrateCallback: GetHydrateCallback) { const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`); - const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null; + if(roots.length === 0) { + throw new Error(`Unable to find the root for the component ${options.name}`); + } + + let innerHTML: string | null = null; + let fragment = roots[0].querySelector(`astro-fragment`); + if(fragment == null && roots[0].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 = roots[0].querySelector(`template[data-astro-template]`); + if(template) { + innerHTML = template.innerHTML; + template.remove(); + } + } else if(fragment) { + innerHTML = fragment.innerHTML; + } const cb = async () => { const hydrate = await getHydrateCallback(); diff --git a/packages/astro/src/runtime/server/hydration.ts b/packages/astro/src/runtime/server/hydration.ts index b339b934f..2ffdc9144 100644 --- a/packages/astro/src/runtime/server/hydration.ts +++ b/packages/astro/src/runtime/server/hydration.ts @@ -116,7 +116,7 @@ export async function generateHydrateScript(scriptOptions: HydrateScriptOptions, const hydrationScript = { props: { type: 'module', 'data-astro-component-hydration': true }, children: `import setup from '${await result.resolve(hydrationSpecifier(hydrate))}'; -setup("${astroId}", {${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''}}, async () => { +setup("${astroId}", {name:"${metadata.displayName}",${metadata.hydrateArgs ? `value: ${JSON.stringify(metadata.hydrateArgs)}` : ''}}, async () => { ${hydrationSource} }); `, diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts index 56a6a2a49..2253e2b99 100644 --- a/packages/astro/src/runtime/server/index.ts +++ b/packages/astro/src/runtime/server/index.ts @@ -274,7 +274,10 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr // INVESTIGATE: This will likely be a problem in streaming because the `<head>` will be gone at this point. result.scripts.add(await generateHydrateScript({ renderer, result, astroId, props }, metadata as Required<AstroComponentMetadata>)); - return unescapeHTML(`<astro-root uid="${astroId}">${html ?? ''}</astro-root>`); + // Render a template if no fragment is provided. + const needsAstroTemplate = children && !/<\/?astro-fragment\>/.test(html); + const template = needsAstroTemplate ? `<template data-astro-template>${children}</template>` : ''; + return unescapeHTML(`<astro-root uid="${astroId}"${needsAstroTemplate ? ' tmpl' : ''}>${html ?? ''}${template}</astro-root>`); } /** Create the Astro.fetchContent() runtime function. */ diff --git a/packages/astro/test/astro-children.test.js b/packages/astro/test/astro-children.test.js index 99c19387c..3df65e2e6 100644 --- a/packages/astro/test/astro-children.test.js +++ b/packages/astro/test/astro-children.test.js @@ -69,4 +69,24 @@ describe('Component children', () => { expect($svelte.children(':first-child').text().trim()).to.equal('Hello world'); expect($svelte.children(':last-child').text().trim()).to.equal('Goodbye world'); }); + + it('Renders a template when children are not rendered for client components', async () => { + const html = await fixture.readFile('/no-render/index.html'); + const $ = cheerio.load(html); + + // test 1: If SSR only, no children are rendered. + expect($('#ssr-only').children()).to.have.lengthOf(0); + + // test 2: If client, and no children are rendered, a template is. + expect($('#client').parent().children()).to.have.lengthOf(2, 'rendered the client component and a template'); + expect($('#client').parent().find('template[data-astro-template]')).to.have.lengthOf(1, 'Found 1 template'); + + // test 3: If client, and children are rendered, no template is. + expect($('#client-render').parent().children()).to.have.lengthOf(1); + expect($('#client-render').parent().find('template')).to.have.lengthOf(0); + + // test 4: If client and no children are provided, no template is. + expect($('#client-no-children').parent().children()).to.have.lengthOf(1); + expect($('#client-no-children').parent().find('template')).to.have.lengthOf(0); + }); }); diff --git a/packages/astro/test/fixtures/astro-children/src/components/NoRender.jsx b/packages/astro/test/fixtures/astro-children/src/components/NoRender.jsx new file mode 100644 index 000000000..f3c41eb54 --- /dev/null +++ b/packages/astro/test/fixtures/astro-children/src/components/NoRender.jsx @@ -0,0 +1,5 @@ +import { h } from 'preact'; + +export default function PreactComponent({ id, children, render = false }) { + return <div id={id} class="preact-no-children">{render && children}</div>; +} diff --git a/packages/astro/test/fixtures/astro-children/src/pages/no-render.astro b/packages/astro/test/fixtures/astro-children/src/pages/no-render.astro new file mode 100644 index 000000000..9b2c3e867 --- /dev/null +++ b/packages/astro/test/fixtures/astro-children/src/pages/no-render.astro @@ -0,0 +1,22 @@ +--- +import PreactComponent from '../components/NoRender.jsx'; +--- +<html> +<head><title>Children</title></head> +<body> + <PreactComponent id="ssr-only"> + <h1>Hello world</h1> + <h1>Goodbye world</h1> + </PreactComponent> + <PreactComponent id="client" client:load> + <h1>Hello world</h1> + <h1>Goodbye world</h1> + </PreactComponent> + <PreactComponent id="client-render" render={true} client:load> + <h1>Hello world</h1> + <h1>Goodbye world</h1> + </PreactComponent> + + <PreactComponent id="client-no-children" client:load></PreactComponent> +</body> +</html> |