0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-31 23:31:30 -05:00

Allow islands to be re-rendered with new props on page transition (#10136)

* Allow islands to be re-rendered with new props on page transition

* Adjust the expected styles

* Restore test expectation

* Add changeset and final change

* linting

* Implement transition:persist-props behavior

* Fix lockfile

* Fix expectations

* App is hyrid

* Update .changeset/lovely-nails-cough.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update .changeset/lovely-nails-cough.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

* Update .changeset/lovely-nails-cough.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>

---------

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
This commit is contained in:
Matthew Phillips 2024-03-08 05:54:16 -05:00 committed by GitHub
parent 3307cb34f1
commit 9cd84bd19b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 106 additions and 15 deletions

View file

@ -0,0 +1,26 @@
---
"@astrojs/react": minor
"astro": minor
---
Changes the default behavior of `transition:persist` to update the props of persisted islands upon navigation. Also adds a new view transitions option `transition:persist-props` (default: `false`) to prevent props from updating as needed.
Islands which have the `transition:persist` property to keep their state when using the `<ViewTransitions />` router will now have their props updated upon navigation. This is useful in cases where the component relies on page-specific props, such as the current page title, which should update upon navigation.
For example, the component below is set to persist across navigation. This component receives a `products` props and might have some internal state, such as which filters are applied:
```astro
<ProductListing transition:persist products={products} />
```
Upon navigation, this component persists, but the desired `products` might change, for example if you are visiting a category of products, or you are performing a search.
Previously the props would not change on navigation, and your island would have to handle updating them externally, such as with API calls.
With this change the props are now updated, while still preserving state.
You can override this new default behavior on a per-component basis using `transition:persist-props=true` to persist both props and state during navigation:
```astro
<ProductListing transition:persist-props=true products={products} />
```

View file

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import './Island.css';
import { indirect} from './css.js';
export default function Counter({ children, count: initialCount, id }) {
export default function Counter({ children, count: initialCount, id, page }) {
const [count, setCount] = useState(initialCount);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);
@ -10,6 +10,7 @@ export default function Counter({ children, count: initialCount, id }) {
return (
<>
<div id={id} className="counter">
<h1 className="page">{page}</h1>
<button className="decrement" onClick={subtract}>-</button>
<pre>{count}</pre>
<button className="increment" onClick={add}>+</button>

View file

@ -1,9 +1,13 @@
---
import Layout from '../components/Layout.astro';
import Island from '../components/Island.jsx';
export const prerender = false;
const persistProps = Astro.url.searchParams.has('persist');
---
<Layout>
<p id="island-one">Page 1</p>
<a id="click-two" href="/island-two">go to 2</a>
<Island count={5} client:load transition:persist transition:name="counter" />
<Island count={5} page="Island 1" client:load transition:persist transition:name="counter"
transition:persist-props={persistProps} />
</Layout>

View file

@ -5,5 +5,5 @@ import Island from '../components/Island.jsx';
<Layout>
<p id="island-two">Page 2</p>
<a id="click-one" href="/island-one">go to 1</a>
<Island count={2} client:load transition:persist transition:name="counter" />
<Island count={2} page="Island 2" client:load transition:persist transition:name="counter" />
</Layout>

View file

@ -543,6 +543,38 @@ test.describe('View Transitions', () => {
cnt = page.locator('.counter pre');
// Count should remain
await expect(cnt).toHaveText('6');
// Props should have changed
const pageTitle = page.locator('.page');
await expect(pageTitle).toHaveText('Island 2');
});
test('transition:persist-props prevents props from changing', async ({ page, astro }) => {
// Go to page 1
await page.goto(astro.resolveUrl('/island-one?persist'));
// Navigate to page 2
await page.click('#click-two');
const p = page.locator('#island-two');
await expect(p).toBeVisible();
// Props should have changed
const pageTitle = page.locator('.page');
await expect(pageTitle).toHaveText('Island 1');
});
test('transition:persist-props=false makes props update', async ({ page, astro }) => {
// Go to page 2
await page.goto(astro.resolveUrl('/island-two'));
// Navigate to page 1
await page.click('#click-one');
const p = page.locator('#island-one');
await expect(p).toBeVisible();
// Props should have changed
const pageTitle = page.locator('.page');
await expect(pageTitle).toHaveText('Island 1');
});
test('Scripts are only executed once', async ({ page, astro }) => {

View file

@ -114,7 +114,7 @@
"test:node": "astro-scripts test \"test/**/*.test.js\""
},
"dependencies": {
"@astrojs/compiler": "^2.5.3",
"@astrojs/compiler": "^2.7.0",
"@astrojs/internal-helpers": "workspace:*",
"@astrojs/markdown-remark": "workspace:*",
"@astrojs/telemetry": "workspace:*",

View file

@ -27,6 +27,7 @@ interface ExtractedProps {
const transitionDirectivesToCopyOnIsland = Object.freeze([
'data-astro-transition-scope',
'data-astro-transition-persist',
'data-astro-transition-persist-props',
]);
// Used to extract the directives, aka `client:load` information about a component.
@ -175,7 +176,7 @@ export async function generateHydrateScript(
);
transitionDirectivesToCopyOnIsland.forEach((name) => {
if (props[name]) {
if (typeof props[name] !== 'undefined') {
island.props[name] = props[name];
}
});

View file

@ -306,6 +306,11 @@ async function updateDOM(
}
};
const shouldCopyProps = (el: HTMLElement): boolean => {
const persistProps = el.dataset.astroTransitionPersistProps;
return persistProps == null || persistProps === 'false';
}
const defaultSwap = (beforeSwapEvent: TransitionBeforeSwapEvent) => {
// swap attributes of the html element
// - delete all attributes from the current document
@ -366,6 +371,11 @@ async function updateDOM(
// The element exists in the new page, replace it with the element
// from the old page so that state is preserved.
newEl.replaceWith(el);
// For islands, copy over the props to allow them to re-render
if(newEl.localName === 'astro-island' && shouldCopyProps(el as HTMLElement)) {
el.setAttribute('ssr', '');
el.setAttribute('props', newEl.getAttribute('props')!);
}
}
}
restoreFocus(savedFocus);

View file

@ -53,6 +53,17 @@ function getChildren(childString, experimentalReactChildren) {
}
}
// Keep a map of roots so we can reuse them on re-renders
let rootMap = new WeakMap();
const getOrCreateRoot = (element, creator) => {
let root = rootMap.get(element);
if(!root) {
root = creator();
rootMap.set(element, root);
}
return root;
};
export default (element) =>
(Component, props, { default: children, ...slotted }, { client }) => {
if (!element.hasAttribute('ssr')) return;
@ -75,14 +86,20 @@ export default (element) =>
}
if (client === 'only') {
return startTransition(() => {
const root = createRoot(element);
const root = getOrCreateRoot(element, () => {
const r = createRoot(element);
element.addEventListener('astro:unmount', () => r.unmount(), { once: true });
return r;
});
root.render(componentEl);
element.addEventListener('astro:unmount', () => root.unmount(), { once: true });
});
}
startTransition(() => {
const root = hydrateRoot(element, componentEl, renderOptions);
const root = getOrCreateRoot(element, () => {
const r = hydrateRoot(element, componentEl, renderOptions);
element.addEventListener('astro:unmount', () => r.unmount(), { once: true });
return r;
});
root.render(componentEl);
element.addEventListener('astro:unmount', () => root.unmount(), { once: true });
});
};

12
pnpm-lock.yaml generated
View file

@ -498,8 +498,8 @@ importers:
packages/astro:
dependencies:
'@astrojs/compiler':
specifier: ^2.5.3
version: 2.5.3
specifier: ^2.7.0
version: 2.7.0
'@astrojs/internal-helpers':
specifier: workspace:*
version: link:../internal-helpers
@ -5529,8 +5529,8 @@ packages:
/@astrojs/compiler@1.8.2:
resolution: {integrity: sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==}
/@astrojs/compiler@2.5.3:
resolution: {integrity: sha512-jzj01BRv/fmo+9Mr2FhocywGzEYiyiP2GVHje1ziGNU6c97kwhYGsnvwMkHrncAy9T9Vi54cjaMK7UE4ClX4vA==}
/@astrojs/compiler@2.7.0:
resolution: {integrity: sha512-XpC8MAaWjD1ff6/IfkRq/5k1EFj6zhCNqXRd5J43SVJEBj/Bsmizkm8N0xOYscGcDFQkRgEw6/eKnI5x/1l6aA==}
/@astrojs/language-server@2.5.5(prettier-plugin-astro@0.12.3)(prettier@3.1.1)(typescript@5.2.2):
resolution: {integrity: sha512-hk7a8S7bpf//BOA6mMjiYqi/eiYtGPfUfw59eVXdutdRFdwDHtu4jcsLu43ZaId56pAcE8qFjIvJxySvzcxiUA==}
@ -5544,7 +5544,7 @@ packages:
prettier-plugin-astro:
optional: true
dependencies:
'@astrojs/compiler': 2.5.3
'@astrojs/compiler': 2.7.0
'@jridgewell/sourcemap-codec': 1.4.15
'@volar/kit': 1.10.10(typescript@5.2.2)
'@volar/language-core': 1.10.10
@ -5580,7 +5580,7 @@ packages:
prettier-plugin-astro:
optional: true
dependencies:
'@astrojs/compiler': 2.5.3
'@astrojs/compiler': 2.7.0
'@jridgewell/sourcemap-codec': 1.4.15
'@volar/kit': 2.0.4(typescript@5.3.2)
'@volar/language-core': 2.0.4