@esmx/router-vue 3.0.0-rc.59 → 3.0.0-rc.62

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
@@ -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
@@ -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
@@ -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 () => {
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/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.59"
53
+ "@esmx/router": "3.0.0-rc.62"
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.59",
65
+ "version": "3.0.0-rc.62",
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": "d221aba2a43064b5666e714f42904ae80b2b57ad"
84
+ "gitHead": "e5a1e811403bf1db4437dff88c3ea8bc6b576f64"
85
85
  }
@@ -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
  });
package/src/use.ts CHANGED
@@ -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