fix: change title only after any pending work has completed (#17061)

* fix: change title only after any pending work has completed

We have to use an effect - not a render effect - for updating the title, and always. That way we change the title only after any pending work has completed.

Fixes #17060

* fix
pull/17093/head
Simon H 3 weeks ago committed by GitHub
parent 723c421fbb
commit 0e709e3fec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: change title only after any pending work has completed

@ -1,16 +1,19 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_template_chunk } from './shared/utils.js';
import { build_template_chunk, Memoizer } from './shared/utils.js';
/**
* @param {AST.TitleElement} node
* @param {ComponentContext} context
*/
export function TitleElement(node, context) {
const memoizer = new Memoizer();
const { has_state, value } = build_template_chunk(
/** @type {any} */ (node.fragment.nodes),
context
context,
context.state,
(value, metadata) => memoizer.add(value, metadata)
);
const evaluated = context.state.scope.evaluate(value);
@ -26,9 +29,21 @@ export function TitleElement(node, context) {
)
);
// Always in an $effect so it only changes the title once async work is done
if (has_state) {
context.state.update.push(statement);
context.state.after_update.push(
b.stmt(
b.call(
'$.template_effect',
b.arrow(memoizer.apply(), b.block([statement])),
memoizer.sync_values(),
memoizer.async_values(),
memoizer.blockers(),
b.true
)
)
);
} else {
context.state.init.push(statement);
context.state.after_update.push(b.stmt(b.call('$.effect', b.thunk(b.block([statement])))));
}
}

@ -14,33 +14,25 @@ import {
MAYBE_DIRTY,
DERIVED,
BOUNDARY_EFFECT,
EAGER_EFFECT
EAGER_EFFECT,
HEAD_EFFECT
} from '#client/constants';
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property } from '../../shared/utils.js';
import {
active_effect,
get,
increment_write_version,
is_dirty,
is_updating_effect,
set_is_updating_effect,
set_signal_status,
tick,
update_effect
} from '../runtime.js';
import * as e from '../errors.js';
import { flush_tasks, queue_micro_task } from '../dom/task.js';
import { DEV } from 'esm-env';
import { invoke_error_boundary } from '../error-handling.js';
import {
flush_eager_effects,
eager_effects,
old_values,
set_eager_effects,
source,
update
} from './sources.js';
import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js';
import { eager_effect, unlink_effect } from './effects.js';
/**
@ -800,7 +792,12 @@ export function schedule_effect(signal) {
// if the effect is being scheduled because a parent (each/await/etc) block
// updated an internal source, bail out or we'll cause a second flush
if (is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0) {
if (
is_flushing &&
effect === active_effect &&
(flags & BLOCK_EFFECT) !== 0 &&
(flags & HEAD_EFFECT) === 0
) {
return;
}

@ -366,10 +366,11 @@ export function render_effect(fn, flags = 0) {
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {Array<Promise<void>>} blockers
* @param {boolean} defer
*/
export function template_effect(fn, sync = [], async = [], blockers = []) {
export function template_effect(fn, sync = [], async = [], blockers = [], defer = false) {
flatten(blockers, sync, async, (values) => {
create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true);
create_effect(defer ? EFFECT : RENDER_EFFECT, () => fn(...values.map(get)), true);
});
}

@ -0,0 +1,15 @@
<script>
let { deferred } = $props();
function push() {
const d = Promise.withResolvers();
deferred.push(() => d.resolve());
return d.promise;
}
</script>
<svelte:head>
<title>title</title>
</svelte:head>
<p>{await push()}</p>

@ -0,0 +1,24 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [toggle, resolve] = target.querySelectorAll('button');
toggle.click();
await tick();
assert.equal(window.document.title, '');
toggle.click();
await tick();
assert.equal(window.document.title, '');
toggle.click();
await tick();
assert.equal(window.document.title, '');
resolve.click();
await tick();
await tick();
assert.equal(window.document.title, 'title');
}
});

@ -0,0 +1,12 @@
<script>
import Inner from './Inner.svelte';
let deferred = [];
let show = $state(false);
</script>
<button onclick={() => show = !show}>toggle</button>
<button onclick={() => deferred.pop()()}>resolve</button>
{#if show}
<Inner {deferred} />
{/if}

@ -0,0 +1,13 @@
<script>
let { deferred } = $props();
function push() {
const d = Promise.withResolvers();
deferred.push(() => d.resolve('title'));
return d.promise;
}
</script>
<svelte:head>
<title>{await push()}</title>
</svelte:head>

@ -0,0 +1,23 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [toggle, resolve] = target.querySelectorAll('button');
toggle.click();
await tick();
assert.equal(window.document.title, '');
toggle.click();
await tick();
assert.equal(window.document.title, '');
toggle.click();
await tick();
assert.equal(window.document.title, '');
resolve.click();
await tick();
assert.equal(window.document.title, 'title');
}
});

@ -0,0 +1,12 @@
<script>
import Inner from './Inner.svelte';
let deferred = [];
let show = $state(false);
</script>
<button onclick={() => show = !show}>toggle</button>
<button onclick={() => deferred.pop()()}>resolve</button>
{#if show}
<Inner {deferred} />
{/if}
Loading…
Cancel
Save