Skip to content

Commit 0ffa4c8

Browse files
feat(link): add 'exact-path' matching option
1 parent 83443ed commit 0ffa4c8

File tree

11 files changed

+110
-31
lines changed

11 files changed

+110
-31
lines changed

Diff for: docs/en/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
- [base](api/options.md#base)
3030
- [linkActiveClass](api/options.md#linkactiveclass)
3131
- [linkExactActiveClass](api/options.md#linkexactactiveclass)
32+
- [linkExactPathActiveClass](api/options.md#linkexactpathactiveclass)
3233
- [scrollBehavior](api/options.md#scrollbehavior)
3334
- [parseQuery / stringifyQuery](api/options.md#parsequery--stringifyquery)
3435
- [fallback](api/options.md#fallback)

Diff for: docs/en/api/options.md

+10
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@
6767

6868
Globally configure `<router-link>` default active class for exact matches. Also see [router-link](router-link.md).
6969

70+
### linkExactPathActiveClass
71+
72+
> 3.X.Y+
73+
74+
- type: `string`
75+
76+
- default: `"router-link-exact-path-active"`
77+
78+
Globally configure `<router-link>` default active class for exact path matches. Also see [router-link](router-link.md).
79+
7080
### scrollBehavior
7181

7282
- type: `Function`

Diff for: docs/en/api/router-link.md

+30-10
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,26 @@
8888

8989
Configure the active CSS class applied when the link is active. Note the default value can also be configured globally via the `linkActiveClass` router constructor option.
9090

91+
- **exact-active-class**
92+
93+
> 2.5.0+
94+
95+
- type: `string`
96+
97+
- default: `"router-link-exact-active"`
98+
99+
Configure the active CSS class applied when the link is active with exact match. Note the default value can also be configured globally via the `linkExactActiveClass` router constructor option.
100+
101+
- **exact-path-active-class**
102+
103+
> 3.X.Y+
104+
105+
- type: `string`
106+
107+
- default: `"router-link-exact-path-active"`
108+
109+
Configure the active CSS class applied when the link is active with exact path match. Note the default value can also be configured globally via the `linkExactPathActiveClass` router constructor option.
110+
91111
- **exact**
92112

93113
- type: `boolean`
@@ -105,25 +125,25 @@
105125

106126
Check out more examples explaining active link class [live](https://jsfiddle.net/8xrk1n9f/).
107127

108-
- **event**
128+
- **exact-path**
109129

110-
> 2.1.0+
130+
> 3.X.Y+
111131
112-
- type: `string | Array<string>`
132+
- type: `boolean`
113133

114-
- default: `'click'`
134+
- default: `false`
115135

116-
Specify the event(s) that can trigger the link navigation.
136+
Same as `exact` matching, but ignoring query parameters.
117137

118-
- **exact-active-class**
138+
- **event**
119139

120-
> 2.5.0+
140+
> 2.1.0+
121141
122-
- type: `string`
142+
- type: `string | Array<string>`
123143

124-
- default: `"router-link-exact-active"`
144+
- default: `'click'`
125145

126-
Configure the active CSS class applied when the link is active with exact match. Note the default value can also be configured globally via the `linkExactActiveClass` router constructor option.
146+
Specify the event(s) that can trigger the link navigation.
127147

128148
### Applying Active Class to Outer Element
129149

Diff for: examples/active-links/app.js

+6
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ new Vue({
4242
4343
<li><router-link to="/users">/users</router-link></li>
4444
<li><router-link to="/users" exact>/users (exact match)</router-link></li>
45+
<li><router-link to="/users?foo=bar" exact-path>/users?foo=bar (exact path match)</router-link></li>
4546
4647
<li><router-link to="/users/evan">/users/evan</router-link></li>
4748
<li><router-link to="/users/evan#foo">/users/evan#foo</router-link></li>
@@ -60,6 +61,11 @@ new Vue({
6061
/users/evan?foo=bar&baz=qux
6162
</router-link>
6263
</li>
64+
<li>
65+
<router-link :to="{ name: 'user', params: { username: 'evan' }, query: { baz: 'qux' }}" exact-path>
66+
/users/evan?baz=qux (named view + exact path match)
67+
</router-link>
68+
</li>
6369
6470
<li><router-link to="/about">/about</router-link></li>
6571

Diff for: examples/active-links/index.html

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
a.router-link-exact-active, li.router-link-exact-active a {
88
border-bottom: 1px solid #f66;
99
}
10+
a.router-link-exact-path-active, li.router-link-exact-path-active a {
11+
border-bottom: 1px solid #f66;
12+
}
1013
</style>
1114
<a href="/">&larr; Examples index</a>
1215
<div id="app"></div>

Diff for: flow/declarations.js

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ declare type RouterOptions = {
3636
fallback?: boolean;
3737
base?: string;
3838
linkActiveClass?: string;
39+
linkExactActiveClass?: string;
40+
linkExactPathActiveClass?: string;
3941
parseQuery?: (query: string) => Object;
4042
stringifyQuery?: (query: Object) => string;
4143
scrollBehavior?: (

Diff for: src/components/link.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ export default {
1919
default: 'a'
2020
},
2121
exact: Boolean,
22+
exactPath: Boolean,
2223
append: Boolean,
2324
replace: Boolean,
2425
activeClass: String,
2526
exactActiveClass: String,
27+
exactPathActiveClass: String,
2628
event: {
2729
type: eventTypes,
2830
default: 'click'
@@ -36,27 +38,37 @@ export default {
3638
const classes = {}
3739
const globalActiveClass = router.options.linkActiveClass
3840
const globalExactActiveClass = router.options.linkExactActiveClass
41+
const globalExactPathActiveClass = router.options.linkExactPathActiveClass
3942
// Support global empty active class
4043
const activeClassFallback = globalActiveClass == null
4144
? 'router-link-active'
4245
: globalActiveClass
4346
const exactActiveClassFallback = globalExactActiveClass == null
4447
? 'router-link-exact-active'
4548
: globalExactActiveClass
49+
const exactPathActiveClassFallback = globalExactPathActiveClass == null
50+
? 'router-link-exact-path-active'
51+
: globalExactPathActiveClass
4652
const activeClass = this.activeClass == null
4753
? activeClassFallback
4854
: this.activeClass
4955
const exactActiveClass = this.exactActiveClass == null
5056
? exactActiveClassFallback
5157
: this.exactActiveClass
58+
const exactPathActiveClass = this.exactPathActiveClass == null
59+
? exactPathActiveClassFallback
60+
: this.exactPathActiveClass
5261
const compareTarget = location.path
5362
? createRoute(null, location, null, router)
5463
: route
5564

65+
classes[exactPathActiveClass] = this.exactPath && isSameRoute(current, compareTarget, true)
5666
classes[exactActiveClass] = isSameRoute(current, compareTarget)
5767
classes[activeClass] = this.exact
5868
? classes[exactActiveClass]
59-
: isIncludedRoute(current, compareTarget)
69+
: this.exactPath
70+
? classes[exactPathActiveClass]
71+
: isIncludedRoute(current, compareTarget)
6072

6173
const handler = e => {
6274
if (guardEvent(e)) {

Diff for: src/util/route.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function getFullPath (
7070
return (path || '/') + stringify(query) + hash
7171
}
7272

73-
export function isSameRoute (a: Route, b: ?Route): boolean {
73+
export function isSameRoute (a: Route, b: ?Route, ignoreQuery: ?boolean): boolean {
7474
if (b === START) {
7575
return a === b
7676
} else if (!b) {
@@ -79,13 +79,13 @@ export function isSameRoute (a: Route, b: ?Route): boolean {
7979
return (
8080
a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
8181
a.hash === b.hash &&
82-
isObjectEqual(a.query, b.query)
82+
(ignoreQuery || isObjectEqual(a.query, b.query))
8383
)
8484
} else if (a.name && b.name) {
8585
return (
8686
a.name === b.name &&
8787
a.hash === b.hash &&
88-
isObjectEqual(a.query, b.query) &&
88+
(ignoreQuery || isObjectEqual(a.query, b.query)) &&
8989
isObjectEqual(a.params, b.params)
9090
)
9191
} else {

Diff for: test/e2e/specs/active-links.js

+29-17
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,40 @@ module.exports = {
44
browser
55
.url('http://localhost:8080/active-links/')
66
.waitForElementVisible('#app', 1000)
7-
.assert.count('li a', 11)
7+
.assert.count('li a', 13)
88
// assert correct href with base
99
.assert.attributeContains('li:nth-child(1) a', 'href', '/active-links/')
1010
.assert.attributeContains('li:nth-child(2) a', 'href', '/active-links/')
1111
.assert.attributeContains('li:nth-child(3) a', 'href', '/active-links/users')
1212
.assert.attributeContains('li:nth-child(4) a', 'href', '/active-links/users')
13-
.assert.attributeContains('li:nth-child(5) a', 'href', '/active-links/users/evan')
14-
.assert.attributeContains('li:nth-child(6) a', 'href', '/active-links/users/evan#foo')
15-
.assert.attributeContains('li:nth-child(7) a', 'href', '/active-links/users/evan?foo=bar')
13+
.assert.attributeContains('li:nth-child(5) a', 'href', '/active-links/users?foo=bar')
14+
.assert.attributeContains('li:nth-child(6) a', 'href', '/active-links/users/evan')
15+
.assert.attributeContains('li:nth-child(7) a', 'href', '/active-links/users/evan#foo')
1616
.assert.attributeContains('li:nth-child(8) a', 'href', '/active-links/users/evan?foo=bar')
17-
.assert.attributeContains('li:nth-child(9) a', 'href', '/active-links/users/evan?foo=bar&baz=qux')
18-
.assert.attributeContains('li:nth-child(10) a', 'href', '/active-links/about')
19-
.assert.attributeContains('li:nth-child(11) a', 'href', '/active-links/about')
17+
.assert.attributeContains('li:nth-child(9) a', 'href', '/active-links/users/evan?foo=bar')
18+
.assert.attributeContains('li:nth-child(10) a', 'href', '/active-links/users/evan?foo=bar&baz=qux')
19+
.assert.attributeContains('li:nth-child(11) a', 'href', '/active-links/users/evan?baz=qux')
20+
.assert.attributeContains('li:nth-child(12) a', 'href', '/active-links/about')
21+
.assert.attributeContains('li:nth-child(13) a', 'href', '/active-links/about')
2022
.assert.containsText('.view', 'Home')
2123

2224
assertActiveLinks(1, [1, 2], null, [1, 2])
2325
assertActiveLinks(2, [1, 2], null, [1, 2])
24-
assertActiveLinks(3, [1, 3, 4], null, [3, 4])
25-
assertActiveLinks(4, [1, 3, 4], null, [3, 4])
26-
assertActiveLinks(5, [1, 3, 5], null, [5])
27-
assertActiveLinks(6, [1, 3, 5, 6], null, [6])
28-
assertActiveLinks(7, [1, 3, 5, 7, 8], null, [7, 8])
29-
assertActiveLinks(8, [1, 3, 5, 7, 8], null, [7, 8])
30-
assertActiveLinks(9, [1, 3, 5, 7, 9], null, [9])
31-
assertActiveLinks(10, [1, 10], [11], [10], [11])
32-
assertActiveLinks(11, [1, 10], [11], [10], [11])
26+
assertActiveLinks(3, [1, 3, 4, 5], null, [3, 4], null, [5])
27+
assertActiveLinks(4, [1, 3, 4, 5], null, [3, 4], null, [5])
28+
assertActiveLinks(5, [1, 3, 5], null, [5], null, [5])
29+
assertActiveLinks(6, [1, 3, 6, 11], null, [6], null, [11])
30+
assertActiveLinks(7, [1, 3, 6, 7], null, [7])
31+
assertActiveLinks(8, [1, 3, 6, 8, 9, 11], null, [8, 9], null, [11])
32+
assertActiveLinks(9, [1, 3, 6, 8, 9, 11], null, [8, 9], null, [11])
33+
assertActiveLinks(10, [1, 3, 6, 8, 10, 11], null, [10], null, [11])
34+
assertActiveLinks(11, [1, 3, 6, 11], null, [11], null, [11])
35+
assertActiveLinks(12, [1, 12], [13], [12], [13])
36+
assertActiveLinks(13, [1, 12], [13], [12], [13])
3337

3438
browser.end()
3539

36-
function assertActiveLinks (n, activeA, activeLI, exactActiveA, exactActiveLI) {
40+
function assertActiveLinks (n, activeA, activeLI, exactActiveA, exactActiveLI, exactPathActiveA, exactPathActiveLI) {
3741
browser.click(`li:nth-child(${n}) a`)
3842
activeA.forEach(i => {
3943
browser.assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-active')
@@ -49,6 +53,14 @@ module.exports = {
4953
browser.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-exact-active')
5054
.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-active')
5155
})
56+
exactPathActiveA && exactPathActiveA.forEach(i => {
57+
browser.assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-exact-path-active')
58+
.assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-active')
59+
})
60+
exactPathActiveLI && exactPathActiveLI.forEach(i => {
61+
browser.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-exact-path-active')
62+
.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-active')
63+
})
5264
}
5365
}
5466
}

Diff for: test/unit/specs/route.spec.js

+12
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ describe('Route utils', () => {
6666
expect(isSameRoute(a, b)).toBe(true)
6767
expect(isSameRoute(a, c)).toBe(false)
6868
})
69+
70+
it('ignore query', () => {
71+
const a = { path: '/abc' }
72+
const b = { path: '/abc', query: { foo: 'bar' }}
73+
const c = { path: '/abc', query: { baz: 'qux' }}
74+
const d = { path: '/xyz', query: { foo: 'bar' }}
75+
expect(isSameRoute(a, b, true)).toBe(true)
76+
expect(isSameRoute(a, c, true)).toBe(true)
77+
expect(isSameRoute(a, d, true)).toBe(false)
78+
expect(isSameRoute(b, c, true)).toBe(true)
79+
expect(isSameRoute(b, d, true)).toBe(false)
80+
})
6981
})
7082

7183
describe('isIncludedRoute', () => {

Diff for: types/router.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export interface RouterOptions {
5353
base?: string;
5454
linkActiveClass?: string;
5555
linkExactActiveClass?: string;
56+
linkExactPathActiveClass?: string;
5657
parseQuery?: (query: string) => Object;
5758
stringifyQuery?: (query: Object) => string;
5859
scrollBehavior?: (

0 commit comments

Comments
 (0)