diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js index 345262dece..440ca9ccdf 100644 --- a/packages/svelte/src/reactivity/url-search-params.js +++ b/packages/svelte/src/reactivity/url-search-params.js @@ -1,21 +1,28 @@ import { source } from '../internal/client/reactivity/sources.js'; import { get } from '../internal/client/runtime.js'; +import { get_current_url } from './url.js'; import { increment } from './utils.js'; export const REPLACE = Symbol(); -export const onChange = Symbol(); export class SvelteURLSearchParams extends URLSearchParams { #version = source(0); - /** - * @type {((command: "set" | "append" | "delete" | "sort", ...args: any[])=>void ) | null} - */ - [onChange] = null; + #url = get_current_url(); + + #updating = false; + + #update_url() { + if (!this.#url || this.#updating) return; + const search = this.toString(); + this.#url.search = search && `?${search}`; + } /** * @param {URLSearchParams} params */ [REPLACE](params) { + this.#updating = true; + for (const key of [...super.keys()]) { super.delete(key); } @@ -25,6 +32,8 @@ export class SvelteURLSearchParams extends URLSearchParams { } increment(this.#version); + + this.#updating = false; } /** @@ -33,10 +42,9 @@ export class SvelteURLSearchParams extends URLSearchParams { * @returns {void} */ append(name, value) { - var res = super.append(name, value); - this[onChange]?.('append', name, value); + super.append(name, value); + this.#update_url(); increment(this.#version); - return res; } /** @@ -46,12 +54,11 @@ export class SvelteURLSearchParams extends URLSearchParams { */ delete(name, value) { var has_value = super.has(name, value); - var res = super.delete(name, value); + super.delete(name, value); if (has_value) { - this[onChange]?.('delete', name, value); + this.#update_url(); increment(this.#version); } - return res; } /** @@ -93,21 +100,20 @@ export class SvelteURLSearchParams extends URLSearchParams { * @returns {void} */ set(name, value) { - var value_before_change = super.getAll(name).join(''); + var previous = super.getAll(name).join(''); super.set(name, value); // can't use has(name, value), because for something like https://svelte.dev?foo=1&bar=2&foo=3 // if you set `foo` to 1, then foo=3 gets deleted whilst `has("foo", "1")` returns true - if (value_before_change !== super.getAll(name).join('')) { - this[onChange]?.('set', name, value); + if (previous !== super.getAll(name).join('')) { + this.#update_url(); increment(this.#version); } } sort() { - var res = super.sort(); - this[onChange]?.('sort'); + super.sort(); + this.#update_url(); increment(this.#version); - return res; } toString() { diff --git a/packages/svelte/src/reactivity/url-search-params.test.ts b/packages/svelte/src/reactivity/url-search-params.test.ts index 5dcb8f1274..3119e6a1d0 100644 --- a/packages/svelte/src/reactivity/url-search-params.test.ts +++ b/packages/svelte/src/reactivity/url-search-params.test.ts @@ -3,6 +3,30 @@ import { flushSync } from '../index-client.js'; import { assert, test } from 'vitest'; import { SvelteURLSearchParams } from './url-search-params'; +test('new URLSearchParams', () => { + const params = new SvelteURLSearchParams('a=b'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(params.toString()); + }); + }); + + flushSync(() => { + params.set('a', 'c'); + }); + + flushSync(() => { + // nothing should happen here + params.set('a', 'c'); + }); + + assert.deepEqual(log, ['a=b', 'a=c']); + + cleanup(); +}); + test('URLSearchParams.set', () => { const params = new SvelteURLSearchParams(); const log: any = []; diff --git a/packages/svelte/src/reactivity/url.js b/packages/svelte/src/reactivity/url.js index e2c273e3e9..7e2e61842d 100644 --- a/packages/svelte/src/reactivity/url.js +++ b/packages/svelte/src/reactivity/url.js @@ -1,6 +1,14 @@ import { source, set } from '../internal/client/reactivity/sources.js'; import { get } from '../internal/client/runtime.js'; -import { onChange, REPLACE, SvelteURLSearchParams } from './url-search-params.js'; +import { REPLACE, SvelteURLSearchParams } from './url-search-params.js'; + +/** @type {SvelteURL | null} */ +let current_url = null; + +export function get_current_url() { + // ideally we'd just export `current_url` directly, but it seems Vitest doesn't respect live bindings + return current_url; +} export class SvelteURL extends URL { #protocol = source(super.protocol); @@ -10,7 +18,8 @@ export class SvelteURL extends URL { #port = source(super.port); #pathname = source(super.pathname); #hash = source(super.hash); - #searchParams = new SvelteURLSearchParams(); + #search = source(super.search); + #searchParams; /** * @param {string | URL} url @@ -19,18 +28,10 @@ export class SvelteURL extends URL { constructor(url, base) { url = new URL(url, base); super(url); - this.#searchParams[REPLACE](url.searchParams); - this.#searchParams[onChange] = (command, ...args) => { - if (this.search !== super.search) { - super.search = this.search; - } - if (this.#searchParams.toString() === super.searchParams.toString()) { - return; - } - // by doing this, we sync SvelteSearchParams with internal implementation of URL.searchParams - // @ts-expect-error - super.searchParams[command](...args); - }; + + current_url = this; + this.#searchParams = new SvelteURLSearchParams(url.searchParams); + current_url = null; } get hash() { @@ -124,13 +125,13 @@ export class SvelteURL extends URL { } get search() { - const search = this.#searchParams?.toString(); - return search ? `?${search}` : ''; + return get(this.#search); } set search(value) { super.search = value; - this.#searchParams[REPLACE](new URLSearchParams(value.replace(/^\?/, ''))); + set(this.#search, value); + this.#searchParams[REPLACE](super.searchParams); } get username() {