diff --git a/.changeset/legal-mangos-peel.md b/.changeset/legal-mangos-peel.md new file mode 100644 index 000000000000..bddad21bff37 --- /dev/null +++ b/.changeset/legal-mangos-peel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: change title only after any pending work has completed diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 98d7880b255d..edd8835e0033 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -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]))))); } } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 9d61b6bbf92b..27c90d770843 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -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; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 4a9fce7286a5..8c4b84438c5b 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -366,10 +366,11 @@ export function render_effect(fn, flags = 0) { * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {Array>} 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); }); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte new file mode 100644 index 000000000000..089ba43607dd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte @@ -0,0 +1,15 @@ + + + + title + + +

{await push()}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js new file mode 100644 index 000000000000..39cbf5becba9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js @@ -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'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte new file mode 100644 index 000000000000..353515708785 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte new file mode 100644 index 000000000000..b2a86562769c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte @@ -0,0 +1,13 @@ + + + + {await push()} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js new file mode 100644 index 000000000000..b89dce62d1ec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js @@ -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'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte new file mode 100644 index 000000000000..353515708785 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if}