Skip to content

Commit 8caaa37

Browse files
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 <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 2cefd78 commit 8caaa37

File tree

8 files changed

+459
-1
lines changed

8 files changed

+459
-1
lines changed

.changeset/tidy-chefs-taste.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
feat: reactive `URL` and `URLSearchParams` classes
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { ReactiveDate as Date } from './date.js';
22
export { ReactiveSet as Set } from './set.js';
33
export { ReactiveMap as Map } from './map.js';
4+
export { ReactiveURL as URL, ReactiveURLSearchParams as URLSearchParams } from './url.js';
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export const Date = globalThis.Date;
22
export const Set = globalThis.Set;
33
export const Map = globalThis.Map;
4+
export const URL = globalThis.URL;
5+
export const URLSearchParams = globalThis.URLSearchParams;

packages/svelte/src/reactivity/url.js

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import { source, set } from '../internal/client/reactivity/sources.js';
2+
import { get } from '../internal/client/runtime.js';
3+
4+
const REPLACE = Symbol();
5+
6+
export class ReactiveURL extends URL {
7+
#protocol = source(super.protocol);
8+
#username = source(super.username);
9+
#password = source(super.password);
10+
#hostname = source(super.hostname);
11+
#port = source(super.port);
12+
#pathname = source(super.pathname);
13+
#hash = source(super.hash);
14+
#searchParams = new ReactiveURLSearchParams();
15+
16+
/**
17+
* @param {string | URL} url
18+
* @param {string | URL} [base]
19+
*/
20+
constructor(url, base) {
21+
url = new URL(url, base);
22+
super(url);
23+
this.#searchParams[REPLACE](url.searchParams);
24+
}
25+
26+
get hash() {
27+
return get(this.#hash);
28+
}
29+
30+
set hash(value) {
31+
super.hash = value;
32+
set(this.#hash, super.hash);
33+
}
34+
35+
get host() {
36+
get(this.#hostname);
37+
get(this.#port);
38+
return super.host;
39+
}
40+
41+
set host(value) {
42+
super.host = value;
43+
set(this.#hostname, super.hostname);
44+
set(this.#port, super.port);
45+
}
46+
47+
get hostname() {
48+
return get(this.#hostname);
49+
}
50+
51+
set hostname(value) {
52+
super.hostname = value;
53+
set(this.#hostname, super.hostname);
54+
}
55+
56+
get href() {
57+
get(this.#protocol);
58+
get(this.#username);
59+
get(this.#password);
60+
get(this.#hostname);
61+
get(this.#port);
62+
get(this.#pathname);
63+
get(this.#hash);
64+
this.#searchParams.toString();
65+
return super.href;
66+
}
67+
68+
set href(value) {
69+
super.href = value;
70+
set(this.#protocol, super.protocol);
71+
set(this.#username, super.username);
72+
set(this.#password, super.password);
73+
set(this.#hostname, super.hostname);
74+
set(this.#port, super.port);
75+
set(this.#pathname, super.pathname);
76+
set(this.#hash, super.hash);
77+
this.#searchParams[REPLACE](super.searchParams);
78+
}
79+
80+
get password() {
81+
return get(this.#password);
82+
}
83+
84+
set password(value) {
85+
super.password = value;
86+
set(this.#password, super.password);
87+
}
88+
89+
get pathname() {
90+
return get(this.#pathname);
91+
}
92+
93+
set pathname(value) {
94+
super.pathname = value;
95+
set(this.#pathname, super.pathname);
96+
}
97+
98+
get port() {
99+
return get(this.#port);
100+
}
101+
102+
set port(value) {
103+
super.port = value;
104+
set(this.#port, super.port);
105+
}
106+
107+
get protocol() {
108+
return get(this.#protocol);
109+
}
110+
111+
set protocol(value) {
112+
super.protocol = value;
113+
set(this.#protocol, super.protocol);
114+
}
115+
116+
get search() {
117+
const search = this.#searchParams?.toString();
118+
return search ? `?${search}` : '';
119+
}
120+
121+
set search(value) {
122+
super.search = value;
123+
this.#searchParams[REPLACE](new URLSearchParams(value.replace(/^\?/, '')));
124+
}
125+
126+
get username() {
127+
return get(this.#username);
128+
}
129+
130+
set username(value) {
131+
super.username = value;
132+
set(this.#username, super.username);
133+
}
134+
135+
get origin() {
136+
get(this.#protocol);
137+
get(this.#hostname);
138+
get(this.#port);
139+
return super.origin;
140+
}
141+
142+
get searchParams() {
143+
return this.#searchParams;
144+
}
145+
146+
toString() {
147+
return this.href;
148+
}
149+
150+
toJSON() {
151+
return this.href;
152+
}
153+
}
154+
155+
export class ReactiveURLSearchParams extends URLSearchParams {
156+
#version = source(0);
157+
158+
#increment_version() {
159+
set(this.#version, this.#version.v + 1);
160+
}
161+
162+
/**
163+
* @param {URLSearchParams} params
164+
*/
165+
[REPLACE](params) {
166+
for (const key of [...super.keys()]) {
167+
super.delete(key);
168+
}
169+
170+
for (const [key, value] of params) {
171+
super.append(key, value);
172+
}
173+
174+
this.#increment_version();
175+
}
176+
177+
/**
178+
* @param {string} name
179+
* @param {string} value
180+
* @returns {void}
181+
*/
182+
append(name, value) {
183+
this.#increment_version();
184+
return super.append(name, value);
185+
}
186+
187+
/**
188+
* @param {string} name
189+
* @param {string=} value
190+
* @returns {void}
191+
*/
192+
delete(name, value) {
193+
this.#increment_version();
194+
return super.delete(name, value);
195+
}
196+
197+
/**
198+
* @param {string} name
199+
* @returns {string|null}
200+
*/
201+
get(name) {
202+
get(this.#version);
203+
return super.get(name);
204+
}
205+
206+
/**
207+
* @param {string} name
208+
* @returns {string[]}
209+
*/
210+
getAll(name) {
211+
get(this.#version);
212+
return super.getAll(name);
213+
}
214+
215+
/**
216+
* @param {string} name
217+
* @param {string=} value
218+
* @returns {boolean}
219+
*/
220+
has(name, value) {
221+
get(this.#version);
222+
return super.has(name, value);
223+
}
224+
225+
keys() {
226+
get(this.#version);
227+
return super.keys();
228+
}
229+
230+
/**
231+
* @param {string} name
232+
* @param {string} value
233+
* @returns {void}
234+
*/
235+
set(name, value) {
236+
this.#increment_version();
237+
return super.set(name, value);
238+
}
239+
240+
sort() {
241+
this.#increment_version();
242+
return super.sort();
243+
}
244+
245+
toString() {
246+
get(this.#version);
247+
return super.toString();
248+
}
249+
250+
values() {
251+
get(this.#version);
252+
return super.values();
253+
}
254+
255+
entries() {
256+
get(this.#version);
257+
return super.entries();
258+
}
259+
260+
[Symbol.iterator]() {
261+
return this.entries();
262+
}
263+
264+
get size() {
265+
get(this.#version);
266+
return super.size;
267+
}
268+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { render_effect, effect_root } from '../internal/client/reactivity/effects.js';
2+
import { flushSync } from '../index-client.js';
3+
import { ReactiveURL, ReactiveURLSearchParams } from './url.js';
4+
import { assert, test } from 'vitest';
5+
6+
test('url.hash', () => {
7+
const url = new ReactiveURL('http://google.com');
8+
const log: any = [];
9+
10+
const cleanup = effect_root(() => {
11+
render_effect(() => {
12+
log.push(url.hash);
13+
});
14+
});
15+
16+
flushSync(() => {
17+
url.hash = 'abc';
18+
});
19+
20+
flushSync(() => {
21+
url.href = 'http://google.com/a/b/c#def';
22+
});
23+
24+
flushSync(() => {
25+
// does not affect hash
26+
url.pathname = 'e/f';
27+
});
28+
29+
assert.deepEqual(log, ['', '#abc', '#def']);
30+
31+
cleanup();
32+
});
33+
34+
test('url.searchParams', () => {
35+
const url = new ReactiveURL('https://svelte.dev?foo=bar&t=123');
36+
const log: any = [];
37+
38+
const cleanup = effect_root(() => {
39+
render_effect(() => {
40+
log.push('search: ' + url.search);
41+
});
42+
render_effect(() => {
43+
log.push('foo: ' + url.searchParams.get('foo'));
44+
});
45+
render_effect(() => {
46+
log.push('q: ' + url.searchParams.has('q'));
47+
});
48+
});
49+
50+
flushSync(() => {
51+
url.search = '?q=kit&foo=baz';
52+
});
53+
54+
flushSync(() => {
55+
url.searchParams.append('foo', 'qux');
56+
});
57+
58+
flushSync(() => {
59+
url.searchParams.delete('foo');
60+
});
61+
62+
assert.deepEqual(log, [
63+
'search: ?foo=bar&t=123',
64+
'foo: bar',
65+
'q: false',
66+
'search: ?q=kit&foo=baz',
67+
'foo: baz',
68+
'q: true',
69+
'search: ?q=kit&foo=baz&foo=qux',
70+
'foo: baz',
71+
'q: true',
72+
'search: ?q=kit',
73+
'foo: null',
74+
'q: true'
75+
]);
76+
77+
cleanup();
78+
});
79+
80+
test('URLSearchParams', () => {
81+
const params = new ReactiveURLSearchParams();
82+
const log: any = [];
83+
84+
const cleanup = effect_root(() => {
85+
render_effect(() => {
86+
log.push(params.toString());
87+
});
88+
});
89+
90+
flushSync(() => {
91+
params.set('a', 'b');
92+
});
93+
94+
flushSync(() => {
95+
params.append('a', 'c');
96+
});
97+
98+
assert.deepEqual(log, ['', 'a=b', 'a=b&a=c']);
99+
100+
cleanup();
101+
});

0 commit comments

Comments
 (0)