@webqit/url-plus
v0.1.3
Published
Upgraded URL API
Downloads
375
Maintainers
Readme
URL+ — Reactive URLs with Object‑Backed Query Parameters
URL+ extends the web’s URL and URLSearchParams primitives with reactivity and a real object model.
Install
npm i @webqit/url-plusimport { URLPlus, URLSearchParamsPlus, Observer } from '@webqit/url-plus';CDN Include
<script src="https://unpkg.com/@webqit/url-plus/dist/main.js"></script>
<script>
const { URLPlus, URLSearchParamsPlus, Observer } = window.webqit;
</script>Overview
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
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.
Construction
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'));The Internal Object Tree
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.
Mutating the Tree
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 }Observing the Tree
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", 6Addressing the Tree
URLSearchParamsPlus 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'Observing the Tree Deeply
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);Value Semantics
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'The Default vs Advanced Mode Comparison
| 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.
Serialization Options
Bracket Encoding
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
URLPlus is a reactive extension of the standard URL interface.
Construction
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'The Existing Update Model
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'...With Observability
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.
Query Parameters
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.
Observing the Full Structure
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);Mode Switch and Serialization Options
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=1License
MIT
