@esmx/router-vue 3.0.0-rc.60 → 3.0.0-rc.63

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,5 +1,5 @@
1
1
  <div align="center">
2
- <img src="https://www.esmnext.com/logo.svg?t=2025" width="120" alt="Esmx Logo" />
2
+ <img src="https://esmx.dev/logo.svg?t=2025" width="120" alt="Esmx Logo" />
3
3
  <h1>@esmx/router-vue</h1>
4
4
 
5
5
  <div>
@@ -9,7 +9,7 @@
9
9
  <a href="https://github.com/esmnext/esmx/actions/workflows/build.yml">
10
10
  <img src="https://github.com/esmnext/esmx/actions/workflows/build.yml/badge.svg" alt="Build" />
11
11
  </a>
12
- <a href="https://www.esmnext.com/coverage/">
12
+ <a href="https://esmx.dev/coverage/">
13
13
  <img src="https://img.shields.io/badge/coverage-live%20report-brightgreen" alt="Coverage Report" />
14
14
  </a>
15
15
  <a href="https://nodejs.org/">
@@ -350,7 +350,7 @@ const link = useLink({
350
350
  <template>
351
351
  <a
352
352
  v-bind="link.attributes"
353
- v-on="link.getEventHandlers()"
353
+ v-on="link.createEventHandlers()"
354
354
  :class="{ active: link.isActive }"
355
355
  >
356
356
  Custom Link
@@ -460,7 +460,7 @@ const link = useLink(props).value;
460
460
  <template>
461
461
  <button
462
462
  v-bind="link.attributes"
463
- v-on="link.getEventHandlers()"
463
+ v-on="link.createEventHandlers()"
464
464
  :class="{
465
465
  active: link.isActive,
466
466
  disabled: disabled
package/README.zh-CN.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img src="https://www.esmnext.com/logo.svg?t=2025" width="120" alt="Esmx Logo" />
2
+ <img src="https://esmx.dev/logo.svg?t=2025" width="120" alt="Esmx Logo" />
3
3
  <h1>@esmx/router-vue</h1>
4
4
 
5
5
  <div>
@@ -9,7 +9,7 @@
9
9
  <a href="https://github.com/esmnext/esmx/actions/workflows/build.yml">
10
10
  <img src="https://github.com/esmnext/esmx/actions/workflows/build.yml/badge.svg" alt="Build" />
11
11
  </a>
12
- <a href="https://www.esmnext.com/coverage/">
12
+ <a href="https://esmx.dev/coverage/">
13
13
  <img src="https://img.shields.io/badge/coverage-live%20report-brightgreen" alt="Coverage Report" />
14
14
  </a>
15
15
  <a href="https://nodejs.org/">
@@ -350,7 +350,7 @@ const link = useLink({
350
350
  <template>
351
351
  <a
352
352
  v-bind="link.attributes"
353
- v-on="link.getEventHandlers()"
353
+ v-on="link.createEventHandlers()"
354
354
  :class="{ active: link.isActive }"
355
355
  >
356
356
  自定义链接
@@ -460,7 +460,7 @@ const link = useLink(props).value;
460
460
  <template>
461
461
  <button
462
462
  v-bind="link.attributes"
463
- v-on="link.getEventHandlers()"
463
+ v-on="link.createEventHandlers()"
464
464
  :class="{
465
465
  active: link.isActive,
466
466
  disabled: disabled
package/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@ export type * from './vue2';
2
2
  export type * from './vue3';
3
3
  export { useRouter, useRoute, useProvideRouter, useLink, getRoute, getRouter } from './use';
4
4
  export { RouterLink } from './router-link';
5
- export { RouterView } from './router-view';
5
+ export { RouterView, RouterViewDepth } from './router-view';
6
6
  export { RouterPlugin } from './plugin';
package/dist/index.mjs CHANGED
@@ -7,5 +7,5 @@ export {
7
7
  getRouter
8
8
  } from "./use.mjs";
9
9
  export { RouterLink } from "./router-link.mjs";
10
- export { RouterView } from "./router-view.mjs";
10
+ export { RouterView, RouterViewDepth } from "./router-view.mjs";
11
11
  export { RouterPlugin } from "./plugin.mjs";
@@ -66,6 +66,7 @@ describe("index.ts - Package Entry Point", () => {
66
66
  // Components
67
67
  "RouterLink",
68
68
  "RouterView",
69
+ "RouterViewDepth",
69
70
  // Plugin
70
71
  "RouterPlugin"
71
72
  ];
@@ -87,6 +88,7 @@ describe("index.ts - Package Entry Point", () => {
87
88
  "getRoute",
88
89
  "RouterLink",
89
90
  "RouterView",
91
+ "RouterViewDepth",
90
92
  "RouterPlugin"
91
93
  ];
92
94
  const unexpectedExports = actualExports.filter(
@@ -1,4 +1,4 @@
1
- import type { RouteLayerOptions, RouteLocationInput, RouteMatchType, RouterLinkType } from '@esmx/router';
1
+ import type { RouterLinkProps } from '@esmx/router';
2
2
  import { type PropType } from 'vue';
3
3
  /**
4
4
  * RouterLink component for navigation.
@@ -56,7 +56,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
56
56
  * @example '/home' | { path: '/user', query: { id: '123' } }
57
57
  */
58
58
  to: {
59
- type: PropType<RouteLocationInput>;
59
+ type: PropType<RouterLinkProps["to"]>;
60
60
  required: true;
61
61
  };
62
62
  /**
@@ -65,7 +65,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
65
65
  * @example 'push' | 'replace' | 'pushWindow' | 'replaceWindow' | 'pushLayer'
66
66
  */
67
67
  type: {
68
- type: PropType<RouterLinkType>;
68
+ type: PropType<RouterLinkProps["type"]>;
69
69
  default: string;
70
70
  };
71
71
  /**
@@ -73,7 +73,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
73
73
  * @example :replace={true} → type="replace"
74
74
  */
75
75
  replace: {
76
- type: BooleanConstructor;
76
+ type: PropType<RouterLinkProps["replace"]>;
77
77
  default: boolean;
78
78
  };
79
79
  /**
@@ -84,7 +84,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
84
84
  * @default 'include'
85
85
  */
86
86
  exact: {
87
- type: PropType<RouteMatchType>;
87
+ type: PropType<RouterLinkProps["exact"]>;
88
88
  default: string;
89
89
  };
90
90
  /**
@@ -92,7 +92,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
92
92
  * @example 'nav-active' | 'selected'
93
93
  */
94
94
  activeClass: {
95
- type: StringConstructor;
95
+ type: PropType<RouterLinkProps["activeClass"]>;
96
96
  };
97
97
  /**
98
98
  * Event(s) that trigger navigation. Can be string or array of strings.
@@ -100,7 +100,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
100
100
  * @example 'click' | ['click', 'mouseenter']
101
101
  */
102
102
  event: {
103
- type: PropType<string | string[]>;
103
+ type: PropType<RouterLinkProps["event"]>;
104
104
  default: string;
105
105
  };
106
106
  /**
@@ -109,7 +109,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
109
109
  * @example 'button' | 'div' | 'span'
110
110
  */
111
111
  tag: {
112
- type: StringConstructor;
112
+ type: PropType<RouterLinkProps["tag"]>;
113
113
  default: string;
114
114
  };
115
115
  /**
@@ -118,34 +118,16 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
118
118
  * @example { zIndex: 1000, autoPush: false, routerOptions: { mode: 'memory' } }
119
119
  */
120
120
  layerOptions: {
121
- type: PropType<RouteLayerOptions>;
121
+ type: PropType<RouterLinkProps["layerOptions"]>;
122
122
  };
123
123
  /**
124
- * Custom event handler to control navigation behavior.
125
- * Should return `true` to allow router to navigate, otherwise to prevent it.
124
+ * Custom navigation handler called before navigation.
125
+ * Receives the event object and the event name that triggered navigation.
126
126
  *
127
127
  * @Note you need to call `e.preventDefault()` to prevent default browser navigation.
128
- * @default
129
- *
130
- * (event: Event & Partial<MouseEvent>): boolean => {
131
- * // don't redirect with control keys
132
- * if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return false;
133
- * // don't redirect when preventDefault called
134
- * if (e.defaultPrevented) return false;
135
- * // don't redirect on right click
136
- * if (e.button !== undefined && e.button !== 0) return false;
137
- * // don't redirect if `target="_blank"`
138
- * const target = e.currentTarget?.getAttribute?.('target') ?? '';
139
- * if (/\b_blank\b/i.test(target)) return false;
140
- * // Prevent default browser navigation to enable SPA routing
141
- * // Note: this may be a Weex event which doesn't have this method
142
- * if (e.preventDefault) e.preventDefault();
143
- *
144
- * return true;
145
- * }
146
128
  */
147
- eventHandler: {
148
- type: PropType<(event: Event) => boolean | undefined | void>;
129
+ beforeNavigate: {
130
+ type: PropType<RouterLinkProps["beforeNavigate"]>;
149
131
  };
150
132
  }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
151
133
  [key: string]: any;
@@ -156,7 +138,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
156
138
  * @example '/home' | { path: '/user', query: { id: '123' } }
157
139
  */
158
140
  to: {
159
- type: PropType<RouteLocationInput>;
141
+ type: PropType<RouterLinkProps["to"]>;
160
142
  required: true;
161
143
  };
162
144
  /**
@@ -165,7 +147,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
165
147
  * @example 'push' | 'replace' | 'pushWindow' | 'replaceWindow' | 'pushLayer'
166
148
  */
167
149
  type: {
168
- type: PropType<RouterLinkType>;
150
+ type: PropType<RouterLinkProps["type"]>;
169
151
  default: string;
170
152
  };
171
153
  /**
@@ -173,7 +155,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
173
155
  * @example :replace={true} → type="replace"
174
156
  */
175
157
  replace: {
176
- type: BooleanConstructor;
158
+ type: PropType<RouterLinkProps["replace"]>;
177
159
  default: boolean;
178
160
  };
179
161
  /**
@@ -184,7 +166,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
184
166
  * @default 'include'
185
167
  */
186
168
  exact: {
187
- type: PropType<RouteMatchType>;
169
+ type: PropType<RouterLinkProps["exact"]>;
188
170
  default: string;
189
171
  };
190
172
  /**
@@ -192,7 +174,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
192
174
  * @example 'nav-active' | 'selected'
193
175
  */
194
176
  activeClass: {
195
- type: StringConstructor;
177
+ type: PropType<RouterLinkProps["activeClass"]>;
196
178
  };
197
179
  /**
198
180
  * Event(s) that trigger navigation. Can be string or array of strings.
@@ -200,7 +182,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
200
182
  * @example 'click' | ['click', 'mouseenter']
201
183
  */
202
184
  event: {
203
- type: PropType<string | string[]>;
185
+ type: PropType<RouterLinkProps["event"]>;
204
186
  default: string;
205
187
  };
206
188
  /**
@@ -209,7 +191,7 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
209
191
  * @example 'button' | 'div' | 'span'
210
192
  */
211
193
  tag: {
212
- type: StringConstructor;
194
+ type: PropType<RouterLinkProps["tag"]>;
213
195
  default: string;
214
196
  };
215
197
  /**
@@ -218,39 +200,21 @@ export declare const RouterLink: import("vue").DefineComponent<import("vue").Ext
218
200
  * @example { zIndex: 1000, autoPush: false, routerOptions: { mode: 'memory' } }
219
201
  */
220
202
  layerOptions: {
221
- type: PropType<RouteLayerOptions>;
203
+ type: PropType<RouterLinkProps["layerOptions"]>;
222
204
  };
223
205
  /**
224
- * Custom event handler to control navigation behavior.
225
- * Should return `true` to allow router to navigate, otherwise to prevent it.
206
+ * Custom navigation handler called before navigation.
207
+ * Receives the event object and the event name that triggered navigation.
226
208
  *
227
209
  * @Note you need to call `e.preventDefault()` to prevent default browser navigation.
228
- * @default
229
- *
230
- * (event: Event & Partial<MouseEvent>): boolean => {
231
- * // don't redirect with control keys
232
- * if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return false;
233
- * // don't redirect when preventDefault called
234
- * if (e.defaultPrevented) return false;
235
- * // don't redirect on right click
236
- * if (e.button !== undefined && e.button !== 0) return false;
237
- * // don't redirect if `target="_blank"`
238
- * const target = e.currentTarget?.getAttribute?.('target') ?? '';
239
- * if (/\b_blank\b/i.test(target)) return false;
240
- * // Prevent default browser navigation to enable SPA routing
241
- * // Note: this may be a Weex event which doesn't have this method
242
- * if (e.preventDefault) e.preventDefault();
243
- *
244
- * return true;
245
- * }
246
210
  */
247
- eventHandler: {
248
- type: PropType<(event: Event) => boolean | undefined | void>;
211
+ beforeNavigate: {
212
+ type: PropType<RouterLinkProps["beforeNavigate"]>;
249
213
  };
250
214
  }>> & Readonly<{}>, {
251
- replace: boolean;
252
- exact: RouteMatchType;
253
- type: RouterLinkType;
254
- event: string | string[];
255
- tag: string;
215
+ type: import("@esmx/router").RouterLinkType | undefined;
216
+ replace: boolean | undefined;
217
+ exact: import("@esmx/router").RouteMatchType | undefined;
218
+ event: string | string[] | undefined;
219
+ tag: string | undefined;
256
220
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -18,12 +18,18 @@ export const RouterLink = defineComponent({
18
18
  * @default 'push'
19
19
  * @example 'push' | 'replace' | 'pushWindow' | 'replaceWindow' | 'pushLayer'
20
20
  */
21
- type: { type: String, default: "push" },
21
+ type: {
22
+ type: String,
23
+ default: "push"
24
+ },
22
25
  /**
23
26
  * @deprecated Use 'type="replace"' instead
24
27
  * @example :replace={true} → type="replace"
25
28
  */
26
- replace: { type: Boolean, default: false },
29
+ replace: {
30
+ type: Boolean,
31
+ default: false
32
+ },
27
33
  /**
28
34
  * How to match the active state.
29
35
  * - 'include': Match if current route includes this path
@@ -31,12 +37,17 @@ export const RouterLink = defineComponent({
31
37
  * - 'route': Match based on route configuration
32
38
  * @default 'include'
33
39
  */
34
- exact: { type: String, default: "include" },
40
+ exact: {
41
+ type: String,
42
+ default: "include"
43
+ },
35
44
  /**
36
45
  * CSS class to apply when link is active (route matches).
37
46
  * @example 'nav-active' | 'selected'
38
47
  */
39
- activeClass: { type: String },
48
+ activeClass: {
49
+ type: String
50
+ },
40
51
  /**
41
52
  * Event(s) that trigger navigation. Can be string or array of strings.
42
53
  * @default 'click'
@@ -57,84 +68,52 @@ export const RouterLink = defineComponent({
57
68
  * Only used when type='pushLayer'.
58
69
  * @example { zIndex: 1000, autoPush: false, routerOptions: { mode: 'memory' } }
59
70
  */
60
- layerOptions: { type: Object },
71
+ layerOptions: {
72
+ type: Object
73
+ },
61
74
  /**
62
- * Custom event handler to control navigation behavior.
63
- * Should return `true` to allow router to navigate, otherwise to prevent it.
75
+ * Custom navigation handler called before navigation.
76
+ * Receives the event object and the event name that triggered navigation.
64
77
  *
65
78
  * @Note you need to call `e.preventDefault()` to prevent default browser navigation.
66
- * @default
67
- *
68
- * (event: Event & Partial<MouseEvent>): boolean => {
69
- * // don't redirect with control keys
70
- * if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return false;
71
- * // don't redirect when preventDefault called
72
- * if (e.defaultPrevented) return false;
73
- * // don't redirect on right click
74
- * if (e.button !== undefined && e.button !== 0) return false;
75
- * // don't redirect if `target="_blank"`
76
- * const target = e.currentTarget?.getAttribute?.('target') ?? '';
77
- * if (/\b_blank\b/i.test(target)) return false;
78
- * // Prevent default browser navigation to enable SPA routing
79
- * // Note: this may be a Weex event which doesn't have this method
80
- * if (e.preventDefault) e.preventDefault();
81
- *
82
- * return true;
83
- * }
84
79
  */
85
- eventHandler: {
80
+ beforeNavigate: {
86
81
  type: Function
87
82
  }
88
83
  },
89
84
  setup(props, context) {
90
- const { slots, attrs } = context;
91
- const link = useLink(props);
92
- const wrapHandler = (externalHandler, internalHandler) => !internalHandler ? externalHandler : async (e) => {
93
- try {
94
- await externalHandler(e);
95
- } finally {
96
- await internalHandler(e);
97
- }
98
- };
99
- const vue3renderer = () => {
100
- var _a;
101
- const data = link.value;
102
- const genEventName = (name) => "on".concat(name.charAt(0).toUpperCase()).concat(name.slice(1));
103
- const eventHandlers = data.getEventHandlers(genEventName);
104
- Object.entries(attrs).forEach(([key, listener]) => {
105
- if (!key.startsWith("on") || typeof listener !== "function")
106
- return;
107
- eventHandlers[key] = wrapHandler(listener, eventHandlers[key]);
108
- });
109
- return h(
110
- data.tag,
111
- {
112
- ...data.attributes,
113
- ...eventHandlers
114
- },
115
- (_a = slots.default) == null ? void 0 : _a.call(slots)
116
- );
117
- };
118
- const vue2renderer = () => {
119
- var _a;
120
- const data = link.value;
121
- const eventHandlers = data.getEventHandlers();
122
- const $listeners = context.listeners || {};
123
- Object.entries($listeners).forEach(([key, listener]) => {
124
- if (typeof listener !== "function") return;
125
- eventHandlers[key] = wrapHandler(listener, eventHandlers[key]);
126
- });
127
- const { class: className, ...attrs2 } = data.attributes;
85
+ const link = useLink(props).value;
86
+ if (isVue3) {
87
+ return () => {
88
+ var _a, _b;
89
+ return h(
90
+ link.tag,
91
+ {
92
+ ...link.attributes,
93
+ ...context.attrs,
94
+ ...link.createEventHandlers(
95
+ (name) => "on".concat(name.charAt(0).toUpperCase()).concat(name.slice(1))
96
+ )
97
+ },
98
+ (_b = (_a = context.slots).default) == null ? void 0 : _b.call(_a)
99
+ );
100
+ };
101
+ }
102
+ return () => {
103
+ var _a, _b;
104
+ const { class: className, ...attributes } = link.attributes;
128
105
  return h(
129
- data.tag,
106
+ link.tag,
130
107
  {
131
- attrs: attrs2,
108
+ attrs: {
109
+ ...attributes,
110
+ ...context.attrs
111
+ },
132
112
  class: className,
133
- on: eventHandlers
113
+ on: link.createEventHandlers()
134
114
  },
135
- (_a = slots.default) == null ? void 0 : _a.call(slots)
115
+ (_b = (_a = context.slots).default) == null ? void 0 : _b.call(_a)
136
116
  );
137
117
  };
138
- return isVue3 ? vue3renderer : vue2renderer;
139
118
  }
140
119
  });
@@ -51,7 +51,14 @@ describe("router-link.ts - RouterLink Component", () => {
51
51
  app.unmount();
52
52
  }
53
53
  if (router) {
54
- router.destroy();
54
+ try {
55
+ await new Promise((resolve) => setTimeout(resolve, 0));
56
+ router.destroy();
57
+ } catch (error) {
58
+ if (!(error instanceof Error) || !error.message.includes("RouteTaskCancelledError")) {
59
+ console.warn("Router destruction error:", error);
60
+ }
61
+ }
55
62
  }
56
63
  if (container.parentNode) {
57
64
  container.parentNode.removeChild(container);
@@ -97,6 +104,29 @@ describe("router-link.ts - RouterLink Component", () => {
97
104
  expect(linkElement).toBeTruthy();
98
105
  expect(linkElement == null ? void 0 : linkElement.textContent).toBe("About Link");
99
106
  });
107
+ it("should render router link with custom attributes", async () => {
108
+ const TestApp = defineComponent({
109
+ setup() {
110
+ useProvideRouter(router);
111
+ return () => h(
112
+ RouterLink,
113
+ {
114
+ to: "/about",
115
+ "data-test": "custom-attr",
116
+ title: "Custom Title"
117
+ },
118
+ () => "Link with Attributes"
119
+ );
120
+ }
121
+ });
122
+ app = createApp(TestApp);
123
+ app.mount(container);
124
+ await nextTick();
125
+ const linkElement = container.querySelector("a");
126
+ expect(linkElement).toBeTruthy();
127
+ expect(linkElement == null ? void 0 : linkElement.getAttribute("data-test")).toBe("custom-attr");
128
+ expect(linkElement == null ? void 0 : linkElement.getAttribute("title")).toBe("Custom Title");
129
+ });
100
130
  it("should render with custom tag", async () => {
101
131
  const TestApp = defineComponent({
102
132
  setup() {
@@ -255,6 +285,36 @@ describe("router-link.ts - RouterLink Component", () => {
255
285
  expect(router.route.path).toBe("/about");
256
286
  expect(router.route.query.tab).toBe("info");
257
287
  });
288
+ it("should handle custom navigation handler", async () => {
289
+ let customHandlerCalled = false;
290
+ let receivedEventName = "";
291
+ const TestApp = defineComponent({
292
+ setup() {
293
+ useProvideRouter(router);
294
+ return () => h(
295
+ RouterLink,
296
+ {
297
+ to: "/about",
298
+ beforeNavigate: (event, eventName) => {
299
+ customHandlerCalled = true;
300
+ receivedEventName = eventName;
301
+ event.preventDefault();
302
+ }
303
+ },
304
+ () => "Custom Handler Link"
305
+ );
306
+ }
307
+ });
308
+ app = createApp(TestApp);
309
+ app.mount(container);
310
+ await nextTick();
311
+ const linkElement = container.querySelector("a");
312
+ expect(linkElement).toBeTruthy();
313
+ linkElement == null ? void 0 : linkElement.click();
314
+ await nextTick();
315
+ expect(customHandlerCalled).toBe(true);
316
+ expect(receivedEventName).toBe("click");
317
+ });
258
318
  });
259
319
  describe("Props Validation", () => {
260
320
  it("should accept string as to prop", async () => {
@@ -1,3 +1,4 @@
1
+ export declare const RouterViewDepth: unique symbol;
1
2
  /**
2
3
  * RouterView component for rendering matched route components.
3
4
  * Acts as a placeholder where route components are rendered based on the current route.
@@ -1,13 +1,13 @@
1
1
  import { defineComponent, h, inject, provide } from "vue";
2
2
  import { useRoute } from "./use.mjs";
3
3
  import { resolveComponent } from "./util.mjs";
4
- const RouterViewDepthKey = Symbol("RouterViewDepth");
4
+ export const RouterViewDepth = Symbol("RouterViewDepth");
5
5
  export const RouterView = defineComponent({
6
6
  name: "RouterView",
7
7
  setup() {
8
8
  const route = useRoute();
9
- const depth = inject(RouterViewDepthKey, 0);
10
- provide(RouterViewDepthKey, depth + 1);
9
+ const depth = inject(RouterViewDepth, 0);
10
+ provide(RouterViewDepth, depth + 1);
11
11
  return () => {
12
12
  const matchedRoute = route.matched[depth];
13
13
  const component = matchedRoute ? resolveComponent(matchedRoute.component) : null;
@@ -170,11 +170,11 @@ describe("router-view.ts - RouterView Component", () => {
170
170
  describe("Depth Tracking", () => {
171
171
  it("should inject depth 0 by default", async () => {
172
172
  let injectedDepth;
173
- const RouterViewDepthKey = Symbol("RouterViewDepth");
173
+ const RouterViewDepth = Symbol("RouterViewDepth");
174
174
  const TestRouterView = defineComponent({
175
175
  name: "TestRouterView",
176
176
  setup() {
177
- injectedDepth = inject(RouterViewDepthKey, -1);
177
+ injectedDepth = inject(RouterViewDepth, -1);
178
178
  return () => h(RouterView);
179
179
  }
180
180
  });
@@ -193,19 +193,19 @@ describe("router-view.ts - RouterView Component", () => {
193
193
  it("should provide correct depth in nested RouterViews", async () => {
194
194
  let parentDepth;
195
195
  let childDepth;
196
- const RouterViewDepthKey = Symbol("RouterViewDepth");
196
+ const RouterViewDepth = Symbol("RouterViewDepth");
197
197
  const ParentTestComponent = defineComponent({
198
198
  name: "ParentTestComponent",
199
199
  setup() {
200
- parentDepth = inject(RouterViewDepthKey, -1);
201
- provide(RouterViewDepthKey, 0);
200
+ parentDepth = inject(RouterViewDepth, -1);
201
+ provide(RouterViewDepth, 0);
202
202
  return () => h("div", [h("span", "Parent"), h(ChildTestComponent)]);
203
203
  }
204
204
  });
205
205
  const ChildTestComponent = defineComponent({
206
206
  name: "ChildTestComponent",
207
207
  setup() {
208
- childDepth = inject(RouterViewDepthKey, -1);
208
+ childDepth = inject(RouterViewDepth, -1);
209
209
  return () => h("div", "Child");
210
210
  }
211
211
  });
@@ -272,12 +272,12 @@ describe("router-view.ts - RouterView Component", () => {
272
272
  });
273
273
  describe("Edge Cases and Error Handling", () => {
274
274
  it("should render null when no route matches at current depth", async () => {
275
- const RouterViewDepthKey = Symbol("RouterViewDepth");
275
+ const RouterViewDepth = Symbol("RouterViewDepth");
276
276
  const DeepRouterView = defineComponent({
277
277
  name: "DeepRouterView",
278
278
  setup() {
279
- const currentDepth = inject(RouterViewDepthKey, 0);
280
- provide(RouterViewDepthKey, currentDepth + 1);
279
+ const currentDepth = inject(RouterViewDepth, 0);
280
+ provide(RouterViewDepth, currentDepth + 1);
281
281
  return () => h(RouterView);
282
282
  }
283
283
  });
package/dist/use.d.ts CHANGED
@@ -178,7 +178,7 @@ export declare function useProvideRouter(router: Router): void;
178
178
  * <template>
179
179
  * <a
180
180
  * v-bind="link.attributes"
181
- * v-on="link.getEventHandlers()"
181
+ * v-on="link.createEventHandlers()"
182
182
  * :class="{ active: link.isActive }"
183
183
  * >
184
184
  * Home
package/dist/use.mjs CHANGED
@@ -62,7 +62,7 @@ export function useRoute() {
62
62
  }
63
63
  export function useProvideRouter(router) {
64
64
  const proxy = getCurrentProxy("useProvideRouter");
65
- const dep = ref(false);
65
+ const dep = ref(0);
66
66
  const proxiedRouter = createDependentProxy(router, dep);
67
67
  const proxiedRoute = createDependentProxy(router.route, dep);
68
68
  const context = {
@@ -74,7 +74,7 @@ export function useProvideRouter(router) {
74
74
  const unwatch = router.afterEach((to) => {
75
75
  if (router.route === to) {
76
76
  to.syncTo(proxiedRoute);
77
- dep.value = !dep.value;
77
+ dep.value++;
78
78
  }
79
79
  });
80
80
  onBeforeUnmount(unwatch);
package/dist/util.d.ts CHANGED
@@ -4,6 +4,6 @@ export declare function createSymbolProperty<T>(symbol: symbol): {
4
4
  readonly set: (instance: any, value: T) => void;
5
5
  readonly get: (instance: any) => T | undefined;
6
6
  };
7
- export declare function createDependentProxy<T extends object>(obj: T, dep: Ref<boolean>): T;
7
+ export declare function createDependentProxy<T extends object>(obj: T, dep: Ref<any>): T;
8
8
  export declare function isESModule(obj: unknown): obj is Record<string | symbol, any>;
9
9
  export declare function resolveComponent(component: unknown): unknown;
package/package.json CHANGED
@@ -50,7 +50,7 @@
50
50
  "vue": "^3.5.0 || ^2.7.0"
51
51
  },
52
52
  "dependencies": {
53
- "@esmx/router": "3.0.0-rc.60"
53
+ "@esmx/router": "3.0.0-rc.63"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@biomejs/biome": "1.9.4",
@@ -62,7 +62,7 @@
62
62
  "vue": "3.5.13",
63
63
  "vue2": "npm:vue@2.7.16"
64
64
  },
65
- "version": "3.0.0-rc.60",
65
+ "version": "3.0.0-rc.63",
66
66
  "type": "module",
67
67
  "private": false,
68
68
  "exports": {
@@ -81,5 +81,5 @@
81
81
  "template",
82
82
  "public"
83
83
  ],
84
- "gitHead": "615e91c617e0a58796c591643c6a2e1d2a1f0a76"
84
+ "gitHead": "18a524ff1b7f9c6ea60d8bb57ad314329febc58c"
85
85
  }
package/src/index.test.ts CHANGED
@@ -79,6 +79,7 @@ describe('index.ts - Package Entry Point', () => {
79
79
  // Components
80
80
  'RouterLink',
81
81
  'RouterView',
82
+ 'RouterViewDepth',
82
83
  // Plugin
83
84
  'RouterPlugin'
84
85
  ];
@@ -102,6 +103,7 @@ describe('index.ts - Package Entry Point', () => {
102
103
  'getRoute',
103
104
  'RouterLink',
104
105
  'RouterView',
106
+ 'RouterViewDepth',
105
107
  'RouterPlugin'
106
108
  ];
107
109
 
package/src/index.ts CHANGED
@@ -11,6 +11,6 @@ export {
11
11
  } from './use';
12
12
 
13
13
  export { RouterLink } from './router-link';
14
- export { RouterView } from './router-view';
14
+ export { RouterView, RouterViewDepth } from './router-view';
15
15
 
16
16
  export { RouterPlugin } from './plugin';
@@ -66,7 +66,20 @@ describe('router-link.ts - RouterLink Component', () => {
66
66
  app.unmount();
67
67
  }
68
68
  if (router) {
69
- router.destroy();
69
+ try {
70
+ // Wait for any pending navigation to complete before destroying
71
+ await new Promise((resolve) => setTimeout(resolve, 0));
72
+ router.destroy();
73
+ } catch (error) {
74
+ // Ignore router destruction errors, as they might be expected
75
+ // when navigation tasks are cancelled during cleanup
76
+ if (
77
+ !(error instanceof Error) ||
78
+ !error.message.includes('RouteTaskCancelledError')
79
+ ) {
80
+ console.warn('Router destruction error:', error);
81
+ }
82
+ }
70
83
  }
71
84
  if (container.parentNode) {
72
85
  container.parentNode.removeChild(container);
@@ -129,6 +142,33 @@ describe('router-link.ts - RouterLink Component', () => {
129
142
  expect(linkElement?.textContent).toBe('About Link');
130
143
  });
131
144
 
145
+ it('should render router link with custom attributes', async () => {
146
+ const TestApp = defineComponent({
147
+ setup() {
148
+ useProvideRouter(router);
149
+ return () =>
150
+ h(
151
+ RouterLink,
152
+ {
153
+ to: '/about',
154
+ 'data-test': 'custom-attr',
155
+ title: 'Custom Title'
156
+ },
157
+ () => 'Link with Attributes'
158
+ );
159
+ }
160
+ });
161
+
162
+ app = createApp(TestApp);
163
+ app.mount(container);
164
+ await nextTick();
165
+
166
+ const linkElement = container.querySelector('a');
167
+ expect(linkElement).toBeTruthy();
168
+ expect(linkElement?.getAttribute('data-test')).toBe('custom-attr');
169
+ expect(linkElement?.getAttribute('title')).toBe('Custom Title');
170
+ });
171
+
132
172
  it('should render with custom tag', async () => {
133
173
  const TestApp = defineComponent({
134
174
  setup() {
@@ -326,6 +366,47 @@ describe('router-link.ts - RouterLink Component', () => {
326
366
  expect(router.route.path).toBe('/about');
327
367
  expect(router.route.query.tab).toBe('info');
328
368
  });
369
+
370
+ it('should handle custom navigation handler', async () => {
371
+ let customHandlerCalled = false;
372
+ let receivedEventName = '';
373
+ const TestApp = defineComponent({
374
+ setup() {
375
+ useProvideRouter(router);
376
+ return () =>
377
+ h(
378
+ RouterLink,
379
+ {
380
+ to: '/about',
381
+ beforeNavigate: (
382
+ event: Event,
383
+ eventName: string
384
+ ) => {
385
+ customHandlerCalled = true;
386
+ receivedEventName = eventName;
387
+ event.preventDefault();
388
+ }
389
+ },
390
+ () => 'Custom Handler Link'
391
+ );
392
+ }
393
+ });
394
+
395
+ app = createApp(TestApp);
396
+ app.mount(container);
397
+ await nextTick();
398
+
399
+ const linkElement = container.querySelector('a');
400
+ expect(linkElement).toBeTruthy();
401
+
402
+ // Simulate click
403
+ linkElement?.click();
404
+ await nextTick();
405
+
406
+ // Check if custom handler was called with correct event name
407
+ expect(customHandlerCalled).toBe(true);
408
+ expect(receivedEventName).toBe('click');
409
+ });
329
410
  });
330
411
 
331
412
  describe('Props Validation', () => {
@@ -1,9 +1,4 @@
1
- import type {
2
- RouteLayerOptions,
3
- RouteLocationInput,
4
- RouteMatchType,
5
- RouterLinkType
6
- } from '@esmx/router';
1
+ import type { RouterLinkProps } from '@esmx/router';
7
2
  import { type PropType, defineComponent, h } from 'vue';
8
3
  import { useLink } from './use';
9
4
  import { isVue3 } from './util';
@@ -66,7 +61,7 @@ export const RouterLink = defineComponent({
66
61
  * @example '/home' | { path: '/user', query: { id: '123' } }
67
62
  */
68
63
  to: {
69
- type: [String, Object] as PropType<RouteLocationInput>,
64
+ type: [String, Object] as PropType<RouterLinkProps['to']>,
70
65
  required: true
71
66
  },
72
67
  /**
@@ -74,12 +69,18 @@ export const RouterLink = defineComponent({
74
69
  * @default 'push'
75
70
  * @example 'push' | 'replace' | 'pushWindow' | 'replaceWindow' | 'pushLayer'
76
71
  */
77
- type: { type: String as PropType<RouterLinkType>, default: 'push' },
72
+ type: {
73
+ type: String as PropType<RouterLinkProps['type']>,
74
+ default: 'push'
75
+ },
78
76
  /**
79
77
  * @deprecated Use 'type="replace"' instead
80
78
  * @example :replace={true} → type="replace"
81
79
  */
82
- replace: { type: Boolean, default: false },
80
+ replace: {
81
+ type: Boolean as PropType<RouterLinkProps['replace']>,
82
+ default: false
83
+ },
83
84
  /**
84
85
  * How to match the active state.
85
86
  * - 'include': Match if current route includes this path
@@ -87,19 +88,24 @@ export const RouterLink = defineComponent({
87
88
  * - 'route': Match based on route configuration
88
89
  * @default 'include'
89
90
  */
90
- exact: { type: String as PropType<RouteMatchType>, default: 'include' },
91
+ exact: {
92
+ type: String as PropType<RouterLinkProps['exact']>,
93
+ default: 'include'
94
+ },
91
95
  /**
92
96
  * CSS class to apply when link is active (route matches).
93
97
  * @example 'nav-active' | 'selected'
94
98
  */
95
- activeClass: { type: String },
99
+ activeClass: {
100
+ type: String as PropType<RouterLinkProps['activeClass']>
101
+ },
96
102
  /**
97
103
  * Event(s) that trigger navigation. Can be string or array of strings.
98
104
  * @default 'click'
99
105
  * @example 'click' | ['click', 'mouseenter']
100
106
  */
101
107
  event: {
102
- type: [String, Array] as PropType<string | string[]>,
108
+ type: [String, Array] as PropType<RouterLinkProps['event']>,
103
109
  default: 'click'
104
110
  },
105
111
  /**
@@ -107,108 +113,59 @@ export const RouterLink = defineComponent({
107
113
  * @default 'a'
108
114
  * @example 'button' | 'div' | 'span'
109
115
  */
110
- tag: { type: String, default: 'a' },
116
+ tag: { type: String as PropType<RouterLinkProps['tag']>, default: 'a' },
111
117
  /**
112
118
  * Layer options for layer-based navigation.
113
119
  * Only used when type='pushLayer'.
114
120
  * @example { zIndex: 1000, autoPush: false, routerOptions: { mode: 'memory' } }
115
121
  */
116
- layerOptions: { type: Object as PropType<RouteLayerOptions> },
122
+ layerOptions: {
123
+ type: Object as PropType<RouterLinkProps['layerOptions']>
124
+ },
117
125
  /**
118
- * Custom event handler to control navigation behavior.
119
- * Should return `true` to allow router to navigate, otherwise to prevent it.
126
+ * Custom navigation handler called before navigation.
127
+ * Receives the event object and the event name that triggered navigation.
120
128
  *
121
129
  * @Note you need to call `e.preventDefault()` to prevent default browser navigation.
122
- * @default
123
- *
124
- * (event: Event & Partial<MouseEvent>): boolean => {
125
- * // don't redirect with control keys
126
- * if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return false;
127
- * // don't redirect when preventDefault called
128
- * if (e.defaultPrevented) return false;
129
- * // don't redirect on right click
130
- * if (e.button !== undefined && e.button !== 0) return false;
131
- * // don't redirect if `target="_blank"`
132
- * const target = e.currentTarget?.getAttribute?.('target') ?? '';
133
- * if (/\b_blank\b/i.test(target)) return false;
134
- * // Prevent default browser navigation to enable SPA routing
135
- * // Note: this may be a Weex event which doesn't have this method
136
- * if (e.preventDefault) e.preventDefault();
137
- *
138
- * return true;
139
- * }
140
130
  */
141
- eventHandler: {
142
- type: Function as PropType<
143
- (event: Event) => boolean | undefined | void
144
- >
131
+ beforeNavigate: {
132
+ type: Function as PropType<RouterLinkProps['beforeNavigate']>
145
133
  }
146
134
  },
147
135
 
148
136
  setup(props, context) {
149
- const { slots, attrs } = context;
150
- const link = useLink(props);
151
-
152
- const wrapHandler = (
153
- externalHandler: Function,
154
- internalHandler: Function | undefined
155
- ) =>
156
- !internalHandler
157
- ? (externalHandler as (e: Event) => Promise<void>)
158
- : async (e: Event) => {
159
- try {
160
- await externalHandler(e);
161
- } finally {
162
- await internalHandler(e);
163
- }
164
- };
165
-
166
- const vue3renderer = () => {
167
- const data = link.value;
168
- const genEventName = (name: string): string =>
169
- `on${name.charAt(0).toUpperCase()}${name.slice(1)}`;
170
-
171
- const eventHandlers = data.getEventHandlers(genEventName);
172
- Object.entries(attrs).forEach(([key, listener]) => {
173
- // In Vue 3, external event handlers are in attrs with 'on' prefix
174
- if (!key.startsWith('on') || typeof listener !== 'function')
175
- return;
176
- eventHandlers[key] = wrapHandler(listener, eventHandlers[key]);
177
- });
137
+ const link = useLink(props).value;
178
138
 
139
+ if (isVue3) {
140
+ return () => {
141
+ return h(
142
+ link.tag,
143
+ {
144
+ ...link.attributes,
145
+ ...context.attrs,
146
+ ...link.createEventHandlers(
147
+ (name) =>
148
+ `on${name.charAt(0).toUpperCase()}${name.slice(1)}`
149
+ )
150
+ },
151
+ context.slots.default?.()
152
+ );
153
+ };
154
+ }
155
+ return () => {
156
+ const { class: className, ...attributes } = link.attributes;
179
157
  return h(
180
- data.tag,
181
- {
182
- ...data.attributes,
183
- ...eventHandlers
184
- },
185
- slots.default?.()
186
- );
187
- };
188
-
189
- const vue2renderer = () => {
190
- const data = link.value;
191
-
192
- const eventHandlers = data.getEventHandlers();
193
- // Vue 2: get external listeners from context
194
- const $listeners = (context as any).listeners || {};
195
- Object.entries($listeners).forEach(([key, listener]) => {
196
- if (typeof listener !== 'function') return;
197
- eventHandlers[key] = wrapHandler(listener, eventHandlers[key]);
198
- });
199
-
200
- const { class: className, ...attrs } = data.attributes;
201
- return h(
202
- data.tag,
158
+ link.tag,
203
159
  {
204
- attrs,
160
+ attrs: {
161
+ ...attributes,
162
+ ...context.attrs
163
+ },
205
164
  class: className,
206
- on: eventHandlers
165
+ on: link.createEventHandlers()
207
166
  },
208
- slots.default?.()
167
+ context.slots.default?.()
209
168
  );
210
169
  };
211
-
212
- return isVue3 ? vue3renderer : vue2renderer;
213
170
  }
214
171
  });
@@ -231,13 +231,13 @@ describe('router-view.ts - RouterView Component', () => {
231
231
  let injectedDepth: number | undefined;
232
232
 
233
233
  // Use the same symbol key that RouterView uses internally
234
- const RouterViewDepthKey = Symbol('RouterViewDepth');
234
+ const RouterViewDepth = Symbol('RouterViewDepth');
235
235
 
236
236
  // Create a custom RouterView component that can capture the injected depth
237
237
  const TestRouterView = defineComponent({
238
238
  name: 'TestRouterView',
239
239
  setup() {
240
- injectedDepth = inject(RouterViewDepthKey, -1);
240
+ injectedDepth = inject(RouterViewDepth, -1);
241
241
  return () => h(RouterView);
242
242
  }
243
243
  });
@@ -263,13 +263,13 @@ describe('router-view.ts - RouterView Component', () => {
263
263
  let parentDepth: number | undefined;
264
264
  let childDepth: number | undefined;
265
265
 
266
- const RouterViewDepthKey = Symbol('RouterViewDepth');
266
+ const RouterViewDepth = Symbol('RouterViewDepth');
267
267
 
268
268
  const ParentTestComponent = defineComponent({
269
269
  name: 'ParentTestComponent',
270
270
  setup() {
271
- parentDepth = inject(RouterViewDepthKey, -1);
272
- provide(RouterViewDepthKey, 0); // Simulate parent RouterView
271
+ parentDepth = inject(RouterViewDepth, -1);
272
+ provide(RouterViewDepth, 0); // Simulate parent RouterView
273
273
  return () =>
274
274
  h('div', [h('span', 'Parent'), h(ChildTestComponent)]);
275
275
  }
@@ -278,7 +278,7 @@ describe('router-view.ts - RouterView Component', () => {
278
278
  const ChildTestComponent = defineComponent({
279
279
  name: 'ChildTestComponent',
280
280
  setup() {
281
- childDepth = inject(RouterViewDepthKey, -1);
281
+ childDepth = inject(RouterViewDepth, -1);
282
282
  return () => h('div', 'Child');
283
283
  }
284
284
  });
@@ -362,14 +362,14 @@ describe('router-view.ts - RouterView Component', () => {
362
362
 
363
363
  describe('Edge Cases and Error Handling', () => {
364
364
  it('should render null when no route matches at current depth', async () => {
365
- const RouterViewDepthKey = Symbol('RouterViewDepth');
365
+ const RouterViewDepth = Symbol('RouterViewDepth');
366
366
 
367
367
  const DeepRouterView = defineComponent({
368
368
  name: 'DeepRouterView',
369
369
  setup() {
370
370
  // Inject depth 0 from parent RouterView and provide depth 1
371
- const currentDepth = inject(RouterViewDepthKey, 0);
372
- provide(RouterViewDepthKey, currentDepth + 1);
371
+ const currentDepth = inject(RouterViewDepth, 0);
372
+ provide(RouterViewDepth, currentDepth + 1);
373
373
  return () => h(RouterView);
374
374
  }
375
375
  });
@@ -2,7 +2,7 @@ import { defineComponent, h, inject, provide } from 'vue';
2
2
  import { useRoute } from './use';
3
3
  import { resolveComponent } from './util';
4
4
 
5
- const RouterViewDepthKey = Symbol('RouterViewDepth');
5
+ export const RouterViewDepth = Symbol('RouterViewDepth');
6
6
 
7
7
  /**
8
8
  * RouterView component for rendering matched route components.
@@ -39,11 +39,11 @@ export const RouterView = defineComponent({
39
39
 
40
40
  // Get current RouterView depth from parent RouterView (if any)
41
41
  // This enables proper nested routing by tracking how deep we are in the component tree
42
- const depth = inject(RouterViewDepthKey, 0);
42
+ const depth = inject(RouterViewDepth, 0);
43
43
 
44
44
  // Provide depth + 1 to child RouterView components
45
45
  // This ensures each nested RouterView renders the correct route component
46
- provide(RouterViewDepthKey, depth + 1);
46
+ provide(RouterViewDepth, depth + 1);
47
47
 
48
48
  return () => {
49
49
  // Get the matched route configuration at current depth
package/src/use.ts CHANGED
@@ -259,7 +259,7 @@ export function useRoute(): Route {
259
259
  export function useProvideRouter(router: Router): void {
260
260
  const proxy = getCurrentProxy('useProvideRouter');
261
261
 
262
- const dep = ref(false);
262
+ const dep = ref(0);
263
263
 
264
264
  const proxiedRouter = createDependentProxy(router, dep);
265
265
  const proxiedRoute = createDependentProxy(router.route, dep);
@@ -275,7 +275,7 @@ export function useProvideRouter(router: Router): void {
275
275
  const unwatch = router.afterEach((to: Route) => {
276
276
  if (router.route === to) {
277
277
  to.syncTo(proxiedRoute);
278
- dep.value = !dep.value;
278
+ dep.value++;
279
279
  }
280
280
  });
281
281
 
@@ -294,7 +294,7 @@ export function useProvideRouter(router: Router): void {
294
294
  * <template>
295
295
  * <a
296
296
  * v-bind="link.attributes"
297
- * v-on="link.getEventHandlers()"
297
+ * v-on="link.createEventHandlers()"
298
298
  * :class="{ active: link.isActive }"
299
299
  * >
300
300
  * Home
package/src/util.ts CHANGED
@@ -16,7 +16,7 @@ export function createSymbolProperty<T>(symbol: symbol) {
16
16
 
17
17
  export function createDependentProxy<T extends object>(
18
18
  obj: T,
19
- dep: Ref<boolean>
19
+ dep: Ref<any>
20
20
  ): T {
21
21
  return new Proxy(obj, {
22
22
  get(target, prop, receiver) {