Skip to content

Commit 4d23a6a

Browse files
committed
feat: reactive url
1 parent d5776c3 commit 4d23a6a

File tree

7 files changed

+444
-0
lines changed

7 files changed

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

0 commit comments

Comments
 (0)