@llui/router 0.5.1 → 0.7.0

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.
@@ -0,0 +1,46 @@
1
+ {
2
+ "compilerVersion": "0.3.0",
3
+ "components": {},
4
+ "helpers": {
5
+ "connect#connectRouter": {
6
+ "helperLocalPaths": [],
7
+ "kind": "view-helper",
8
+ "viaParams": [
9
+ {
10
+ "index": 0,
11
+ "reads": [
12
+ "mode"
13
+ ],
14
+ "shape": "state-value"
15
+ },
16
+ {
17
+ "index": 1,
18
+ "reads": [
19
+ "beforeEnter",
20
+ "beforeLeave"
21
+ ],
22
+ "shape": "state-value"
23
+ }
24
+ ]
25
+ },
26
+ "index#createRouter": {
27
+ "helperLocalPaths": [],
28
+ "kind": "view-helper",
29
+ "viaParams": [
30
+ {
31
+ "index": 0,
32
+ "shape": "opaque"
33
+ },
34
+ {
35
+ "index": 1,
36
+ "reads": [
37
+ "fallback",
38
+ "mode"
39
+ ],
40
+ "shape": "state-value"
41
+ }
42
+ ]
43
+ }
44
+ },
45
+ "version": 2
46
+ }
package/dist/connect.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Router } from './index.js';
2
+ import type { Mountable, Renderable, ChildNode } from '@llui/dom';
2
3
  export interface RouterEffect {
3
4
  type: '__router';
4
5
  action: 'push' | 'replace' | 'navigate' | 'back' | 'forward' | 'scroll';
@@ -76,16 +77,17 @@ export interface ConnectedRouter<R> {
76
77
  }) => boolean;
77
78
  /**
78
79
  * View helper: attach URL change listener via onMount.
79
- * Returns an empty comment node. Sends { type: 'navigate', route } on URL change.
80
+ * Returns the onMount marker to place in the view. Sends { type: 'navigate', route } on URL change.
80
81
  */
81
- listener<M>(send: (msg: M) => void, msgFactory?: (route: R) => M): Node[];
82
+ listener<M>(send: (msg: M) => void, msgFactory?: (route: R) => M): Renderable;
82
83
  /**
83
84
  * View helper: render a navigation link.
84
85
  * Generates <a> with proper href and click handler that sends navigate message.
85
86
  */
86
- link<M>(send: (msg: M) => void, route: R, attrs: Record<string, unknown>, children: Node[], msgFactory?: (route: R) => M): Node;
87
+ link<M>(send: (msg: M) => void, route: R, attrs: Record<string, unknown>, children: readonly ChildNode[], msgFactory?: (route: R) => M): Mountable;
87
88
  /**
88
- * Create an update handler for mergeHandlers.
89
+ * Create an update handler for navigate messages — call it from your
90
+ * component's `update` (returns early when it handles the message).
89
91
  * Returns [newState, Effect[]] for navigate messages, null for others.
90
92
  */
