Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support view transitions for client:only components #8745

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lazy-keys-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Support view transitions for client:only components
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Layout from '../components/Layout.astro';
import Island from '../components/Island';
---
<Layout>
<p id="page-one">Page 1</p>
<a id="click-two" href="/client-only-two">go to page 2</a>
<div transition:persist="island">
<Island client:only count={5}>message here</Island>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Island from '../components/Island';
---
<Layout>
<p id="page-two">Page 2</p>
<a id="click-one" href="/client-only-one">go to page 1</a>
<div transition:persist="island">
<Island client:only count={5}>message here</Island>
</div>
Expand Down
18 changes: 17 additions & 1 deletion packages/astro/e2e/view-transitions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -649,23 +649,39 @@ test.describe('View Transitions', () => {
});

test('client:only styles are retained on transition', async ({ page, astro }) => {
const loads = [];
page.addListener('load', async (p) => {
loads.push(p);
});

const totalExpectedStyles = 7;

// Go to page 1
// Go to page 1 (normal load)
await page.goto(astro.resolveUrl('/client-only-one'));
let msg = page.locator('.counter-message');
await expect(msg).toHaveText('message here');

let styles = await page.locator('style').all();
expect(styles.length).toEqual(totalExpectedStyles);

// Transition to page 2 (will do a full load)
await page.click('#click-two');

let pageTwo = page.locator('#page-two');
await expect(pageTwo, 'should have content').toHaveText('Page 2');

// Transition to page 1 (will do a full load)
await page.click('#click-one');

let pageOne = page.locator('#page-one');
await expect(pageOne, 'should have content').toHaveText('Page 1');

// Transition to page 1 (real transition, no full load)
await page.click('#click-two');

styles = await page.locator('style').all();
expect(styles.length).toEqual(totalExpectedStyles, 'style count has not changed');
expect(loads.length, 'There should only be 1 page load').toEqual(3);
});

test('Horizontal scroll position restored on back button', async ({ page, astro }) => {
Expand Down
85 changes: 71 additions & 14 deletions packages/astro/src/transitions/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ type State = {
};
type Events = 'astro:page-load' | 'astro:after-swap';

let viteDevIds: { static: Record<string, string[]>; dynamic: Record<string, string[]> };
if (import.meta.env.DEV) {
// viteDevIds on a page
viteDevIds = JSON.parse(
sessionStorage.getItem('astro:viteDevIds') || '{"static":{},"dynamic":{}}'
);
}
const page = (url: { origin: string; pathname: string }) => url.origin + url.pathname;

// only update history entries that are managed by us
// leave other entries alone and do not accidently add state.
const persistState = (state: State) => history.state && history.replaceState(state, '');
Expand Down Expand Up @@ -44,8 +53,10 @@ const PERSIST_ATTR = 'data-astro-transition-persist';
const parser = new DOMParser();
// explained at its usage
let noopEl: HTMLDivElement;
let reloadEl: HTMLDivElement;
if (import.meta.env.DEV) {
noopEl = document.createElement('div');
reloadEl = document.createElement('div');
}

// The History API does not tell you if navigation is forward or back, so
Expand Down Expand Up @@ -198,20 +209,40 @@ async function updateDOM(
const href = el.getAttribute('href');
return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
}
// What follows is a fix for an issue (#8472) with missing client:only styles after transition.
// That problem exists only in dev mode where styles are injected into the page by Vite.
// Returning a noop element ensures that the styles are not removed from the old document.
// Guarding the code below with the dev mode check
// allows tree shaking to remove this code in production.

if (import.meta.env.DEV) {
if (el.tagName === 'STYLE' && el.dataset.viteDevId) {
const devId = el.dataset.viteDevId;
// If this same style tag exists, remove it from the new page
return (
newDocument.querySelector(`style[data-vite-dev-id="${devId}"]`) ||
// Otherwise, keep it anyways. This is client:only styles.
noopEl
);
const viteDevId = el.getAttribute('data-vite-dev-id');
if (!viteDevId) {
return null;
}
const newDevEl = newDocument.head.querySelector(`[data-vite-dev-id="${viteDevId}"]`);
if (newDevEl) {
return newDevEl;
}
// What follows is a fix for an issue (#8472) with missing client:only styles after transition.
// That problem exists only in dev mode where styles are injected into the page by Vite.
// Returning a noop element ensures that the styles are not removed from the old document.
// Guarding the code below with the dev mode check
// allows tree shaking to remove this code in production.
if (
document.querySelector(
`[${PERSIST_ATTR}] astro-island[client="only"], astro-island[client="only"][${PERSIST_ATTR}]`
)
) {
const here = page(toLocation);
const dynamicViteDevIds = viteDevIds.dynamic[here];
if (!dynamicViteDevIds) {
console.info(`
${toLocation.pathname}
Development mode only: This page uses view transitions with persisted client:only Astro islands.
On the first transition to this page, Astro did a full page reload to capture the dynamic effects of the client only code.
`);
location.href = toLocation.href;
return reloadEl;
}
if (dynamicViteDevIds?.includes(viteDevId)) {
return noopEl;
}
}
}
return null;
Expand Down Expand Up @@ -249,12 +280,16 @@ async function updateDOM(
// Swap head
for (const el of Array.from(document.head.children)) {
const newEl = persistedHeadElement(el as HTMLElement);
if (newEl === reloadEl) {
return;
}
// If the element exists in the document already, remove it
// from the new document and leave the current node alone
if (newEl) {
newEl.remove();
} else {
// Otherwise remove the element in the head. It doesn't exist in the new page.
// Otherwise remove the element from the head.
// It doesn't exist in the new page or will be re-inserted after this loop
el.remove();
}
}
Expand Down Expand Up @@ -336,6 +371,20 @@ async function transition(
options: Options,
popState?: State
) {
if (import.meta.env.DEV) {
const thisPageStaticViteDevIds = viteDevIds.static[page(location)];
if (thisPageStaticViteDevIds) {
const allViteDevIds = new Set<string>();
document.head
.querySelectorAll('[data-vite-dev-id]')
.forEach((el) => allViteDevIds.add(el.getAttribute('data-vite-dev-id')!));
viteDevIds.dynamic[page(location)] = [...allViteDevIds].filter(
(x) => !thisPageStaticViteDevIds.includes(x)
);
sessionStorage.setItem('astro:viteDevIds', JSON.stringify(viteDevIds, null, 2));
}
}

let finished: Promise<void>;
const href = toLocation.href;
const response = await fetchHTML(href);
Expand All @@ -360,6 +409,14 @@ async function transition(
location.href = href;
return;
}
if (import.meta.env.DEV) {
const staticViteDevIds = new Set<string>();
newDocument.querySelectorAll('head > [data-vite-dev-id]').forEach((el) => {
staticViteDevIds.add(el.getAttribute('data-vite-dev-id')!);
});
viteDevIds.static[page(toLocation)] = [...staticViteDevIds];
sessionStorage.setItem('astro:viteDevIds', JSON.stringify(viteDevIds, null, 2));
}

if (!popState) {
// save the current scroll position before we change the DOM and transition to the new page
Expand Down