Skip to content

Commit 86a0bf8

Browse files
feat: Add separate entry point for a Navigation API-driven router (#139)
* chore: Add nav types as a devDep * feat: Support the navigation API * fix: You'd think I'd learn to stop renaming right before pushing * refactor: Clean up * docs: Update ReadMe * chore: Sync double decode fix Co-authored-by: Jovi De Croock <decroockjovi@gmail.com> --------- Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>
1 parent 3966f58 commit 86a0bf8

7 files changed

Lines changed: 1539 additions & 5 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ Isomorphic async tools for Preact.
2626
- [hydrate()](#hydrate)
2727
- [prerender()](#prerender)
2828
- [locationStub()](#locationstub)
29+
- [Navigation API Entry Docs](#navigation-api-entry-docs)
30+
- [Differences in usage](#differences-in-usage)
2931

3032
---
3133

@@ -438,6 +440,18 @@ locationStub('/foo/bar?baz=qux#quux');
438440
console.log(location.pathname); // "/foo/bar"
439441
```
440442

443+
## Navigation API Entry Docs
444+
445+
The Navigation API is a new web standard that provides an updated method of handling "navigation" in web applications, supporting SPA-style routing as a first-class citizen. The older History API can be wrangled to support this and has been the standard for many years, but the Navigation API provides a much more robust set of tools that are really, really attractive for routers like `preact-iso` to take advantage of.
446+
447+
Whilst the API [sees fairly wide support](https://caniuse.com/wf-navigation), it is still newly available and thus may not be viable for some targets. As such, we've provided a new entry point that will allow you to take advantage of this API if you wish, but the default remains targetting the History API. The Navigation API entry point is available at `preact-iso/router/navigation-api`.
448+
449+
### Differences in usage
450+
451+
The differences lie entirely within the [`useLocation()`](#uselocation) hook: instead of returning a `route()` function, you use the global `navigation` object to perform all navigations.
452+
453+
The [`navigation` object](https://developer.mozilla.org/en-US/docs/Web/API/Navigation) contains many of the useful utilities that go along with a router, like `.forward()`, `.back()`, `.canGoForward()`, `.canGoBack()`, `.entries()`, etc. It actually offers far more utilities than the base router did, and does so with less library code overall, so if you have access to it, it's a really nice upgrade.
454+
441455
## License
442456

443457
[MIT](./LICENSE)

package-lock.json

Lines changed: 9 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"exports": {
99
".": "./src/index.js",
1010
"./router": "./src/router.js",
11+
"./router/navigation-api": "./src/router-navigation-api.js",
1112
"./lazy": "./src/lazy.js",
1213
"./prerender": "./src/prerender.js",
1314
"./hydrate": "./src/hydrate.js"
@@ -44,6 +45,7 @@
4445
"chai": "^5.1.1",
4546
"htm": "^3.1.1",
4647
"kleur": "^4.1.5",
48+
"navigation-api-types": "^0.6.1",
4749
"preact": "^10.26.5",
4850
"preact-render-to-string": "^6.6.1",
4951
"sinon": "^18.0.0",

src/internal.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference types="navigation-api-types" />
12
import { Component } from 'preact';
23

34
export interface AugmentedComponent extends Component<any, any> {

src/router-navigation-api.d.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { AnyComponent, ComponentChildren, Context, VNode } from 'preact';
2+
3+
export const LocationProvider: {
4+
(props: { scope?: string | RegExp; children?: ComponentChildren; }): VNode;
5+
ctx: Context<LocationHook>;
6+
};
7+
8+
type NestedArray<T> = Array<T | NestedArray<T>>;
9+
10+
interface KnownProps {
11+
path: string;
12+
query: Record<string, string>;
13+
params: Record<string, string>;
14+
default?: boolean;
15+
rest?: string;
16+
component?: AnyComponent;
17+
}
18+
19+
interface ArbitraryProps {
20+
[prop: string]: any;
21+
}
22+
23+
type MatchProps = KnownProps & ArbitraryProps;
24+
25+
/**
26+
* Check if a URL path matches against a URL path pattern.
27+
*
28+
* Warning: This is largely an internal API, it may change in the future
29+
* @param url - URL path (e.g. /user/12345)
30+
* @param route - URL pattern (e.g. /user/:id)
31+
*/
32+
export function exec(url: string, route: string, matches?: MatchProps): MatchProps
33+
34+
export function Router(props: {
35+
onRouteChange?: (url: string) => void;
36+
onLoadEnd?: (url: string) => void;
37+
onLoadStart?: (url: string) => void;
38+
children?: NestedArray<VNode>;
39+
}): VNode;
40+
41+
interface LocationHook {
42+
url: string;
43+
path: string;
44+
query: Record<string, string>;
45+
}
46+
export const useLocation: () => LocationHook;
47+
48+
interface RouteHook {
49+
path: string;
50+
query: Record<string, string>;
51+
params: Record<string, string>;
52+
}
53+
export const useRoute: () => RouteHook;
54+
55+
type RoutableProps =
56+
| { path: string; default?: false; }
57+
| { path?: never; default: true; }
58+
59+
export type RouteProps<Props> = RoutableProps & { component: AnyComponent<Props> };
60+
61+
export type RoutePropsForPath<Path extends string> = Path extends '*'
62+
? { params: {}; rest: string }
63+
64+
: Path extends `:${infer placeholder}?/${infer rest}`
65+
? { [k in placeholder]?: string } & { params: RoutePropsForPath<rest>['params'] & { [k in placeholder]?: string } } & Omit<RoutePropsForPath<rest>, 'params'>
66+
67+
: Path extends `:${infer placeholder}/${infer rest}`
68+
? { [k in placeholder]: string } & { params: RoutePropsForPath<rest>['params'] & { [k in placeholder]: string } } & Omit<RoutePropsForPath<rest>, 'params'>
69+
70+
: Path extends `:${infer placeholder}?`
71+
? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } }
72+
73+
: Path extends `:${infer placeholder}*`
74+
? { [k in placeholder]?: string } & { params: { [k in placeholder]?: string } }
75+
76+
: Path extends `:${infer placeholder}+`
77+
? { [k in placeholder]: string } & { params: { [k in placeholder]: string } }
78+
79+
: Path extends `:${infer placeholder}`
80+
? { [k in placeholder]: string } & { params: { [k in placeholder]: string } }
81+
82+
: Path extends (`/${infer rest}` | `${infer _}/${infer rest}`)
83+
? RoutePropsForPath<rest>
84+
85+
: { params: {} };
86+
87+
export function Route<Props>(props: RouteProps<Props> & Partial<Props>): VNode;
88+
89+
declare module 'preact' {
90+
// The code below automatically adds `path` and `default` as optional props for every component
91+
// (effectively reserving those names, so no component should use those names in its own props).
92+
// These declarations extend from `RouteableProps`, which is not allowed in modern TypeScript and
93+
// causes a TS2312 error. However, the compiler does seems to honor the intent of this code, so
94+
// to avoid an API regression, let's ignore the error rather than loosening the type validation.
95+
namespace JSX {
96+
/** @ts-ignore */
97+
interface IntrinsicAttributes extends RoutableProps {}
98+
}
99+
/** @ts-ignore */
100+
interface Attributes extends RoutableProps {}
101+
}

0 commit comments

Comments
 (0)