URL+ extends the web’s URL and URLSearchParams primitives with reactivity and a real object model.
npm i @webqit/url-plusimport { URLPlus, URLSearchParamsPlus, Observer } from '@webqit/url-plus';<script src="https://unpkg.com/@webqit/url-plus/dist/main.js"></script>
<script>
const { URLPlus, URLSearchParamsPlus, Observer } = window.webqit;
</script>At its core, URL+ provides two things:
- Reactive URLs — a
URLwhose fields can be observed for changes - Object‑backed query parameters —
searchParamswith a real object model behind it
All of this builds directly on the native URL and URLSearchParams semantics.
For example, URL+ works like the native URL and URLSearchParams by default:
const url = new URLPlus('https://example.com/level1/level2?foo=bar');
console.log(url.pathname); // '/level1/level2'
console.log(url.search); // '?foo=bar'
console.log(url.searchParams.get('foo')); // 'bar'But it lets you do more:
// A new ".ancestorPathname" field
console.log(url.ancestorPathname); // '/level1'
// A new ".query" field – an object model of url's query params
console.log(url.query); // { foo: 'bar' }
// Observability
Observer.observe(url, 'href', (mutation) => {
console.log(mutation.value); // 'https://example.com/level1/level2/level3?foo=bar'
});
url.pathname = '/level1/level2/level3';
// Deep, fine-grained observation
Observer.observe(url.query, 'foo', (mutation) => {
console.log(mutation.value); // 'bar'
});
url.searchParams.set('foo', 'baz');URL+'s capabilities start with URLSearchParamsPlus.
URLSearchParamsPlus extends the standard URLSearchParams interface with a persistent object model and deterministic synchronization between that object tree and the serialized query string.
Internally, parameters are always represented as structured data. This internal representation exists regardless of how the instance is configured or mutated.
new URLSearchParamsPlus(init?, options?)initmay be a query string, iterable, plain object, or another params instance
optionscontrols compatibility mode and serialization behavior
// Strings
const params1 = new URLSearchParamsPlus('a=1&b=2');
// Iterables
const params2 = new URLSearchParamsPlus([['a', 1], ['b', 2]]);
// Object
const params3 = new URLSearchParamsPlus({ a: 1, b: 2 });
// Other params
const params4 = new URLSearchParamsPlus(new URLSearchParams('a=1&b=2'));
const params5 = new URLSearchParamsPlus(new URLSearchParamsPlus('a=1&b=2'));Every URLSearchParamsPlus instance maintains a live object tree representing the semantic structure of the query.
This tree can be accessed via:
const tree = params.json();const params = new URLSearchParamsPlus('a=1&b=2');
const tree = params.json();
console.log(tree); // { a: 1, b: 2 }This tree is returned by reference and is mutable.
This tree is the authoritative source of truth for the query string. Mutations to the tree are reflected in the query string:
tree.c = 3;
params.toString(); // 'a=1&b=2&c=3'
delete tree.a;
params.toString(); // 'b=2&c=3'The instance mutation APIs all converge on the tree:
params.set('d', 4);
console.log(tree); // { b: 2, c: 3, d: 4 }The tree is fully observable across all modes of mutation:
Observer.observe(tree, (mutations) => {
console.log(mutations[0].key, mutations[0].value);
});
// Reactive mutation via instance API
params.set('e', 5); // Console: "e", 5
// Reactive mutation via the Observer API
Observer.set(tree, 'e', 6); // Console: "e", 6URLSearchParamsPlus lets you address this model deeply using paths:
const params = new URLSearchParamsPlus();
const tree = params.json();
params.set('a[b][c]', 1);
console.log(tree); // { a: { b: { c: 1 } } }
tree.a.b.c = 2;
console.log(params.toString()); // 'a%5Bb%5D%5Bc%5D=2'
console.log(params.stringify({ prettyPrint: true })); // 'a[b][c]=2'const params = new URLSearchParamsPlus('a[b][c]=1');
const tree = params.json();
console.log(tree); // { a: { b: { c: 1 } } }By comparing, the URLSearchParams API does accept the bracket notation on key names but not with
any specific semantics attached. It's just a string.
URLSearchParamsPlus lets that address the underlying object model, while aligning with the
surface behavior of the URLSearchParams API:
Traversal is by literal string identifiers, not by path:
const params1 = new URLSearchParams('a[b][]=1&a[b][]=2');
const params2 = new URLSearchParamsPlus('a[b][]=1&a[b][]=2');
// Keys are just strings that must match strictly
console.log(params1.get('a[b][]')); // '1'
console.log(params2.get('a[b][]')); // '1'
console.log(params1.getAll('a[b][]')); // ['1', '2']
console.log(params2.getAll('a[b][]')); // ['1', '2']
// ...not interpreted structurally
console.log(params1.get('a[b]')); // null
console.log(params2.get('a[b]')); // null
console.log(params1.get('a[b][0]')); // null
console.log(params2.get('a[b][0]')); // null
console.log(params1.getAll('a[b]')); // []
console.log(params2.getAll('a[b]')); // []Enumeration and stringification expose exact strings as set:
// Enumaration
console.log([...params1.keys()]); // ['a[b][]', 'a[b][]']
console.log([...params2.keys()]); // ['a[b][]', 'a[b][]']
console.log([...params1.entries()]); // [['a[b][]', '1'], ['a[b][]', '2']]
console.log([...params2.entries()]); // [['a[b][]', '1'], ['a[b][]', '2']]
// Stringification
console.log(params1.toString()); // 'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=2'
console.log(params2.toString()); // 'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=2'
console.log(params2.stringify({ prettyPrint: true })); // 'a[b][]=1&a[b][]=2'But this alignment with the URLSearchParams API is only one of two modes with the URLSearchParamsPlus API – and the default. URLSearchParamsPlus lets you opt out of this
"compatibility" with URLSearchParams into full structural mode:
const params = new URLSearchParamsPlus(null, { compatMode: false });In this mode, URLSearchParamsPlus exposes its internal tree to its traversal APIs – not just its mutation APIs:
Query keys are interpreted as paths into that tree:
const params2 = new URLSearchParamsPlus('a[b][]=1&a[b][]=2', { compatMode: false });
// Keys are interpreted structurally
console.log(params2.get('a[b][]')); // 1
console.log(params2.get('a[b][0]')); // 1
console.log(params2.get('a[b][1]')); // 2
// Traverse in and out the structure
console.log(params2.get('a[b]')); // [1, 2]
console.log(params2.get('a')); // URLSearchParamsPlus { b: [1, 2] }
// Traverse in and out programmatically
console.log(params2.get('a').get('b')); // [1, 2]
// Mutate by reference
console.log(params2.get('a').get('b').pop()); // 2
console.log(params2.get('a[b]')); // [1]Enumeration and stringification expose fully-qualified paths:
// Enumaration
console.log([...params2.keys()]); // ['a[b][0]', 'a[b][1]']
console.log([...params2.entries()]); // [['a[b][0]', 1], ['a[b][1]', 2]]
// Stringification
console.log(params2.toString()); // 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B1%5D=2'
console.log(params2.stringify({ prettyPrint: true })); // 'a[b][0]=1&a[b][1]=2'The tree can be observed to any depth:
// Observe a key
Observer.observe(tree, 'a', (mutation) => {
console.log(mutation.key, mutation.value);
});
// Observe 2-levels deep
Observer.observe(tree, Observer.path('a', 'b'), (mutation) => {
console.log(mutation.path, mutation.key, mutation.value);
});
// Observe full depth
Observer.observe(tree, Observer.subtree(), (mutations) => {
console.log(mutations.map((m) => m.path, m.key, m.value));
});
// Reactive mutation via instance API
params.set('a[b][c]', 5);
// Reactive mutation via the Observer API
Observer.set(tree.a.b, 'c', 6);In advanced mode, values retain their actual types in the tree.
For strings passed to the constuctor for hydration, numeric values are sensibly cast to numbers during parsing:
const params = new URLSearchParamsPlus('a=39', { compatMode: false });
params.json().a; // 39Programmatic sets preserve exact value types:
params.set('x', 39);
params.set('y', '39');
params.json().x; // 39
params.json().y; // '39'| Aspect | Default Mode (compatMode: true) |
Advanced Mode (compatMode: false) |
|---|---|---|
| Key Interpretation | Literal strings | Structural paths |
| Traversal | By literal strings | By paths |
| Enumeration | By literal strings | By fully-qualified paths |
| Stringification | By literal strings | By fully-qualified paths |
| Value Semantics | Always strings | Actual types as set |
Default mode provides exact URLSearchParams behavior.
By default, bracket characters are percent‑encoded to match native behavior.
const params = new URLSearchParamsPlus('a[b][0]=1');
params.toString(); // a%5Bb%5D%5B0%5D=1For readability, this can be disabled:
const params = new URLSearchParamsPlus(null, { prettyPrint: true });params.toString(); // a[b][0]=1
params.stringify({ prettyPrint: false }); // a%5Bb%5D%5B0%5D=1toString()always returns the canonical, spec-aligned representation.stringify()allows formatting control.- The constructor lets you set a default for
prettyPrint.
URLPlus is a reactive extension of the standard URL interface.
new URLPlus(input, base?, options?)inputmay be a string or another URL instancebasemay be a string or another URL instanceoptionscontrols compatibility mode and serialization behavior
const url = new URLPlus('https://example.com/a/b?x=1');url.protocol; // 'https:'
url.username; // ''
url.password; // ''
url.hostname; // 'example.com'
url.port; // ''
url.host; // 'example.com'
url.origin; // 'https://example.com'
url.pathname; // '/a/b'
url.ancestorPathname; // '/a'
url.searchParams; // URLSearchParamsPlus { x: 1 }
url.query; // { x: 1 }
url.search; // '?x=1'
url.hash; // ''
url.href; // 'https://example.com/a/b?x=1'Mutating one field updates the others:
url.pathname = '/a/b/c';
console.log(url.href); // 'https://example.com/a/b/c?x=1'
console.log(url.ancestorPathname); // '/a/b'url.href = 'https://example.com/x/y?x=2';
console.log(url.pathname); // '/x/y'
console.log(url.ancestorPathname); // '/x'
console.log(url.searchParams); // URLSearchParamsPlus { x: 2 }
console.log(url.query); // { x: 2 }
console.log(url.search); // '?x=2'Each URL field can be observed via the Observer API.
Observer.observe(url, 'href', mutation => {
console.log('href →', mutation.value);
});
url.pathname = '/p/q';
// href → https://example.com/p/q?x=1Observation works symmetrically:
Observer.observe(url, 'pathname', mutation => {
console.log('pathname →', mutation.value);
});
url.href = 'https://example.com/m/n?x=1';
// Console: pathname → /m/nObservers react to the resulting state in each case.
The Search Params (.searchParams) is backed by URLSearchParamsPlus:
url.searchParams instanceof URLSearchParamsPlus; // trueThe special .query field is a direct reference to the underlying object model of the search params:
url.query === url.searchParams.json(); // trueconsole.log(url.query); // { x: 1 }This object is live.
Mutating it updates the Search Params, and therefore, the URL:
url.query.a = { b: [1, 2] };
console.log(url.search); // '?a[b][0]=1&a[b][1]=2'
console.log(url.href); // 'https://example.com/a/b?a[b][0]=1&a[b][1]=2'As with URLSearchParamsPlus, operations over searchParams converge on the same underlying model:
url.searchParams.append('a[b][]', 3);
console.log(url.query); // { a: { b: [1, 2, 3] } }Updates to search and href also converge on the same underlying model:
url.search = '?a[b][0]=10';
console.log(url.query); // { a: { b: [10] } }url.href = 'https://example.com/?x[y][z]=9';
console.log(url.query); // { x: { y: { z: 9 } } }All mutation paths converge on the same underlying state.
Because the query object is part of the URL’s state, deep observers work across all mutation paths.
Observer.observe(url, Observer.subtree(), mutations => {
console.log(
mutations.map((m) => [m.path, m.key, m.value])
);
});The above will react to changes to any part of the URL's state:
url.searchParams.set('a[b][0]', 20);
url.search = '?a[b][1]=30';
url.href = 'https://example.com/?a[b][2]=40';
// Reactive array mutation via Observer.proxy()
Observer.proxy(url.query.a.b).push(4);URLPlus options object can be used to configure the compatibility mode and serialization behavior of its search params.
const url = new URLPlus('https://example.com?a[b]=1', null, {
compatMode: false,
prettyPrint: true
});
console.log(url.searchParams.toString()); // a[b]=1
console.log(url.searchParams.stringify({ prettyPrint: false })); // a%5Bb%5D=1
console.log(url.stringify({ prettyPrint: false })); // https://example.com?a%5Bb%5D=1MIT