Skip to content

Commit adecc2b

Browse files
feat(link): add 'exact-path' matching option
1 parent 7a4c44b commit adecc2b

File tree

9 files changed

+101
-23
lines changed

9 files changed

+101
-23
lines changed

Diff for: docs/api/README.md

+33
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,30 @@ In this case the `<a>` will be the actual link (and will get the correct `href`)
131131

132132
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.
133133

134+
### exact-path
135+
136+
- type: `boolean`
137+
- default: `false`
138+
139+
The exact active class matching behavior is **strict match**. For example, `<router-link to="/a" exact>` will get this class applied as long as the current path is `/a`.
140+
141+
One consequence of this is that `<router-link to="/a" exact>` won't be active when the query parameters do not match. To force the link into "exact path match mode", use the `exact-path` prop:
142+
143+
```html
144+
<!-- this link will also be active at `/a?page=2` or `/a#foo` -->
145+
<router-link to="/a" exact-path>
146+
```
147+
148+
This is useful when using pagination
149+
150+
### exact-path-active-class
151+
152+
- type: `string`
153+
- default: `"router-link-exact-path-active"`
154+
155+
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.
156+
157+
134158
## `<router-view>`
135159

136160
The `<router-view>` component is a functional component that renders the matched component for the given path. Components rendered in `<router-view>` can also contain its own `<router-view>`, which will render components for nested paths.
@@ -260,6 +284,15 @@ Since it's just a component, it works with `<transition>` and `<keep-alive>`. Wh
260284

261285
Setting this to `false` essentially makes every `router-link` navigation a full page refresh in IE9. This is useful when the app is server-rendered and needs to work in IE9, because a hash mode URL does not work with SSR.
262286

287+
### linkExactPathActiveClass
288+
289+
- type: `string`
290+
291+
- default: `"router-link-exact-path-active"`
292+
293+
Globally configure `<router-link>` default active class for exact path matches. Also see [router-link](router-link.md).
294+
295+
263296
## Router Instance Properties
264297

265298
### router.app

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

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ declare type RouterOptions = {
3636
fallback?: boolean;
3737
base?: string;
3838
linkActiveClass?: string;
39+
linkExactPathActiveClass?: string;
3940
parseQuery?: (query: string) => Object;
4041
stringifyQuery?: (query: Object) => string;
4142
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-5
Original file line numberDiff line numberDiff line change
@@ -70,22 +70,20 @@ 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, exactPath: ?boolean): boolean {
7474
if (b === START) {
7575
return a === b
7676
} else if (!b) {
7777
return false
7878
} else if (a.path && b.path) {
7979
return (
8080
a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
81-
a.hash === b.hash &&
82-
isObjectEqual(a.query, b.query)
81+
(exactPath || (a.hash === b.hash && isObjectEqual(a.query, b.query)))
8382
)
8483
} else if (a.name && b.name) {
8584
return (
8685
a.name === b.name &&
87-
a.hash === b.hash &&
88-
isObjectEqual(a.query, b.query) &&
86+
(exactPath || (a.hash === b.hash && isObjectEqual(a.query, b.query))) &&
8987
isObjectEqual(a.params, b.params)
9088
)
9189
} 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, 11], null, [7], null, [11])
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('exact path', () => {
71+
const a = { path: '/abc' }
72+
const b = { path: '/abc', query: { foo: 'bar' }, hash: '#foo' }
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)