@llui/router 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,107 @@
1
1
  # @llui/router
2
2
 
3
- Router for [LLui](https://github.com/fponticelli/llui).
4
-
5
- Structured path matching, history/hash mode, `routing.link()` helper, `routing.listener()` for popstate, and `createHandler()` for mergeHandlers integration.
3
+ Router for [LLui](https://github.com/fponticelli/llui). Structured path matching with history and hash mode support.
6
4
 
7
5
  ```bash
8
6
  pnpm add @llui/router
9
7
  ```
10
8
 
9
+ ## Usage
10
+
11
+ ```ts
12
+ import { route, param, rest, createRouter, connectRouter } from '@llui/router'
13
+ import { div, a } from '@llui/dom'
14
+
15
+ // Define routes
16
+ const home = route([])
17
+ const search = route(['search'], (b) => b, ['q', 'page'])
18
+ const detail = route(['item', param('id')])
19
+ const docs = route(['docs', rest('path')])
20
+
21
+ // Create router
22
+ const router = createRouter({ home, search, detail, docs }, { mode: 'history' })
23
+
24
+ // Connect to effects system
25
+ const routing = connectRouter(router)
26
+ ```
27
+
28
+ ## API
29
+
30
+ ### Route Definition
31
+
32
+ | Function | Description |
33
+ | --------------------------------------- | --------------------------------------------------------- |
34
+ | `route(segments, builder?, queryKeys?)` | Define a route with path segments and optional query keys |
35
+ | `param(name)` | Named path parameter (e.g. `/item/:id`) |
36
+ | `rest(name)` | Rest parameter capturing remaining path |
37
+
38
+ ### Router
39
+
40
+ | Function | Description |
41
+ | ------------------------------ | ------------------------------------------------------- |
42
+ | `createRouter(routes, config)` | Create router instance (`history` or `hash` mode) |
43
+ | `connectRouter(router)` | Connect router to LLui effects, returns routing helpers |
44
+
45
+ ### Routing Helpers (from connectRouter)
46
+
47
+ | Method / Effect | Description |
48
+ | ------------------------------------- | ------------------------------------------------------------- |
49
+ | `.link(send, route, attrs, children)` | Render a navigation link with client-side routing |
50
+ | `.listener(send)` | Popstate listener -- call in `view()` to react to URL changes |
51
+ | `.handleEffect` | Effect handler plugin for navigate/push/replace effects |
52
+ | `.push(route)` | Push navigation effect |
53
+ | `.replace(route)` | Replace navigation effect |
54
+ | `.back()` | Navigate back effect |
55
+ | `.forward()` | Navigate forward effect |
56
+ | `.scroll()` | Scroll restoration effect |
57
+
58
+ ## Guards
59
+
60
+ Router guards let you block or redirect navigation. Pass `beforeEnter` and/or `beforeLeave` to `connectRouter`:
61
+
62
+ ```ts
63
+ const routing = connectRouter(router, {
64
+ // Called before entering a new route
65
+ beforeEnter(to, from) {
66
+ // Return void → allow
67
+ // Return false → block
68
+ // Return Route → redirect
69
+ },
70
+ // Called before leaving the current route
71
+ beforeLeave(from, to) {
72
+ // Return true → allow
73
+ // Return false → block
74
+ },
75
+ })
76
+ ```
77
+
78
+ Guards run in the effect handler and the popstate listener, keeping `update()` pure.
79
+
80
+ ### Auth guard
81
+
82
+ ```ts
83
+ const routing = connectRouter(router, {
84
+ beforeEnter(to) {
85
+ if (to.page === 'admin' && !isLoggedIn()) {
86
+ return { page: 'login' }
87
+ }
88
+ },
89
+ })
90
+ ```
91
+
92
+ ### Unsaved changes guard
93
+
94
+ ```ts
95
+ const routing = connectRouter(router, {
96
+ beforeLeave(from) {
97
+ if (from.page === 'editor' && hasUnsavedChanges()) {
98
+ return confirm('Discard unsaved changes?')
99
+ }
100
+ return true
101
+ },
102
+ })
103
+ ```
104
+
11
105
  ## License
12
106
 
13
107
  MIT
package/dist/connect.d.ts CHANGED
@@ -6,6 +6,21 @@ export interface RouterEffect {
6
6
  x?: number;
7
7
  y?: number;
8
8
  }
9
+ export interface ConnectOptions<R> {
10
+ /**
11
+ * Called before entering a new route. Return:
12
+ * - `void` / `undefined` → allow navigation
13
+ * - `false` → block navigation (stay on current route)
14
+ * - a different `Route` → redirect to that route
15
+ */
16
+ beforeEnter?: (to: R, from: R | null) => R | false | void;
17
+ /**
18
+ * Called before leaving the current route. Return:
19
+ * - `true` → allow navigation
20
+ * - `false` → block (e.g. unsaved changes prompt)
21
+ */
22
+ beforeLeave?: (from: R, to: R) => boolean;
23
+ }
9
24
  export interface ConnectedRouter<R> {
10
25
  /** Effect: push a new route onto history */
11
26
  push(route: R): RouterEffect;
@@ -50,5 +65,5 @@ export interface ConnectedRouter<R> {
50
65
  onNavigate: (state: S, route: R) => [S, E[]];
51
66
  }): (state: S, msg: M) => [S, E[]] | null;
52
67
  }
53
- export declare function connectRouter<R>(router: Router<R>): ConnectedRouter<R>;
68
+ export declare function connectRouter<R>(router: Router<R>, options?: ConnectOptions<R>): ConnectedRouter<R>;
54
69
  //# sourceMappingURL=connect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAKrC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAA;IAChB,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAA;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,CAAC,CAAC,EAAE,MAAM,CAAA;CACX;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,4CAA4C;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,YAAY,CAAA;IAC5B,4CAA4C;IAC5C,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,YAAY,CAAA;IAC/B,sBAAsB;IACtB,IAAI,IAAI,YAAY,CAAA;IACpB,yBAAyB;IACzB,OAAO,IAAI,YAAY,CAAA;IACvB,iCAAiC;IACjC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;IAE1C,8DAA8D;IAC9D,YAAY,EAAE,CAAC,GAAG,EAAE;QAAE,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAA;IAEhG;;;OAGG;IACH,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,CAAA;IAEzE;;;OAGG;IACH,IAAI,CAAC,CAAC,EACJ,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EACtB,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,QAAQ,EAAE,IAAI,EAAE,EAChB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAC3B,WAAW,CAAA;IAEd;;;OAGG;IACH,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE;QAC7B,mDAAmD;QACnD,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,iCAAiC;QACjC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAA;QACvB,oCAAoC;QACpC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;QACjC,8CAA8C;QAC9C,UAAU,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;KAC7C,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,CAAA;CAC1C;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,CAgHtE"}
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAKrC,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAA;IAChB,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAA;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,CAAC,CAAC,EAAE,MAAM,CAAA;CACX;AAED,MAAM,WAAW,cAAc,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG,IAAI,CAAA;IACzD;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,OAAO,CAAA;CAC1C;AAED,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,4CAA4C;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,YAAY,CAAA;IAC5B,4CAA4C;IAC5C,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,YAAY,CAAA;IAC/B,sBAAsB;IACtB,IAAI,IAAI,YAAY,CAAA;IACpB,yBAAyB;IACzB,OAAO,IAAI,YAAY,CAAA;IACvB,iCAAiC;IACjC,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;IAE1C,8DAA8D;IAC9D,YAAY,EAAE,CAAC,GAAG,EAAE;QAAE,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAA;IAEhG;;;OAGG;IACH,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,EAAE,CAAA;IAEzE;;;OAGG;IACH,IAAI,CAAC,CAAC,EACJ,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EACtB,KAAK,EAAE,CAAC,EACR,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,QAAQ,EAAE,IAAI,EAAE,EAChB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAC3B,WAAW,CAAA;IAEd;;;OAGG;IACH,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE;QAC7B,mDAAmD;QACnD,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,iCAAiC;QACjC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAA;QACvB,oCAAoC;QACpC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAA;QACjC,8CAA8C;QAC9C,UAAU,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;KAC7C,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,CAAA;CAC1C;AAED,wBAAgB,aAAa,CAAC,CAAC,EAC7B,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,GAC1B,eAAe,CAAC,CAAC,CAAC,CAyJpB"}
package/dist/connect.js CHANGED
@@ -1,23 +1,57 @@
1
1
  import { a, onMount } from '@llui/dom';
2
- export function connectRouter(router) {
2
+ export function connectRouter(router, options) {
3
+ let currentRoute = null;
4
+ /**
5
+ * Run guards for a navigation to `newRoute`. Returns the final route
6
+ * to navigate to, or `null` if navigation should be blocked.
7
+ */
8
+ function runGuards(newRoute) {
9
+ if (options?.beforeLeave && currentRoute !== null) {
10
+ if (!options.beforeLeave(currentRoute, newRoute))
11
+ return null;
12
+ }
13
+ if (options?.beforeEnter) {
14
+ const result = options.beforeEnter(newRoute, currentRoute);
15
+ if (result === false)
16
+ return null;
17
+ if (result !== undefined && result !== null && typeof result === 'object') {
18
+ return result;
19
+ }
20
+ }
21
+ return newRoute;
22
+ }
3
23
  function applyEffect(effect) {
4
24
  switch (effect.action) {
5
- case 'push':
25
+ case 'push': {
26
+ const target = router.match(effect.path);
27
+ const finalRoute = runGuards(target);
28
+ if (finalRoute === null)
29
+ return;
30
+ const finalPath = router.href(finalRoute);
6
31
  if (router.mode === 'hash') {
7
- location.hash = effect.path;
32
+ location.hash = finalPath;
8
33
  }
9
34
  else {
10
- history.pushState(null, '', effect.path);
35
+ history.pushState(null, '', finalPath);
11
36
  }
37
+ currentRoute = finalRoute;
12
38
  break;
13
- case 'replace':
39
+ }
40
+ case 'replace': {
41
+ const target = router.match(effect.path);
42
+ const finalRoute = runGuards(target);
43
+ if (finalRoute === null)
44
+ return;
45
+ const finalPath = router.href(finalRoute);
14
46
  if (router.mode === 'hash') {
15
- location.replace(effect.path);
47
+ location.replace(finalPath);
16
48
  }
17
49
  else {
18
- history.replaceState(null, '', effect.path);
50
+ history.replaceState(null, '', finalPath);
19
51
  }
52
+ currentRoute = finalRoute;
20
53
  break;
54
+ }
21
55
  case 'back':
22
56
  history.back();
23
57
  break;
@@ -58,7 +92,17 @@ export function connectRouter(router) {
58
92
  const handler = () => {
59
93
  const input = router.mode === 'hash' ? location.hash : location.pathname + location.search;
60
94
  const route = router.match(input);
61
- send(factory(route));
95
+ const finalRoute = runGuards(route);
96
+ if (finalRoute === null) {
97
+ // Guard blocked — restore previous URL
98
+ if (currentRoute !== null) {
99
+ const restorePath = router.href(currentRoute);
100
+ history.pushState(null, '', restorePath);
101
+ }
102
+ return;
103
+ }
104
+ currentRoute = finalRoute;
105
+ send(factory(finalRoute));
62
106
  };
63
107
  window.addEventListener(event, handler);
64
108
  return () => window.removeEventListener(event, handler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/router",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,7 +21,7 @@
21
21
  "test": "vitest run"
22
22
  },
23
23
  "peerDependencies": {
24
- "@llui/dom": "^0.0.1"
24
+ "@llui/dom": "^0.0.3"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@llui/dom": "workspace:*",