91
93
  createHandler<S, M, E>(config: {
@@ -1 +1 @@
1
- {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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"}
package/dist/connect.js CHANGED
@@ -126,39 +126,43 @@ export function connectRouter(router, options) {
126
126
  },
127
127
  listener(send, msgFactory) {
128
128
  const factory = msgFactory ?? ((r) => ({ type: 'navigate', route: r }));
129
- onMount(() => {
130
- // Capture send/factory so the navigate() effect can dispatch
131
- // route-changed messages from any reducer, not just from
132
- // popstate or click handlers. Stored as the generic `unknown`
133
- // shape so applyEffect doesn't need to know R or M; the only
134
- // consumer is the navigate case above, which round-trips R
135
- // through factory back to the user's M.
136
- listenerSend = send;
137
- listenerFactory = factory;
138
- const event = router.mode === 'hash' ? 'hashchange' : 'popstate';
139
- const handler = () => {
140
- const input = router.mode === 'hash' ? location.hash : location.pathname + location.search;
141
- const route = router.match(input);
142
- const finalRoute = runGuards(route);
143
- if (finalRoute === null) {
144
- // Guard blocked restore previous URL
145
- if (currentRoute !== null) {
146
- const restorePath = router.href(currentRoute);
147
- history.pushState(null, '', restorePath);
129
+ // Place the onMount marker in the view; its callback registers the URL listener
130
+ // on mount. (onMount is a lazy Mountable — calling it for side effect and
131
+ // discarding the return would never register.)
132
+ return [
133
+ onMount(() => {
134
+ // Capture send/factory so the navigate() effect can dispatch
135
+ // route-changed messages from any reducer, not just from
136
+ // popstate or click handlers. Stored as the generic `unknown`
137
+ // shape so applyEffect doesn't need to know R or M; the only
138
+ // consumer is the navigate case above, which round-trips R
139
+ // through factory back to the user's M.
140
+ listenerSend = send;
141
+ listenerFactory = factory;
142
+ const event = router.mode === 'hash' ? 'hashchange' : 'popstate';
143
+ const handler = () => {
144
+ const input = router.mode === 'hash' ? location.hash : location.pathname + location.search;
145
+ const route = router.match(input);
146
+ const finalRoute = runGuards(route);
147
+ if (finalRoute === null) {
148
+ // Guard blocked — restore previous URL
149
+ if (currentRoute !== null) {
150
+ const restorePath = router.href(currentRoute);
151
+ history.pushState(null, '', restorePath);
152
+ }
153
+ return;
148
154
  }
149
- return;
150
- }
151
- currentRoute = finalRoute;
152
- send(factory(finalRoute));
153
- };
154
- window.addEventListener(event, handler);
155
- return () => {
156
- window.removeEventListener(event, handler);
157
- listenerSend = null;
158
- listenerFactory = null;
159
- };
160
- });
161
- return [document.createComment('router')];
155
+ currentRoute = finalRoute;
156
+ send(factory(finalRoute));
157
+ };
158
+ window.addEventListener(event, handler);
159
+ return () => {
160
+ window.removeEventListener(event, handler);
161
+ listenerSend = null;
162
+ listenerFactory = null;
163
+ };
164
+ }),
165
+ ];
162
166
  },
163
167
  link(send, route, attrs, children, msgFactory) {
164
168
  const factory = msgFactory ?? ((r) => ({ type: 'navigate', route: r }));
@@ -1 +1 @@
1
- {"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA+GtC,MAAM,UAAU,aAAa,CAC3B,MAAiB,EACjB,OAA2B;IAE3B,IAAI,YAAY,GAAa,IAAI,CAAA;IACjC,2DAA2D;IAC3D,iEAAiE;IACjE,iEAAiE;IACjE,mEAAmE;IACnE,mEAAmE;IACnE,kDAAkD;IAClD,IAAI,YAAY,GAAoC,IAAI,CAAA;IACxD,IAAI,eAAe,GAAmC,IAAI,CAAA;IAC1D;;;OAGG;IACH,SAAS,SAAS,CAAC,QAAW;QAC5B,IAAI,OAAO,EAAE,WAAW,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;YAClD,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,YAAY,EAAE,QAAQ,CAAC;gBAAE,OAAO,IAAI,CAAA;QAC/D,CAAC;QACD,IAAI,OAAO,EAAE,WAAW,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAA;YAC1D,IAAI,MAAM,KAAK,KAAK;gBAAE,OAAO,IAAI,CAAA;YACjC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAC1E,OAAO,MAAW,CAAA;YACpB,CAAC;QACH,CAAC;QACD,OAAO,QAAQ,CAAA;IACjB,CAAC;IAED,SAAS,WAAW,CAAC,MAAoB;QACvC,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;YACtB,KAAK,MAAM,CAAC,CAAC,CAAC;gBACZ,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAK,CAAC,CAAA;gBACzC,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;gBACpC,IAAI,UAAU,KAAK,IAAI;oBAAE,OAAM;gBAC/B,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;gBACzC,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;oBAC3B,QAAQ,CAAC,IAAI,GAAG,SAAS,CAAA;gBAC3B,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,EAAE,SAAS,CAAC,CAAA;gBACxC,CAAC;gBACD,YAAY,GAAG,UAAU,CAAA;gBACzB,MAAK;YACP,CAAC;YACD,KAAK,SAAS,CAAC,CAAC,CAAC;gBACf,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAK,CAAC,CAAA;gBACzC,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;gBACpC,IAAI,UAAU,KAAK,IAAI;oBAAE,OAAM;gBAC/B,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;gBACzC,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;oBAC3B,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;gBAC7B,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,EAAE,EAAE,SAAS,CAAC,CAAA;gBAC3C,CAAC;gBACD,YAAY,GAAG,UAAU,CAAA;gBACzB,MAAK;YACP,CAAC;YACD,KAAK,UAAU,CAAC,CAAC,CAAC;gBAChB,6DAA6D;gBAC7D,2DAA2D;gBAC3D,+DAA+D;gBAC/D,8DAA8D;gBAC9D,8DAA8D;gBAC9D,+DAA+D;gBAC/D,iCAAiC;gBACjC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAK,CAAC,CAAA;gBACzC,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAAC,CAAA;gBACpC,IAAI,UAAU,KAAK,IAAI;oBAAE,OAAM;gBAC/B,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;gBACzC,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;oBAC3B,QAAQ,CAAC,IAAI,GAAG,SAAS,CAAA;gBAC3B,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,EAAE,SAAS,CAAC,CAAA;gBACxC,CAAC;gBACD,YAAY,GAAG,UAAU,CAAA;gBACzB,IAAI,YAAY,KAAK,IAAI,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;oBACtD,YAAY,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAA;gBAC3C,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,IAAI,CACV,+OAA+O,CAChP,CAAA;gBACH,CAAC;gBACD,MAAK;YACP,CAAC;YACD,KAAK,MAAM;gBACT,OAAO,CAAC,IAAI,EAAE,CAAA;gBACd,MAAK;YACP,KAAK,SAAS;gBACZ,OAAO,CAAC,OAAO,EAAE,CAAA;gBACjB,MAAK;YACP,KAAK,QAAQ;gBACX,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAE,EAAE,MAAM,CAAC,CAAE,CAAC,CAAA;gBACrC,MAAK;QACT,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,CAAC,KAAK;YACR,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAA;QACvE,CAAC;QACD,OAAO,CAAC,KAAK;YACX,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAA;QAC1E,CAAC;QACD,QAAQ,CAAC,KAAK;YACZ,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAA;QAC3E,CAAC;QACD,IAAI;YACF,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;QAC7C,CAAC;QACD,OAAO;YACL,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;QAChD,CAAC;QACD,MAAM,CAAC,CAAC,EAAE,CAAC;YACT,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,EAAE,CAAA;QACrD,CAAC;QAED,YAAY,CAAC,EAAE,MAAM,EAAE;YACrB,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU;gBAAE,OAAO,KAAK,CAAA;YAC5C,WAAW,CAAC,MAAsB,CAAC,CAAA;YACnC,OAAO,IAAI,CAAA;QACb,CAAC;QAED,QAAQ,CAAI,IAAsB,EAAE,UAA4B;YAC9D,MAAM,OAAO,GAAG,UAAU,IAAI,CAAC,CAAC,CAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,CAAM,CAAC,CAAA;YAC/E,OAAO,CAAC,GAAG,EAAE;gBACX,6DAA6D;gBAC7D,yDAAyD;gBACzD,8DAA8D;gBAC9D,6DAA6D;gBAC7D,2DAA2D;gBAC3D,wCAAwC;gBACxC,YAAY,GAAG,IAA8B,CAAA;gBAC7C,eAAe,GAAG,OAAgC,CAAA;gBAElD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAA;gBAChE,MAAM,OAAO,GAAG,GAAG,EAAE;oBACnB,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAA;oBAC1F,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;oBACjC,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,CAAC,CAAA;oBACnC,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;wBACxB,uCAAuC;wBACvC,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;4BAC1B,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;4BAC7C,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,EAAE,WAAW,CAAC,CAAA;wBAC1C,CAAC;wBACD,OAAM;oBACR,CAAC;oBACD,YAAY,GAAG,UAAU,CAAA;oBACzB,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAA;gBAC3B,CAAC,CAAA;gBACD,MAAM,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;gBACvC,OAAO,GAAG,EAAE;oBACV,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;oBAC1C,YAAY,GAAG,IAAI,CAAA;oBACnB,eAAe,GAAG,IAAI,CAAA;gBACxB,CAAC,CAAA;YACH,CAAC,CAAC,CAAA;YACF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAA;QAC3C,CAAC;QAED,IAAI,CACF,IAAsB,EACtB,KAAQ,EACR,KAA8B,EAC9B,QAAgB,EAChB,UAA4B;YAE5B,MAAM,OAAO,GAAG,UAAU,IAAI,CAAC,CAAC,CAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,CAAM,CAAC,CAAA;YAC/E,OAAO,CAAC,CACN;gBACE,GAAG,KAAK;gBACR,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;gBACxB,OAAO,EAAE,CAAC,CAAQ,EAAE,EAAE;oBACpB,MAAM,EAAE,GAAG,CAAe,CAAA;oBAC1B,IAAI,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;wBAAE,OAAM;oBACnF,CAAC,CAAC,cAAc,EAAE,CAAA;oBAClB,mEAAmE;oBACnE,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;wBAC3B,sEAAsE;wBACtE,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;wBAClC,OAAM;oBACR,CAAC;oBACD,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;oBAC/C,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAA;gBACtB,CAAC;aACF,EACD,QAAQ,CACT,CAAA;QACH,CAAC;QAED,aAAa,CAAU,MAKtB;YACC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,UAAU,CAAA;YAC5C,OAAO,CAAC,KAAQ,EAAE,GAAM,EAAE,EAAE;gBAC1B,IAAK,GAA+B,CAAC,IAAI,KAAK,OAAO;oBAAE,OAAO,IAAI,CAAA;gBAClE,IAAI,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;gBAChC,IAAI,MAAM,CAAC,KAAK;oBAAE,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;gBACpD,OAAO,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YACxC,CAAC,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import type { Router } from './index.js'\nimport { a, onMount } from '@llui/dom'\n\n// ── Router Effects ───────────────────────────────────────────────\n\nexport interface RouterEffect {\n type: '__router'\n action: 'push' | 'replace' | 'navigate' | 'back' | 'forward' | 'scroll'\n path?: string\n x?: number\n y?: number\n}\n\nexport interface ConnectOptions<R> {\n /**\n * Called before entering a new route. Return:\n * - `void` / `undefined` → allow navigation\n * - `false` → block navigation (stay on current route)\n * - a different `Route` → redirect to that route\n */\n beforeEnter?: (to: R, from: R | null) => R | false | void\n /**\n * Called before leaving the current route. Return:\n * - `true` → allow navigation\n * - `false` → block (e.g. unsaved changes prompt)\n */\n beforeLeave?: (from: R, to: R) => boolean\n}\n\nexport interface ConnectedRouter<R> {\n /**\n * Effect: push a new history entry — URL only.\n *\n * Use when the reducer that emitted the effect has already updated\n * `state.route` itself (e.g. a `Router/Navigate` handler that bundles\n * state changes inline before delegating URL work). For\n * navigate-and-let-the-app-react flows from anywhere else, prefer\n * `navigate()` — it dispatches the listener-captured navigate\n * message after pushState so `state.route` and route-side-effects\n * stay in sync without each reducer re-implementing the delegation.\n */\n push(route: R): RouterEffect\n /**\n * Effect: replace the current history entry — URL only. Same\n * URL-only contract as `push()`. For replace-and-react flows, see\n * `navigate()` (push semantics) — there's no `replaceAndDispatch`\n * variant yet because the use case hasn't surfaced; if it does,\n * model it the same way.\n */\n replace(route: R): RouterEffect\n /**\n * Effect: push history AND dispatch the listener-captured navigate\n * message so the reducer can update `state.route` and run any\n * route-side-effects (data fetches, page-meta resets, analytics).\n *\n * Resolves the asymmetry where `link()` did pushState + send while\n * `push()` did pushState only — apps that wanted programmatic\n * navigation from arbitrary reducers had to either re-implement the\n * delegation or live with desynced `state.route`.\n *\n * Requires that the app has mounted `listener()` (typically inside\n * the shell view) — the navigate effect uses the send/factory\n * captured there. If `navigate()` runs before `listener()` mounts,\n * the URL still updates but no message is dispatched and a\n * `console.warn` surfaces the gap. After listener unmount the same\n * fallback applies.\n */\n navigate(route: R): RouterEffect\n /** Effect: go back */\n back(): RouterEffect\n /** Effect: go forward */\n forward(): RouterEffect\n /** Effect: scroll to position */\n scroll(x: number, y: number): RouterEffect\n\n /** Plugin for handleEffects().use() — handles RouterEffect */\n handleEffect: (ctx: { effect: { type: string }; send: unknown; signal: AbortSignal }) => boolean\n\n /**\n * View helper: attach URL change listener via onMount.\n * Returns an empty comment node. Sends { type: 'navigate', route } on URL change.\n */\n listener<M>(send: (msg: M) => void, msgFactory?: (route: R) => M): Node[]\n\n /**\n * View helper: render a navigation link.\n * Generates <a> with proper href and click handler that sends navigate message.\n */\n link<M>(\n send: (msg: M) => void,\n route: R,\n attrs: Record<string, unknown>,\n children: Node[],\n msgFactory?: (route: R) => M,\n ): Node\n\n /**\n * Create an update handler for mergeHandlers.\n * Returns [newState, Effect[]] for navigate messages, null for others.\n */\n createHandler<S, M, E>(config: {\n /** Message type to handle (default: 'navigate') */\n message?: string\n /** Extract route from message */\n getRoute: (msg: M) => R\n /** Optional guard — can redirect */\n guard?: (route: R, state: S) => R\n /** Build new state + effects for the route */\n onNavigate: (state: S, route: R) => [S, E[]]\n }): (state: S, msg: M) => [S, E[]] | null\n}\n\nexport function connectRouter<R>(\n router: Router<R>,\n options?: ConnectOptions<R>,\n): ConnectedRouter<R> {\n let currentRoute: R | null = null\n // Captured by listener() at mount, cleared at unmount. The\n // navigate() effect reads these to dispatch the navigate message\n // after pushState — they are the bridge between the reducer-side\n // (which produces effects) and the dispatcher-side (which receives\n // messages). Module-scope inside the closure: at most one listener\n // is active per ConnectedRouter (the shell view).\n let listenerSend: ((msg: unknown) => void) | null = null\n let listenerFactory: ((route: R) => unknown) | null = null\n /**\n * Run guards for a navigation to `newRoute`. Returns the final route\n * to navigate to, or `null` if navigation should be blocked.\n */\n function runGuards(newRoute: R): R | null {\n if (options?.beforeLeave && currentRoute !== null) {\n if (!options.beforeLeave(currentRoute, newRoute)) return null\n }\n if (options?.beforeEnter) {\n const result = options.beforeEnter(newRoute, currentRoute)\n if (result === false) return null\n if (result !== undefined && result !== null && typeof result === 'object') {\n return result as R\n }\n }\n return newRoute\n }\n\n function applyEffect(effect: RouterEffect): void {\n switch (effect.action) {\n case 'push': {\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.hash = finalPath\n } else {\n history.pushState(null, '', finalPath)\n }\n currentRoute = finalRoute\n break\n }\n case 'replace': {\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.replace(finalPath)\n } else {\n history.replaceState(null, '', finalPath)\n }\n currentRoute = finalRoute\n break\n }\n case 'navigate': {\n // pushState semantics + dispatch the navigate message so the\n // app reducer sees the route change. This is the asymmetry\n // fix: link() always did push+send (because click handlers run\n // synchronously in view code with send/factory in scope), but\n // push() as an effect could only do push (no access to send).\n // navigate() resolves it by reading the closure variables that\n // listener() sets at mount time.\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.hash = finalPath\n } else {\n history.pushState(null, '', finalPath)\n }\n currentRoute = finalRoute\n if (listenerSend !== null && listenerFactory !== null) {\n listenerSend(listenerFactory(finalRoute))\n } else {\n console.warn(\n '@llui/router: navigate() effect dispatched but listener() is not mounted — URL updated, but no navigate message was sent. Mount connectedRouter.listener() in your shell view, or use push() and dispatch the route-changed message yourself.',\n )\n }\n break\n }\n case 'back':\n history.back()\n break\n case 'forward':\n history.forward()\n break\n case 'scroll':\n window.scrollTo(effect.x!, effect.y!)\n break\n }\n }\n\n return {\n push(route) {\n return { type: '__router', action: 'push', path: router.href(route) }\n },\n replace(route) {\n return { type: '__router', action: 'replace', path: router.href(route) }\n },\n navigate(route) {\n return { type: '__router', action: 'navigate', path: router.href(route) }\n },\n back() {\n return { type: '__router', action: 'back' }\n },\n forward() {\n return { type: '__router', action: 'forward' }\n },\n scroll(x, y) {\n return { type: '__router', action: 'scroll', x, y }\n },\n\n handleEffect({ effect }) {\n if (effect.type !== '__router') return false\n applyEffect(effect as RouterEffect)\n return true\n },\n\n listener<M>(send: (msg: M) => void, msgFactory?: (route: R) => M): Node[] {\n const factory = msgFactory ?? ((r: R) => ({ type: 'navigate', route: r }) as M)\n onMount(() => {\n // Capture send/factory so the navigate() effect can dispatch\n // route-changed messages from any reducer, not just from\n // popstate or click handlers. Stored as the generic `unknown`\n // shape so applyEffect doesn't need to know R or M; the only\n // consumer is the navigate case above, which round-trips R\n // through factory back to the user's M.\n listenerSend = send as (msg: unknown) => void\n listenerFactory = factory as (route: R) => unknown\n\n const event = router.mode === 'hash' ? 'hashchange' : 'popstate'\n const handler = () => {\n const input = router.mode === 'hash' ? location.hash : location.pathname + location.search\n const route = router.match(input)\n const finalRoute = runGuards(route)\n if (finalRoute === null) {\n // Guard blocked — restore previous URL\n if (currentRoute !== null) {\n const restorePath = router.href(currentRoute)\n history.pushState(null, '', restorePath)\n }\n return\n }\n currentRoute = finalRoute\n send(factory(finalRoute))\n }\n window.addEventListener(event, handler)\n return () => {\n window.removeEventListener(event, handler)\n listenerSend = null\n listenerFactory = null\n }\n })\n return [document.createComment('router')]\n },\n\n link<M>(\n send: (msg: M) => void,\n route: R,\n attrs: Record<string, unknown>,\n children: Node[],\n msgFactory?: (route: R) => M,\n ): Node {\n const factory = msgFactory ?? ((r: R) => ({ type: 'navigate', route: r }) as M)\n return a(\n {\n ...attrs,\n href: router.href(route),\n onClick: (e: Event) => {\n const me = e as MouseEvent\n if (me.ctrlKey || me.metaKey || me.shiftKey || me.altKey || me.button !== 0) return\n e.preventDefault()\n // Push history — pushState doesn't fire popstate, so no double-nav\n if (router.mode === 'hash') {\n // hashchange will fire the listener, which sends the navigate message\n location.hash = router.href(route)\n return\n }\n history.pushState(null, '', router.href(route))\n send(factory(route))\n },\n },\n children,\n )\n },\n\n createHandler<S, M, E>(config: {\n message?: string\n getRoute: (msg: M) => R\n guard?: (route: R, state: S) => R\n onNavigate: (state: S, route: R) => [S, E[]]\n }): (state: S, msg: M) => [S, E[]] | null {\n const msgType = config.message ?? 'navigate'\n return (state: S, msg: M) => {\n if ((msg as Record<string, unknown>).type !== msgType) return null\n let route = config.getRoute(msg)\n if (config.guard) route = config.guard(route, state)\n return config.onNavigate(state, route)\n }\n },\n }\n}\n"]}
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Router } from './index.js'\nimport { a, onMount } from '@llui/dom'\nimport type { Mountable, Renderable, ChildNode } from '@llui/dom'\n\n// ── Router Effects ───────────────────────────────────────────────\n\nexport interface RouterEffect {\n type: '__router'\n action: 'push' | 'replace' | 'navigate' | 'back' | 'forward' | 'scroll'\n path?: string\n x?: number\n y?: number\n}\n\nexport interface ConnectOptions<R> {\n /**\n * Called before entering a new route. Return:\n * - `void` / `undefined` → allow navigation\n * - `false` → block navigation (stay on current route)\n * - a different `Route` → redirect to that route\n */\n beforeEnter?: (to: R, from: R | null) => R | false | void\n /**\n * Called before leaving the current route. Return:\n * - `true` → allow navigation\n * - `false` → block (e.g. unsaved changes prompt)\n */\n beforeLeave?: (from: R, to: R) => boolean\n}\n\nexport interface ConnectedRouter<R> {\n /**\n * Effect: push a new history entry — URL only.\n *\n * Use when the reducer that emitted the effect has already updated\n * `state.route` itself (e.g. a `Router/Navigate` handler that bundles\n * state changes inline before delegating URL work). For\n * navigate-and-let-the-app-react flows from anywhere else, prefer\n * `navigate()` — it dispatches the listener-captured navigate\n * message after pushState so `state.route` and route-side-effects\n * stay in sync without each reducer re-implementing the delegation.\n */\n push(route: R): RouterEffect\n /**\n * Effect: replace the current history entry — URL only. Same\n * URL-only contract as `push()`. For replace-and-react flows, see\n * `navigate()` (push semantics) — there's no `replaceAndDispatch`\n * variant yet because the use case hasn't surfaced; if it does,\n * model it the same way.\n */\n replace(route: R): RouterEffect\n /**\n * Effect: push history AND dispatch the listener-captured navigate\n * message so the reducer can update `state.route` and run any\n * route-side-effects (data fetches, page-meta resets, analytics).\n *\n * Resolves the asymmetry where `link()` did pushState + send while\n * `push()` did pushState only — apps that wanted programmatic\n * navigation from arbitrary reducers had to either re-implement the\n * delegation or live with desynced `state.route`.\n *\n * Requires that the app has mounted `listener()` (typically inside\n * the shell view) — the navigate effect uses the send/factory\n * captured there. If `navigate()` runs before `listener()` mounts,\n * the URL still updates but no message is dispatched and a\n * `console.warn` surfaces the gap. After listener unmount the same\n * fallback applies.\n */\n navigate(route: R): RouterEffect\n /** Effect: go back */\n back(): RouterEffect\n /** Effect: go forward */\n forward(): RouterEffect\n /** Effect: scroll to position */\n scroll(x: number, y: number): RouterEffect\n\n /** Plugin for handleEffects().use() — handles RouterEffect */\n handleEffect: (ctx: { effect: { type: string }; send: unknown; signal: AbortSignal }) => boolean\n\n /**\n * View helper: attach URL change listener via onMount.\n * Returns the onMount marker to place in the view. Sends { type: 'navigate', route } on URL change.\n */\n listener<M>(send: (msg: M) => void, msgFactory?: (route: R) => M): Renderable\n\n /**\n * View helper: render a navigation link.\n * Generates <a> with proper href and click handler that sends navigate message.\n */\n link<M>(\n send: (msg: M) => void,\n route: R,\n attrs: Record<string, unknown>,\n children: readonly ChildNode[],\n msgFactory?: (route: R) => M,\n ): Mountable\n\n /**\n * Create an update handler for navigate messages — call it from your\n * component's `update` (returns early when it handles the message).\n * Returns [newState, Effect[]] for navigate messages, null for others.\n */\n createHandler<S, M, E>(config: {\n /** Message type to handle (default: 'navigate') */\n message?: string\n /** Extract route from message */\n getRoute: (msg: M) => R\n /** Optional guard — can redirect */\n guard?: (route: R, state: S) => R\n /** Build new state + effects for the route */\n onNavigate: (state: S, route: R) => [S, E[]]\n }): (state: S, msg: M) => [S, E[]] | null\n}\n\nexport function connectRouter<R>(\n router: Router<R>,\n options?: ConnectOptions<R>,\n): ConnectedRouter<R> {\n let currentRoute: R | null = null\n // Captured by listener() at mount, cleared at unmount. The\n // navigate() effect reads these to dispatch the navigate message\n // after pushState — they are the bridge between the reducer-side\n // (which produces effects) and the dispatcher-side (which receives\n // messages). Module-scope inside the closure: at most one listener\n // is active per ConnectedRouter (the shell view).\n let listenerSend: ((msg: unknown) => void) | null = null\n let listenerFactory: ((route: R) => unknown) | null = null\n /**\n * Run guards for a navigation to `newRoute`. Returns the final route\n * to navigate to, or `null` if navigation should be blocked.\n */\n function runGuards(newRoute: R): R | null {\n if (options?.beforeLeave && currentRoute !== null) {\n if (!options.beforeLeave(currentRoute, newRoute)) return null\n }\n if (options?.beforeEnter) {\n const result = options.beforeEnter(newRoute, currentRoute)\n if (result === false) return null\n if (result !== undefined && result !== null && typeof result === 'object') {\n return result as R\n }\n }\n return newRoute\n }\n\n function applyEffect(effect: RouterEffect): void {\n switch (effect.action) {\n case 'push': {\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.hash = finalPath\n } else {\n history.pushState(null, '', finalPath)\n }\n currentRoute = finalRoute\n break\n }\n case 'replace': {\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.replace(finalPath)\n } else {\n history.replaceState(null, '', finalPath)\n }\n currentRoute = finalRoute\n break\n }\n case 'navigate': {\n // pushState semantics + dispatch the navigate message so the\n // app reducer sees the route change. This is the asymmetry\n // fix: link() always did push+send (because click handlers run\n // synchronously in view code with send/factory in scope), but\n // push() as an effect could only do push (no access to send).\n // navigate() resolves it by reading the closure variables that\n // listener() sets at mount time.\n const target = router.match(effect.path!)\n const finalRoute = runGuards(target)\n if (finalRoute === null) return\n const finalPath = router.href(finalRoute)\n if (router.mode === 'hash') {\n location.hash = finalPath\n } else {\n history.pushState(null, '', finalPath)\n }\n currentRoute = finalRoute\n if (listenerSend !== null && listenerFactory !== null) {\n listenerSend(listenerFactory(finalRoute))\n } else {\n console.warn(\n '@llui/router: navigate() effect dispatched but listener() is not mounted — URL updated, but no navigate message was sent. Mount connectedRouter.listener() in your shell view, or use push() and dispatch the route-changed message yourself.',\n )\n }\n break\n }\n case 'back':\n history.back()\n break\n case 'forward':\n history.forward()\n break\n case 'scroll':\n window.scrollTo(effect.x!, effect.y!)\n break\n }\n }\n\n return {\n push(route) {\n return { type: '__router', action: 'push', path: router.href(route) }\n },\n replace(route) {\n return { type: '__router', action: 'replace', path: router.href(route) }\n },\n navigate(route) {\n return { type: '__router', action: 'navigate', path: router.href(route) }\n },\n back() {\n return { type: '__router', action: 'back' }\n },\n forward() {\n return { type: '__router', action: 'forward' }\n },\n scroll(x, y) {\n return { type: '__router', action: 'scroll', x, y }\n },\n\n handleEffect({ effect }) {\n if (effect.type !== '__router') return false\n applyEffect(effect as RouterEffect)\n return true\n },\n\n listener<M>(send: (msg: M) => void, msgFactory?: (route: R) => M): Renderable {\n const factory = msgFactory ?? ((r: R) => ({ type: 'navigate', route: r }) as M)\n // Place the onMount marker in the view; its callback registers the URL listener\n // on mount. (onMount is a lazy Mountable — calling it for side effect and\n // discarding the return would never register.)\n return [\n onMount(() => {\n // Capture send/factory so the navigate() effect can dispatch\n // route-changed messages from any reducer, not just from\n // popstate or click handlers. Stored as the generic `unknown`\n // shape so applyEffect doesn't need to know R or M; the only\n // consumer is the navigate case above, which round-trips R\n // through factory back to the user's M.\n listenerSend = send as (msg: unknown) => void\n listenerFactory = factory as (route: R) => unknown\n\n const event = router.mode === 'hash' ? 'hashchange' : 'popstate'\n const handler = () => {\n const input =\n router.mode === 'hash' ? location.hash : location.pathname + location.search\n const route = router.match(input)\n const finalRoute = runGuards(route)\n if (finalRoute === null) {\n // Guard blocked — restore previous URL\n if (currentRoute !== null) {\n const restorePath = router.href(currentRoute)\n history.pushState(null, '', restorePath)\n }\n return\n }\n currentRoute = finalRoute\n send(factory(finalRoute))\n }\n window.addEventListener(event, handler)\n return () => {\n window.removeEventListener(event, handler)\n listenerSend = null\n listenerFactory = null\n }\n }),\n ]\n },\n\n link<M>(\n send: (msg: M) => void,\n route: R,\n attrs: Record<string, unknown>,\n children: readonly ChildNode[],\n msgFactory?: (route: R) => M,\n ): Mountable {\n const factory = msgFactory ?? ((r: R) => ({ type: 'navigate', route: r }) as M)\n return a(\n {\n ...attrs,\n href: router.href(route),\n onClick: (e: Event) => {\n const me = e as MouseEvent\n if (me.ctrlKey || me.metaKey || me.shiftKey || me.altKey || me.button !== 0) return\n e.preventDefault()\n // Push history — pushState doesn't fire popstate, so no double-nav\n if (router.mode === 'hash') {\n // hashchange will fire the listener, which sends the navigate message\n location.hash = router.href(route)\n return\n }\n history.pushState(null, '', router.href(route))\n send(factory(route))\n },\n },\n children,\n )\n },\n\n createHandler<S, M, E>(config: {\n message?: string\n getRoute: (msg: M) => R\n guard?: (route: R, state: S) => R\n onNavigate: (state: S, route: R) => [S, E[]]\n }): (state: S, msg: M) => [S, E[]] | null {\n const msgType = config.message ?? 'navigate'\n return (state: S, msg: M) => {\n if ((msg as Record<string, unknown>).type !== msgType) return null\n let route = config.getRoute(msg)\n if (config.guard) route = config.guard(route, state)\n return config.onNavigate(state, route)\n }\n },\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/router",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,12 +15,12 @@
15
15
  }
16
16
  },
17
17
  "peerDependencies": {
18
- "@llui/dom": "^0.5.0"
18
+ "@llui/dom": "^0.7.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "typescript": "^6.0.0",
22
22
  "vitest": "^4.1.2",
23
- "@llui/dom": "0.5.3"
23
+ "@llui/dom": "0.7.0"
24
24
  },
25
25
  "sideEffects": false,
26
26
  "description": "LLui router — structured path matching, history/hash mode, link helper",