fix: allow component to be mounted in iframe (#13225)

* fix: allow component to be mounted in iframe

* prettier

* prettier

* fix: allow HMR cleanup styles in iframe and custom elements

* add test

* avoid creating unnecessary microtasks

* simplify

* rename stuff

* typo

* lint

* changeset

* changeset

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Rich Harris <hello@rich-harris.dev>
pull/13290/head
Maxime LUCE 3 days ago committed by GitHub
parent a680e6f2e7
commit 5210ae2610
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: allow custom element styles to be updated in HMR mode

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: inject styles correctly when mounting inside an iframe

@ -494,18 +494,7 @@ export function client_component(analysis, options) {
if (analysis.css.hash) {
// remove existing `<style>` element, in case CSS changed
accept_fn_body.unshift(
b.stmt(
b.call(
b.member(
b.call('document.querySelector', b.literal('#' + analysis.css.hash)),
'remove',
false,
true
)
)
)
);
accept_fn_body.unshift(b.stmt(b.call('$.cleanup_styles', b.literal(analysis.css.hash))));
}
const hmr = b.block([

@ -0,0 +1,31 @@
/** @type {Map<String, Set<HTMLStyleElement>>} */
var all_styles = new Map();
/**
* @param {String} hash
* @param {HTMLStyleElement} style
*/
export function register_style(hash, style) {
var styles = all_styles.get(hash);
if (!styles) {
styles = new Set();
all_styles.set(hash, styles);
}
styles.add(style);
}
/**
* @param {String} hash
*/
export function cleanup_styles(hash) {
var styles = all_styles.get(hash);
if (!styles) return;
for (const style of styles) {
style.remove();
}
all_styles.delete(hash);
}

@ -1,16 +1,22 @@
import { DEV } from 'esm-env';
import { queue_micro_task } from './task.js';
import { register_style } from '../dev/css.js';
var seen = new Set();
var roots = new WeakMap();
/**
* @param {Node} anchor
* @param {{ hash: string, code: string }} css
* @param {boolean} [is_custom_element]
*/
export function append_styles(anchor, css, is_custom_element = false) {
export function append_styles(anchor, css, is_custom_element) {
// in dev, always check the DOM, so that styles can be replaced with HMR
if (!DEV && !is_custom_element) {
var doc = /** @type {Document} */ (anchor.ownerDocument);
if (!roots.has(doc)) roots.set(doc, new Set());
const seen = roots.get(doc);
if (seen.has(css)) return;
seen.add(css);
}
@ -29,6 +35,10 @@ export function append_styles(anchor, css, is_custom_element = false) {
style.textContent = css.code;
target.appendChild(style);
if (DEV) {
register_style(css.hash, style);
}
}
});
}

@ -1,4 +1,5 @@
export { FILENAME, HMR } from '../../constants.js';
export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
export {

@ -0,0 +1,19 @@
<script>
import { mount } from 'svelte';
import Child from './Child.svelte';
let { count } = $props();
let iframe;
$effect(() => {
mount(Child, {
target: iframe.contentWindow.document.body,
props: {
count
}
});
});
</script>
<iframe title="container" bind:this={iframe}></iframe>

@ -0,0 +1,13 @@
<svelte:options css="injected" />
<script>
let { count } = $props();
</script>
<h1>count: {count}</h1>
<style>
h1 {
color: red;
}
</style>

@ -0,0 +1,22 @@
import { flushSync } from 'svelte';
import { test } from '../../assert';
export default test({
async test({ target, assert }) {
const button = target.querySelector('button');
const h1 = () =>
/** @type {HTMLHeadingElement} */ (
/** @type {Window} */ (
target.querySelector('iframe')?.contentWindow
).document.querySelector('h1')
);
assert.equal(h1().textContent, 'count: 0');
assert.equal(getComputedStyle(h1()).color, 'rgb(255, 0, 0)');
flushSync(() => button?.click());
assert.equal(h1().textContent, 'count: 1');
assert.equal(getComputedStyle(h1()).color, 'rgb(255, 0, 0)');
}
});

@ -0,0 +1,11 @@
<script>
import App from './App.svelte';
let count = $state(0);
</script>
<button onclick={() => (count += 1)}>remount</button>
{#key count}
<App {count} />
{/key}
Loading…
Cancel
Save