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:
parent
3307cb34f1
commit
9cd84bd19b
10 changed files with 106 additions and 15 deletions
26
.changeset/lovely-nails-cough.md
Normal file
26
.changeset/lovely-nails-cough.md
Normal 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} />
|
||||
```
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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:*",
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
12
pnpm-lock.yaml
generated
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue