feat: add reactive URL object to svelte/reactivity (#11157)

* feat: reactive url

* fix

* simplify

* tidy

* simplify, make ReactiveURLSearchParams signature match URLSearchParams

* Update .changeset/tidy-chefs-taste.md

* fix

* fix

* regenerate types

* improve minifiability

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Rich Harris <hello@rich-harris.dev>
pull/11162/head
Tan Li Hau 1 year ago committed by GitHub
parent 2cefd785a4
commit 8caaa375cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: reactive `URL` and `URLSearchParams` classes

@ -1,3 +1,4 @@
export { ReactiveDate as Date } from './date.js';
export { ReactiveSet as Set } from './set.js';
export { ReactiveMap as Map } from './map.js';
export { ReactiveURL as URL, ReactiveURLSearchParams as URLSearchParams } from './url.js';

@ -1,3 +1,5 @@
export const Date = globalThis.Date;
export const Set = globalThis.Set;
export const Map = globalThis.Map;
export const URL = globalThis.URL;
export const URLSearchParams = globalThis.URLSearchParams;

@ -0,0 +1,268 @@
import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js';
const REPLACE = Symbol();
export class ReactiveURL extends URL {
#protocol = source(super.protocol);
#username = source(super.username);
#password = source(super.password);
#hostname = source(super.hostname);
#port = source(super.port);
#pathname = source(super.pathname);
#hash = source(super.hash);
#searchParams = new ReactiveURLSearchParams();
/**
* @param {string | URL} url
* @param {string | URL} [base]
*/
constructor(url, base) {
url = new URL(url, base);
super(url);
this.#searchParams[REPLACE](url.searchParams);
}
get hash() {
return get(this.#hash);
}
set hash(value) {
super.hash = value;
set(this.#hash, super.hash);
}
get host() {
get(this.#hostname);
get(this.#port);
return super.host;
}
set host(value) {
super.host = value;
set(this.#hostname, super.hostname);
set(this.#port, super.port);
}
get hostname() {
return get(this.#hostname);
}
set hostname(value) {
super.hostname = value;
set(this.#hostname, super.hostname);
}
get href() {
get(this.#protocol);
get(this.#username);
get(this.#password);
get(this.#hostname);
get(this.#port);
get(this.#pathname);
get(this.#hash);
this.#searchParams.toString();
return super.href;
}
set href(value) {
super.href = value;
set(this.#protocol, super.protocol);
set(this.#username, super.username);
set(this.#password, super.password);
set(this.#hostname, super.hostname);
set(this.#port, super.port);
set(this.#pathname, super.pathname);
set(this.#hash, super.hash);
this.#searchParams[REPLACE](super.searchParams);
}
get password() {
return get(this.#password);
}
set password(value) {
super.password = value;
set(this.#password, super.password);
}
get pathname() {
return get(this.#pathname);
}
set pathname(value) {
super.pathname = value;
set(this.#pathname, super.pathname);
}
get port() {
return get(this.#port);
}
set port(value) {
super.port = value;
set(this.#port, super.port);
}
get protocol() {
return get(this.#protocol);
}
set protocol(value) {
super.protocol = value;
set(this.#protocol, super.protocol);
}
get search() {
const search = this.#searchParams?.toString();
return search ? `?${search}` : '';
}
set search(value) {
super.search = value;
this.#searchParams[REPLACE](new URLSearchParams(value.replace(/^\?/, '')));
}
get username() {
return get(this.#username);
}
set username(value) {
super.username = value;
set(this.#username, super.username);
}
get origin() {
get(this.#protocol);
get(this.#hostname);
get(this.#port);
return super.origin;
}
get searchParams() {
return this.#searchParams;
}
toString() {
return this.href;
}
toJSON() {
return this.href;
}
}
export class ReactiveURLSearchParams extends URLSearchParams {
#version = source(0);
#increment_version() {
set(this.#version, this.#version.v + 1);
}
/**
* @param {URLSearchParams} params
*/
[REPLACE](params) {
for (const key of [...super.keys()]) {
super.delete(key);
}
for (const [key, value] of params) {
super.append(key, value);
}
this.#increment_version();
}
/**
* @param {string} name
* @param {string} value
* @returns {void}
*/
append(name, value) {
this.#increment_version();
return super.append(name, value);
}
/**
* @param {string} name
* @param {string=} value
* @returns {void}
*/
delete(name, value) {
this.#increment_version();
return super.delete(name, value);
}
/**
* @param {string} name
* @returns {string|null}
*/
get(name) {
get(this.#version);
return super.get(name);
}
/**
* @param {string} name
* @returns {string[]}
*/
getAll(name) {
get(this.#version);
return super.getAll(name);
}
/**
* @param {string} name
* @param {string=} value
* @returns {boolean}
*/
has(name, value) {
get(this.#version);
return super.has(name, value);
}
keys() {
get(this.#version);
return super.keys();
}
/**
* @param {string} name
* @param {string} value
* @returns {void}
*/
set(name, value) {
this.#increment_version();
return super.set(name, value);
}
sort() {
this.#increment_version();
return super.sort();
}
toString() {
get(this.#version);
return super.toString();
}
values() {
get(this.#version);
return super.values();
}
entries() {
get(this.#version);
return super.entries();
}
[Symbol.iterator]() {
return this.entries();
}
get size() {
get(this.#version);
return super.size;
}
}

@ -0,0 +1,101 @@
import { render_effect, effect_root } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../index-client.js';
import { ReactiveURL, ReactiveURLSearchParams } from './url.js';
import { assert, test } from 'vitest';
test('url.hash', () => {
const url = new ReactiveURL('http://google.com');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(url.hash);
});
});
flushSync(() => {
url.hash = 'abc';
});
flushSync(() => {
url.href = 'http://google.com/a/b/c#def';
});
flushSync(() => {
// does not affect hash
url.pathname = 'e/f';
});
assert.deepEqual(log, ['', '#abc', '#def']);
cleanup();
});
test('url.searchParams', () => {
const url = new ReactiveURL('https://svelte.dev?foo=bar&t=123');
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push('search: ' + url.search);
});
render_effect(() => {
log.push('foo: ' + url.searchParams.get('foo'));
});
render_effect(() => {
log.push('q: ' + url.searchParams.has('q'));
});
});
flushSync(() => {
url.search = '?q=kit&foo=baz';
});
flushSync(() => {
url.searchParams.append('foo', 'qux');
});
flushSync(() => {
url.searchParams.delete('foo');
});
assert.deepEqual(log, [
'search: ?foo=bar&t=123',
'foo: bar',
'q: false',
'search: ?q=kit&foo=baz',
'foo: baz',
'q: true',
'search: ?q=kit&foo=baz&foo=qux',
'foo: baz',
'q: true',
'search: ?q=kit',
'foo: null',
'q: true'
]);
cleanup();
});
test('URLSearchParams', () => {
const params = new ReactiveURLSearchParams();
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(params.toString());
});
});
flushSync(() => {
params.set('a', 'b');
});
flushSync(() => {
params.append('a', 'c');
});
assert.deepEqual(log, ['', 'a=b', 'a=b&a=c']);
cleanup();
});

@ -0,0 +1,46 @@
import { flushSync } from '../../../../src/index-client';
import { test } from '../../test';
export default test({
html: `<div>href: https://svelte.dev/repl/hello-world?version=5.0</div><div>host: svelte.dev</div><div>pathname: /repl/hello-world</div><div>search: ?version=5.0</div><div>version: 5.0</div><div>t:</div><button>update hostname</button><button>update pathname</button><button>update search</button><button>update href</button>`,
test({ assert, target }) {
const [btn, btn2, btn3, btn4] = target.querySelectorAll('button');
flushSync(() => {
btn?.click();
});
assert.htmlEqual(
target.innerHTML,
`<div>href: https://kit.svelte.dev/repl/hello-world?version=5.0</div><div>host: kit.svelte.dev</div><div>pathname: /repl/hello-world</div><div>search: ?version=5.0</div><div>version: 5.0</div><div>t:</div><button>update hostname</button><button>update pathname</button><button>update search</button><button>update href</button>`
);
flushSync(() => {
btn2?.click();
});
assert.htmlEqual(
target.innerHTML,
`<div>href: https://kit.svelte.dev/docs/introduction?version=5.0</div><div>host: kit.svelte.dev</div><div>pathname: /docs/introduction</div><div>search: ?version=5.0</div><div>version: 5.0</div><div>t:</div><button>update hostname</button><button>update pathname</button><button>update search</button><button>update href</button>`
);
flushSync(() => {
btn3?.click();
});
assert.htmlEqual(
target.innerHTML,
`<div>href: https://kit.svelte.dev/docs/introduction?t=123</div><div>host: kit.svelte.dev</div><div>pathname: /docs/introduction</div><div>search: ?t=123</div><div>version:</div><div>t: 123</div><button>update hostname</button><button>update pathname</button><button>update search</button><button>update href</button>`
);
flushSync(() => {
btn4?.click();
});
assert.htmlEqual(
target.innerHTML,
`<div>href: https://google.com/search?version=3</div><div>host: google.com</div><div>pathname: /search</div><div>search: ?version=3</div><div>version: 3</div><div>t:</div><button>update hostname</button><button>update pathname</button><button>update search</button><button>update href</button>`
);
}
});

@ -0,0 +1,25 @@
<script>
import { URL } from 'svelte/reactivity';
let url = new URL('https://svelte.dev/repl/hello-world?version=5.0');
</script>
<div>href: {url.href}</div>
<div>host: {url.host}</div>
<div>pathname: {url.pathname}</div>
<div>search: {url.search}</div>
<div>version: {url.searchParams.get('version')}</div>
<div>t: {url.searchParams.get('t')}</div>
<button onclick={() => {
url.hostname = 'kit.svelte.dev';
}}>update hostname</button>
<button onclick={() => {
url.pathname = 'docs/introduction';
}}>update pathname</button>
<button onclick={() => {
url.search = '?t=123';
}}>update search</button>
<button onclick={() => {
url.href = 'https://google.com/search?version=3';
}}>update href</button>

@ -2008,8 +2008,18 @@ declare module 'svelte/reactivity' {
[Symbol.iterator](): IterableIterator<[K, V]>;
#private;
}
class ReactiveURL extends URL {
get searchParams(): ReactiveURLSearchParams;
#private;
}
class ReactiveURLSearchParams extends URLSearchParams {
[REPLACE](params: URLSearchParams): void;
#private;
}
const REPLACE: unique symbol;
export { ReactiveDate as Date, ReactiveSet as Set, ReactiveMap as Map };
export { ReactiveDate as Date, ReactiveSet as Set, ReactiveMap as Map, ReactiveURL as URL, ReactiveURLSearchParams as URLSearchParams };
}
declare module 'svelte/server' {

Loading…
Cancel
Save