Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/legal-mangos-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: change title only after any pending work has completed
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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])))));
}
}
4 changes: 2 additions & 2 deletions packages/svelte/src/internal/client/dom/blocks/svelte-head.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @import { TemplateNode } from '#client' */
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { block, branch } from '../../reactivity/effects.js';
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';

/**
Expand Down Expand Up @@ -49,7 +49,7 @@ export function head(hash, render_fn) {
}

try {
block(() => render_fn(anchor), HEAD_EFFECT);
block(() => branch(() => render_fn(anchor)), HEAD_EFFECT);
} finally {
if (was_hydrating) {
set_hydrating(true);
Expand Down
5 changes: 3 additions & 2 deletions packages/svelte/src/internal/client/reactivity/effects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't love this, but it felt even weirder to have a entirely different method that is 90% the same, and I couldn't come up with a good name for that method

});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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');
}
});
Original file line number Diff line number Diff line change
@@ -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}
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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');
}
});
Original file line number Diff line number Diff line change
@@ -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