Skip to content

feat(link): add 'exact-path' matching option #2193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,30 @@ In this case the `<a>` will be the actual link (and will get the correct `href`)

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.

### exact-path

- type: `boolean`
- default: `false`

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`.

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:

```html
<!-- this link will also be active at `/a?page=2` or `/a#foo` -->
<router-link to="/a" exact-path>
```

This is useful when using pagination

### exact-path-active-class

- type: `string`
- default: `"router-link-exact-path-active"`

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.


## `<router-view>`

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.
Expand Down Expand Up @@ -260,6 +284,15 @@ Since it's just a component, it works with `<transition>` and `<keep-alive>`. Wh

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.

### linkExactPathActiveClass

- type: `string`

- default: `"router-link-exact-path-active"`

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


## Router Instance Properties

### router.app
Expand Down
6 changes: 6 additions & 0 deletions examples/active-links/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ new Vue({

<li><router-link to="/users">/users</router-link></li>
<li><router-link to="/users" exact>/users (exact match)</router-link></li>
<li><router-link to="/users?foo=bar" exact-path>/users?foo=bar (exact path match)</router-link></li>

<li><router-link to="/users/evan">/users/evan</router-link></li>
<li><router-link to="/users/evan#foo">/users/evan#foo</router-link></li>
Expand All @@ -60,6 +61,11 @@ new Vue({
/users/evan?foo=bar&baz=qux
</router-link>
</li>
<li>
<router-link :to="{ name: 'user', params: { username: 'evan' }, query: { baz: 'qux' }}" exact-path>
/users/evan?baz=qux (named view + exact path match)
</router-link>
</li>

<li><router-link to="/about">/about</router-link></li>

Expand Down
3 changes: 3 additions & 0 deletions examples/active-links/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
a.router-link-exact-active, li.router-link-exact-active a {
border-bottom: 1px solid #f66;
}
a.router-link-exact-path-active, li.router-link-exact-path-active a {
border-bottom: 1px solid #f66;
}
</style>
<a href="/">&larr; Examples index</a>
<div id="app"></div>
Expand Down
1 change: 1 addition & 0 deletions flow/declarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ declare type RouterOptions = {
fallback?: boolean;
base?: string;
linkActiveClass?: string;
linkExactPathActiveClass?: string;
linkExactActiveClass?: string;
parseQuery?: (query: string) => Object;
stringifyQuery?: (query: Object) => string;
Expand Down
14 changes: 13 additions & 1 deletion src/components/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ export default {
default: 'a'
},
exact: Boolean,
exactPath: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
exactPathActiveClass: String,
event: {
type: eventTypes,
default: 'click'
Expand All @@ -36,27 +38,37 @@ export default {
const classes = {}
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
const globalExactPathActiveClass = router.options.linkExactPathActiveClass
// Support global empty active class
const activeClassFallback = globalActiveClass == null
? 'router-link-active'
: globalActiveClass
const exactActiveClassFallback = globalExactActiveClass == null
? 'router-link-exact-active'
: globalExactActiveClass
const exactPathActiveClassFallback = globalExactPathActiveClass == null
? 'router-link-exact-path-active'
: globalExactPathActiveClass
const activeClass = this.activeClass == null
? activeClassFallback
: this.activeClass
const exactActiveClass = this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass
const exactPathActiveClass = this.exactPathActiveClass == null
? exactPathActiveClassFallback
: this.exactPathActiveClass
const compareTarget = location.path
? createRoute(null, location, null, router)
: route

classes[exactPathActiveClass] = this.exactPath && isSameRoute(current, compareTarget, true)
classes[exactActiveClass] = isSameRoute(current, compareTarget)
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
: this.exactPath
? classes[exactPathActiveClass]
: isIncludedRoute(current, compareTarget)

const handler = e => {
if (guardEvent(e)) {
Expand Down
8 changes: 3 additions & 5 deletions src/util/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,20 @@ function getFullPath (
return (path || '/') + stringify(query) + hash
}

export function isSameRoute (a: Route, b: ?Route): boolean {
export function isSameRoute (a: Route, b: ?Route, exactPath: ?boolean): boolean {
if (b === START) {
return a === b
} else if (!b) {
return false
} else if (a.path && b.path) {
return (
a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
a.hash === b.hash &&
isObjectEqual(a.query, b.query)
(exactPath || (a.hash === b.hash && isObjectEqual(a.query, b.query)))
)
} else if (a.name && b.name) {
return (
a.name === b.name &&
a.hash === b.hash &&
isObjectEqual(a.query, b.query) &&
(exactPath || (a.hash === b.hash && isObjectEqual(a.query, b.query))) &&
isObjectEqual(a.params, b.params)
)
} else {
Expand Down
46 changes: 29 additions & 17 deletions test/e2e/specs/active-links.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,40 @@ module.exports = {
browser
.url('http://localhost:8080/active-links/')
.waitForElementVisible('#app', 1000)
.assert.count('li a', 11)
.assert.count('li a', 13)
// assert correct href with base
.assert.attributeContains('li:nth-child(1) a', 'href', '/active-links/')
.assert.attributeContains('li:nth-child(2) a', 'href', '/active-links/')
.assert.attributeContains('li:nth-child(3) a', 'href', '/active-links/users')
.assert.attributeContains('li:nth-child(4) a', 'href', '/active-links/users')
.assert.attributeContains('li:nth-child(5) a', 'href', '/active-links/users/evan')
.assert.attributeContains('li:nth-child(6) a', 'href', '/active-links/users/evan#foo')
.assert.attributeContains('li:nth-child(7) a', 'href', '/active-links/users/evan?foo=bar')
.assert.attributeContains('li:nth-child(5) a', 'href', '/active-links/users?foo=bar')
.assert.attributeContains('li:nth-child(6) a', 'href', '/active-links/users/evan')
.assert.attributeContains('li:nth-child(7) a', 'href', '/active-links/users/evan#foo')
.assert.attributeContains('li:nth-child(8) a', 'href', '/active-links/users/evan?foo=bar')
.assert.attributeContains('li:nth-child(9) a', 'href', '/active-links/users/evan?foo=bar&baz=qux')
.assert.attributeContains('li:nth-child(10) a', 'href', '/active-links/about')
.assert.attributeContains('li:nth-child(11) a', 'href', '/active-links/about')
.assert.attributeContains('li:nth-child(9) a', 'href', '/active-links/users/evan?foo=bar')
.assert.attributeContains('li:nth-child(10) a', 'href', '/active-links/users/evan?foo=bar&baz=qux')
.assert.attributeContains('li:nth-child(11) a', 'href', '/active-links/users/evan?baz=qux')
.assert.attributeContains('li:nth-child(12) a', 'href', '/active-links/about')
.assert.attributeContains('li:nth-child(13) a', 'href', '/active-links/about')
.assert.containsText('.view', 'Home')

assertActiveLinks(1, [1, 2], null, [1, 2])
assertActiveLinks(2, [1, 2], null, [1, 2])
assertActiveLinks(3, [1, 3, 4], null, [3, 4])
assertActiveLinks(4, [1, 3, 4], null, [3, 4])
assertActiveLinks(5, [1, 3, 5], null, [5])
assertActiveLinks(6, [1, 3, 5, 6], null, [6])
assertActiveLinks(7, [1, 3, 5, 7, 8], null, [7, 8])
assertActiveLinks(8, [1, 3, 5, 7, 8], null, [7, 8])
assertActiveLinks(9, [1, 3, 5, 7, 9], null, [9])
assertActiveLinks(10, [1, 10], [11], [10], [11])
assertActiveLinks(11, [1, 10], [11], [10], [11])
assertActiveLinks(3, [1, 3, 4, 5], null, [3, 4], null, [5])
assertActiveLinks(4, [1, 3, 4, 5], null, [3, 4], null, [5])
assertActiveLinks(5, [1, 3, 5], null, [5], null, [5])
assertActiveLinks(6, [1, 3, 6, 11], null, [6], null, [11])
assertActiveLinks(7, [1, 3, 6, 7, 11], null, [7], null, [11])
assertActiveLinks(8, [1, 3, 6, 8, 9, 11], null, [8, 9], null, [11])
assertActiveLinks(9, [1, 3, 6, 8, 9, 11], null, [8, 9], null, [11])
assertActiveLinks(10, [1, 3, 6, 8, 10, 11], null, [10], null, [11])
assertActiveLinks(11, [1, 3, 6, 11], null, [11], null, [11])
assertActiveLinks(12, [1, 12], [13], [12], [13])
assertActiveLinks(13, [1, 12], [13], [12], [13])

browser.end()

function assertActiveLinks (n, activeA, activeLI, exactActiveA, exactActiveLI) {
function assertActiveLinks (n, activeA, activeLI, exactActiveA, exactActiveLI, exactPathActiveA, exactPathActiveLI) {
browser.click(`li:nth-child(${n}) a`)
activeA.forEach(i => {
browser.assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-active')
Expand All @@ -49,6 +53,14 @@ module.exports = {
browser.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-exact-active')
.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-active')
})
exactPathActiveA && exactPathActiveA.forEach(i => {
browser.assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-exact-path-active')
.assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-active')
})
exactPathActiveLI && exactPathActiveLI.forEach(i => {
browser.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-exact-path-active')
.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-active')
})
}
}
}
12 changes: 12 additions & 0 deletions test/unit/specs/route.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ describe('Route utils', () => {
expect(isSameRoute(a, b)).toBe(true)
expect(isSameRoute(a, c)).toBe(false)
})

it('exact path', () => {
const a = { path: '/abc' }
const b = { path: '/abc', query: { foo: 'bar' }, hash: '#foo' }
const c = { path: '/abc', query: { baz: 'qux' }}
const d = { path: '/xyz', query: { foo: 'bar' }}
expect(isSameRoute(a, b, true)).toBe(true)
expect(isSameRoute(a, c, true)).toBe(true)
expect(isSameRoute(a, d, true)).toBe(false)
expect(isSameRoute(b, c, true)).toBe(true)
expect(isSameRoute(b, d, true)).toBe(false)
})
})

describe('isIncludedRoute', () => {
Expand Down
1 change: 1 addition & 0 deletions types/router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface RouterOptions {
base?: string;
linkActiveClass?: string;
linkExactActiveClass?: string;
linkExactPathActiveClass?: string;
parseQuery?: (query: string) => Object;
stringifyQuery?: (query: Object) => string;
scrollBehavior?: (
Expand Down