summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@skypack.dev> 2022-02-16 10:11:54 -0500
committerGravatar GitHub <noreply@github.com> 2022-02-16 10:11:54 -0500
commit102161761de629fe1bfee7d151d4956c57ea2f42 (patch)
treedbfe0f25bef0e49fe489f3a3c30873c99269191f
parent19d548f400dc200ddd0e682520899a862c4d668a (diff)
downloadastro-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.md5
-rw-r--r--packages/astro/src/@types/astro.ts1
-rw-r--r--packages/astro/src/runtime/client/idle.ts20
-rw-r--r--packages/astro/src/runtime/client/load.ts22
-rw-r--r--packages/astro/src/runtime/client/media.ts18
-rw-r--r--packages/astro/src/runtime/client/only.ts20
-rw-r--r--packages/astro/src/runtime/client/visible.ts20
-rw-r--r--packages/astro/src/runtime/server/hydration.ts2
-rw-r--r--packages/astro/src/runtime/server/index.ts5
-rw-r--r--packages/astro/test/astro-children.test.js20
-rw-r--r--packages/astro/test/fixtures/astro-children/src/components/NoRender.jsx5
-rw-r--r--packages/astro/test/fixtures/astro-children/src/pages/no-render.astro22
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>