@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 +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/router-link.d.ts +30 -66
- package/dist/router-link.mjs +48 -69
- package/dist/router-link.test.mjs +61 -1
- package/dist/use.d.ts +1 -1
- package/package.json +3 -3
- package/src/router-link.test.ts +82 -1
- package/src/router-link.ts +52 -95
- package/src/use.ts +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
463
|
+
v-on="link.createEventHandlers()"
|
|
464
464
|
:class="{
|
|
465
465
|
active: link.isActive,
|
|
466
466
|
disabled: disabled
|
package/dist/router-link.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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<
|
|
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<
|
|
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:
|
|
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<
|
|
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:
|
|
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<
|
|
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:
|
|
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<
|
|
121
|
+
type: PropType<RouterLinkProps["layerOptions"]>;
|
|
122
122
|
};
|
|
123
123
|
/**
|
|
124
|
-
* Custom
|
|
125
|
-
*
|
|
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
|
-
|
|
148
|
-
type: PropType<
|
|
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<
|
|
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<
|
|
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:
|
|
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<
|
|
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:
|
|
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<
|
|
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:
|
|
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<
|
|
203
|
+
type: PropType<RouterLinkProps["layerOptions"]>;
|
|
222
204
|
};
|
|
223
205
|
/**
|
|
224
|
-
* Custom
|
|
225
|
-
*
|
|
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
|
-
|
|
248
|
-
type: PropType<
|
|
211
|
+
beforeNavigate: {
|
|
212
|
+
type: PropType<RouterLinkProps["beforeNavigate"]>;
|
|
249
213
|
};
|
|
250
214
|
}>> & Readonly<{}>, {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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>;
|
package/dist/router-link.mjs
CHANGED
|
@@ -18,12 +18,18 @@ export const RouterLink = defineComponent({
|
|
|
18
18
|
* @default 'push'
|
|
19
19
|
* @example 'push' | 'replace' | 'pushWindow' | 'replaceWindow' | 'pushLayer'
|
|
20
20
|
*/
|
|
21
|
-
type: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
71
|
+
layerOptions: {
|
|
72
|
+
type: Object
|
|
73
|
+
},
|
|
61
74
|
/**
|
|
62
|
-
* Custom
|
|
63
|
-
*
|
|
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
|
-
|
|
80
|
+
beforeNavigate: {
|
|
86
81
|
type: Function
|
|
87
82
|
}
|
|
88
83
|
},
|
|
89
84
|
setup(props, context) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
106
|
+
link.tag,
|
|
130
107
|
{
|
|
131
|
-
attrs:
|
|
108
|
+
attrs: {
|
|
109
|
+
...attributes,
|
|
110
|
+
...context.attrs
|
|
111
|
+
},
|
|
132
112
|
class: className,
|
|
133
|
-
on:
|
|
113
|
+
on: link.createEventHandlers()
|
|
134
114
|
},
|
|
135
|
-
(_a = slots.default) == null ? void 0 :
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
84
|
+
"gitHead": "e5a1e811403bf1db4437dff88c3ea8bc6b576f64"
|
|
85
85
|
}
|
package/src/router-link.test.ts
CHANGED
|
@@ -66,7 +66,20 @@ describe('router-link.ts - RouterLink Component', () => {
|
|
|
66
66
|
app.unmount();
|
|
67
67
|
}
|
|
68
68
|
if (router) {
|
|
69
|
-
|
|
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', () => {
|
package/src/router-link.ts
CHANGED
|
@@ -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<
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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<
|
|
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
|
|
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: {
|
|
122
|
+
layerOptions: {
|
|
123
|
+
type: Object as PropType<RouterLinkProps['layerOptions']>
|
|
124
|
+
},
|
|
117
125
|
/**
|
|
118
|
-
* Custom
|
|
119
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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