From e0c36b574e2411ad69073734729f8460f37fe8fe Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 14:57:05 -0500 Subject: [PATCH 01/15] fix(view-transitions): update persistence logic for improved unmount behavior --- .../astro/components/ViewTransitions.astro | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro index 33741d535b4e..a1ce47759325 100644 --- a/packages/astro/components/ViewTransitions.astro +++ b/packages/astro/components/ViewTransitions.astro @@ -163,18 +163,20 @@ const { fallback = 'animate' } = Astro.props as Props; // Everything left in the new head is new, append it all. document.head.append(...doc.head.children); - // Move over persist stuff in the body + // Persist elements in the existing body const oldBody = document.body; - document.body.replaceWith(doc.body); for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) { const id = el.getAttribute(PERSIST_ATTR); - const newEl = document.querySelector(`[${PERSIST_ATTR}="${id}"]`); + const newEl = doc.querySelector(`[${PERSIST_ATTR}="${id}"]`); if (newEl) { // The element exists in the new page, replace it with the element // from the old page so that state is preserved. newEl.replaceWith(el); } } + // Only replace the existing body *AFTER* persistent elements are moved over + // This avoids disconnecting `astro-island` nodes multiple times + document.body.replaceWith(doc.body); // Simulate scroll behavior of Safari and // Chromium based browsers (Chrome, Edge, Opera, ...) @@ -220,15 +222,23 @@ const { fallback = 'animate' } = Astro.props as Props; links.length && (await Promise.all(links)); if (fallback === 'animate') { + let isAnimating = false; + addEventListener('animationstart', () => (isAnimating = true), { once: true }); + // Trigger the animations document.documentElement.dataset.astroTransitionFallback = 'old'; - const finished = Promise.all(document.getAnimations().map(a => a.finished)); const fallbackSwap = () => { + removeEventListener('animationend', fallbackSwap); + clearTimeout(timeout); swap(); document.documentElement.dataset.astroTransitionFallback = 'new'; }; - await finished; - fallbackSwap(); + // If there are any animations, want for the animationend event. + addEventListener('animationend', fallbackSwap, { once: true }); + // If there are no animations, go ahead and swap on next tick + // This is necessary because we do not know if there are animations. + // The setTimeout is a fallback in case there are none. + let timeout = setTimeout(() => !isAnimating && fallbackSwap()); } else { swap(); } From b20452909d4f60db553e8c396526ad0d810106f1 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 15:08:23 -0500 Subject: [PATCH 02/15] feat(astro): add `astro:unmount` event --- .changeset/sharp-bobcats-agree.md | 5 +++++ packages/astro/src/runtime/server/astro-island.ts | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/sharp-bobcats-agree.md diff --git a/.changeset/sharp-bobcats-agree.md b/.changeset/sharp-bobcats-agree.md new file mode 100644 index 000000000000..826cbcc7b792 --- /dev/null +++ b/.changeset/sharp-bobcats-agree.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Emit `astro:unmount` even when islands should be unmounted diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 7be630d068b1..08d554af20e4 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -51,6 +51,11 @@ declare const Astro: { public Component: any; public hydrator: any; static observedAttributes = ['props']; + disconnectedCallback() { + document.addEventListener('astro:after-swap', () => { + if (!this.isConnected) this.dispatchEvent(new CustomEvent('astro:unmount')) + }, { once: true }) + } connectedCallback() { if (!this.hasAttribute('await-children') || this.firstChild) { this.childrenConnectedCallback(); From 5a98953f9b2f09aea59044843bbc263c1f17a899 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 15:41:36 -0500 Subject: [PATCH 03/15] feat(vue): automatically unmount islands --- .changeset/ninety-boats-brake.md | 5 +++++ packages/integrations/vue/client.js | 16 +++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 .changeset/ninety-boats-brake.md diff --git a/.changeset/ninety-boats-brake.md b/.changeset/ninety-boats-brake.md new file mode 100644 index 000000000000..cb08241b2117 --- /dev/null +++ b/.changeset/ninety-boats-brake.md @@ -0,0 +1,5 @@ +--- +'@astrojs/vue': patch +--- + +Automatically unmount islands when `astro:unmount` is fired diff --git a/packages/integrations/vue/client.js b/packages/integrations/vue/client.js index ca61116b2af1..8b2a5eede16c 100644 --- a/packages/integrations/vue/client.js +++ b/packages/integrations/vue/client.js @@ -21,15 +21,13 @@ export default (element) => content = h(Suspense, null, content); } - if (client === 'only') { - const app = createApp({ name, render: () => content }); - await setup(app); - app.mount(element, false); - } else { - const app = createSSRApp({ name, render: () => content }); - await setup(app); - app.mount(element, true); - } + const isHydrate = client !== 'only'; + const boostrap = isHydrate ? createSSRApp : createApp; + const app = boostrap({ name, render: () => content }); + await setup(app); + app.mount(element, isHydrate); + + element.addEventListener('astro:unmount', () => app.unmount(), { once: true }); }; function isAsync(fn) { From 1a10c6acc6dacd1f437939fb8789184c1b8625c9 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 15:46:37 -0500 Subject: [PATCH 04/15] feat(react): automatically unmount islands --- packages/integrations/react/client-v17.js | 11 ++++++----- packages/integrations/react/client.js | 10 +++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/integrations/react/client-v17.js b/packages/integrations/react/client-v17.js index 44310960303c..70bddc353b76 100644 --- a/packages/integrations/react/client-v17.js +++ b/packages/integrations/react/client-v17.js @@ -1,5 +1,5 @@ import { createElement } from 'react'; -import { render, hydrate } from 'react-dom'; +import { render, hydrate, unmountComponentAtNode } from 'react-dom'; import StaticHtml from './static-html.js'; export default (element) => @@ -12,8 +12,9 @@ export default (element) => props, children != null ? createElement(StaticHtml, { value: children }) : children ); - if (client === 'only') { - return render(componentEl, element); - } - return hydrate(componentEl, element); + + const isHydrate = client !== 'only'; + const bootstrap = isHydrate ? hydrate : render; + bootstrap(componentEl, element); + element.addEventListener('astro:unmount', () => unmountComponentAtNode(element), { once: true }); }; diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js index d8948e7bb7a6..dbd32c0c5a54 100644 --- a/packages/integrations/react/client.js +++ b/packages/integrations/react/client.js @@ -31,10 +31,14 @@ export default (element) => } if (client === 'only') { return startTransition(() => { - createRoot(element).render(componentEl); + const root = createRoot(element); + root.render(componentEl); + element.addEventListener('astro:unmount', () => root.unmount(), { once: true }); }); } - return startTransition(() => { - hydrateRoot(element, componentEl, renderOptions); + startTransition(() => { + const root = hydrateRoot(element, componentEl, renderOptions); + root.render(componentEl); + element.addEventListener('astro:unmount', () => root.unmount(), { once: true }); }); }; From 641b34474a0f147ce1a75a35621bc042f4c1cdaf Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 15:47:13 -0500 Subject: [PATCH 05/15] feat(react): automatically unmount islands --- .changeset/wise-weeks-melt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wise-weeks-melt.md diff --git a/.changeset/wise-weeks-melt.md b/.changeset/wise-weeks-melt.md new file mode 100644 index 000000000000..071c7fae6734 --- /dev/null +++ b/.changeset/wise-weeks-melt.md @@ -0,0 +1,5 @@ +--- +'@astrojs/react': patch +--- + +Automatically unmount islands when `astro:unmount` fires From fd914eac3c2719f7f34a262914798555f7e5b790 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 16:09:07 -0500 Subject: [PATCH 06/15] feat(solid): automatically dispose of islands --- packages/integrations/solid/src/client.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/integrations/solid/src/client.ts b/packages/integrations/solid/src/client.ts index 730db0f517a1..61d79a534072 100644 --- a/packages/integrations/solid/src/client.ts +++ b/packages/integrations/solid/src/client.ts @@ -9,7 +9,7 @@ export default (element: HTMLElement) => } if (!element.hasAttribute('ssr')) return; - const fn = client === 'only' ? render : hydrate; + const boostrap = client === 'only' ? render : hydrate; let _slots: Record = {}; if (Object.keys(slotted).length > 0) { @@ -30,7 +30,7 @@ export default (element: HTMLElement) => const { default: children, ...slots } = _slots; const renderId = element.dataset.solidRenderId; - fn( + const dispose = boostrap( () => createComponent(Component, { ...props, @@ -42,4 +42,6 @@ export default (element: HTMLElement) => renderId, } ); + + document.addEventListener('astro:unmount', () => dispose(), { once: true }) }; From fc01cb8b5e466e2cdd4162793d69fdc756dd3c47 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 16:09:13 -0500 Subject: [PATCH 07/15] feat(svelte): automatically destroy of islands --- packages/integrations/svelte/client.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/integrations/svelte/client.js b/packages/integrations/svelte/client.js index 0d07ff2ba563..305fe17c9113 100644 --- a/packages/integrations/svelte/client.js +++ b/packages/integrations/svelte/client.js @@ -14,7 +14,7 @@ export default (target) => { try { if (import.meta.env.DEV) useConsoleFilter(); - new Component({ + const component = new Component({ target, props: { ...props, @@ -24,6 +24,8 @@ export default (target) => { hydrate: client !== 'only', $$inline: true, }); + + document.addEventListener('astro:unmount', () => component.$destroy(), { once: true }) } catch (e) { } finally { if (import.meta.env.DEV) finishUsingConsoleFilter(); From 774b96d3af51b45e6efce2c3ef9e83fe2d618a3b Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 16:40:41 -0500 Subject: [PATCH 08/15] feat(svelte): automatically destroy of islands --- packages/integrations/svelte/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/svelte/client.js b/packages/integrations/svelte/client.js index 305fe17c9113..99612a580a6e 100644 --- a/packages/integrations/svelte/client.js +++ b/packages/integrations/svelte/client.js @@ -25,7 +25,7 @@ export default (target) => { $$inline: true, }); - document.addEventListener('astro:unmount', () => component.$destroy(), { once: true }) + element.addEventListener('astro:unmount', () => component.$destroy(), { once: true }) } catch (e) { } finally { if (import.meta.env.DEV) finishUsingConsoleFilter(); From 21bf59d0fbec14592592a62ab2844ac0f27784c7 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 16:40:45 -0500 Subject: [PATCH 09/15] feat(solid): automatically dispose of islands --- packages/integrations/solid/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/integrations/solid/src/client.ts b/packages/integrations/solid/src/client.ts index 61d79a534072..66b3767ea9fd 100644 --- a/packages/integrations/solid/src/client.ts +++ b/packages/integrations/solid/src/client.ts @@ -43,5 +43,5 @@ export default (element: HTMLElement) => } ); - document.addEventListener('astro:unmount', () => dispose(), { once: true }) + element.addEventListener('astro:unmount', () => dispose(), { once: true }) }; From ad6c6f3ef6c0646306eefc8abc4959ad540df95a Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 16:40:59 -0500 Subject: [PATCH 10/15] feat(preact): automatically unmount islands --- packages/integrations/preact/src/client.ts | 31 ++++++++-------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/integrations/preact/src/client.ts b/packages/integrations/preact/src/client.ts index f90614398886..ad24e886b4f4 100644 --- a/packages/integrations/preact/src/client.ts +++ b/packages/integrations/preact/src/client.ts @@ -1,6 +1,6 @@ -import { h, render, type JSX } from 'preact'; -import StaticHtml from './static-html.js'; import type { SignalLike } from './types'; +import { h, render, hydrate } from 'preact'; +import StaticHtml from './static-html.js'; const sharedSignalMap = new Map(); @@ -8,7 +8,8 @@ export default (element: HTMLElement) => async ( Component: any, props: Record, - { default: children, ...slotted }: Record + { default: children, ...slotted }: Record, + { client }: Record ) => { if (!element.hasAttribute('ssr')) return; for (const [key, value] of Object.entries(slotted)) { @@ -27,23 +28,13 @@ export default (element: HTMLElement) => } } - // eslint-disable-next-line @typescript-eslint/no-shadow - function Wrapper({ children }: { children: JSX.Element }) { - let attrs = Object.fromEntries( - Array.from(element.attributes).map((attr) => [attr.name, attr.value]) - ); - return h(element.localName, attrs, children); - } - - let parent = element.parentNode as Element; + const bootstrap = client !== 'only' ? hydrate : render; - render( - h( - Wrapper, - null, - h(Component, props, children != null ? h(StaticHtml, { value: children }) : children) - ), - parent, - element + bootstrap( + h(Component, props, children != null ? h(StaticHtml, { value: children }) : children), + element, ); + + // Preact has no "unmount" option, but you can use `render(null, element)` + element.addEventListener('astro:unmount', () => render(null, element), { once: true }) }; From eb6446c1557c8d11480ae3abbaa0fb9ef8f5e239 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 28 Aug 2023 16:41:06 -0500 Subject: [PATCH 11/15] chore: update changeset --- .changeset/ninety-boats-brake.md | 4 ++++ .changeset/sharp-bobcats-agree.md | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 .changeset/sharp-bobcats-agree.md diff --git a/.changeset/ninety-boats-brake.md b/.changeset/ninety-boats-brake.md index cb08241b2117..30c13a8207a7 100644 --- a/.changeset/ninety-boats-brake.md +++ b/.changeset/ninety-boats-brake.md @@ -1,5 +1,9 @@ --- +'@astrojs/react': patch +'@astrojs/preact': patch '@astrojs/vue': patch +'@astrojs/solid-js': patch +'@astrojs/svelte': patch --- Automatically unmount islands when `astro:unmount` is fired diff --git a/.changeset/sharp-bobcats-agree.md b/.changeset/sharp-bobcats-agree.md deleted file mode 100644 index 826cbcc7b792..000000000000 --- a/.changeset/sharp-bobcats-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'astro': patch ---- - -Emit `astro:unmount` even when islands should be unmounted From c0b75bec454ba83c4e16b08449aa5885650f5f4a Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 29 Aug 2023 08:56:01 -0500 Subject: [PATCH 12/15] fix: rebase issue --- packages/astro/components/ViewTransitions.astro | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/astro/components/ViewTransitions.astro b/packages/astro/components/ViewTransitions.astro index a1ce47759325..15bad445d817 100644 --- a/packages/astro/components/ViewTransitions.astro +++ b/packages/astro/components/ViewTransitions.astro @@ -222,23 +222,15 @@ const { fallback = 'animate' } = Astro.props as Props; links.length && (await Promise.all(links)); if (fallback === 'animate') { - let isAnimating = false; - addEventListener('animationstart', () => (isAnimating = true), { once: true }); - // Trigger the animations document.documentElement.dataset.astroTransitionFallback = 'old'; + const finished = Promise.all(document.getAnimations().map(a => a.finished)); const fallbackSwap = () => { - removeEventListener('animationend', fallbackSwap); - clearTimeout(timeout); swap(); document.documentElement.dataset.astroTransitionFallback = 'new'; }; - // If there are any animations, want for the animationend event. - addEventListener('animationend', fallbackSwap, { once: true }); - // If there are no animations, go ahead and swap on next tick - // This is necessary because we do not know if there are animations. - // The setTimeout is a fallback in case there are none. - let timeout = setTimeout(() => !isAnimating && fallbackSwap()); + await finished; + fallbackSwap(); } else { swap(); } From b0a3573ad44eda1862d4f15830ad517151f1b089 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 29 Aug 2023 08:56:09 -0500 Subject: [PATCH 13/15] chore: add clarifying comment --- packages/astro/src/runtime/server/astro-island.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 08d554af20e4..e0e09eaec6f3 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -53,6 +53,7 @@ declare const Astro: { static observedAttributes = ['props']; disconnectedCallback() { document.addEventListener('astro:after-swap', () => { + // If element wasn't persisted, fire unmount event if (!this.isConnected) this.dispatchEvent(new CustomEvent('astro:unmount')) }, { once: true }) } From a07cda269830967790e92cef37ae3259e19001b1 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 29 Aug 2023 08:56:55 -0500 Subject: [PATCH 14/15] chore: remove duplicate changeset --- .changeset/wise-weeks-melt.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/wise-weeks-melt.md diff --git a/.changeset/wise-weeks-melt.md b/.changeset/wise-weeks-melt.md deleted file mode 100644 index 071c7fae6734..000000000000 --- a/.changeset/wise-weeks-melt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@astrojs/react': patch ---- - -Automatically unmount islands when `astro:unmount` fires From bf81bb887c4c09267f7b530a6ebfa678953a494c Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Tue, 29 Aug 2023 08:57:46 -0500 Subject: [PATCH 15/15] chore: add changeset --- .changeset/perfect-socks-hammer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perfect-socks-hammer.md diff --git a/.changeset/perfect-socks-hammer.md b/.changeset/perfect-socks-hammer.md new file mode 100644 index 000000000000..baae63ffe8da --- /dev/null +++ b/.changeset/perfect-socks-hammer.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fire `astro:unmount` event when island is disconnected