@llui/router 0.0.2 → 0.0.4

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
@@ -19,10 +19,7 @@ const detail = route(['item', param('id')])
19
19
  const docs = route(['docs', rest('path')])
20
20
 
21
21
  // Create router
22
- const router = createRouter(
23
- { home, search, detail, docs },
24
- { mode: 'history' },
25
- )
22
+ const router = createRouter({ home, search, detail, docs }, { mode: 'history' })
26
23
 
27
24
  // Connect to effects system
28
25
  const routing = connectRouter(router)
@@ -32,32 +29,75 @@ const routing = connectRouter(router)
32
29
 
33
30
  ### Route Definition
34
31
 
35
- | Function | Description |
36
- | --------------------------------- | -------------------------------------------- |
32
+ | Function | Description |
33
+ | --------------------------------------- | --------------------------------------------------------- |
37
34
  | `route(segments, builder?, queryKeys?)` | Define a route with path segments and optional query keys |
38
- | `param(name)` | Named path parameter (e.g. `/item/:id`) |
39
- | `rest(name)` | Rest parameter capturing remaining path |
35
+ | `param(name)` | Named path parameter (e.g. `/item/:id`) |
36
+ | `rest(name)` | Rest parameter capturing remaining path |
40
37
 
41
38
  ### Router
42
39
 
43
- | Function | Description |
44
- | --------------------------------- | -------------------------------------------- |
45
- | `createRouter(routes, config)` | Create router instance (`history` or `hash` mode) |
46
- | `connectRouter(router)` | Connect router to LLui effects, returns routing helpers |
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 |
47
44
 
48
45
  ### Routing Helpers (from connectRouter)
49
46
 
50
- | Method / Effect | Description |
51
- | --------------------------------- | -------------------------------------------- |
52
- | `.link(send, route, attrs, children)` | Render a navigation link with client-side routing |
53
- | `.listener(send)` | Popstate listener -- call in `view()` to react to URL changes |
54
- | `.handleEffect` | Effect handler plugin for navigate/push/replace effects |
55
- | `.push(route)` | Push navigation effect |
56
- | `.replace(route)` | Replace navigation effect |
57
- | `.back()` | Navigate back effect |
58
- | `.forward()` | Navigate forward effect |
59
- | `.scroll()` | Scroll restoration effect |
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 |
60
57
 
61
- ## License
58
+ ## Guards
62
59
 
63
- MIT
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
+ ```
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.2",
3
+ "version": "0.0.4",
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.2"
24
+ "@llui/dom": "^0.0.3"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@llui/dom": "workspace:*",