Skip to content

Commit

Permalink
feat(link): add 'exact-path' matching option
Browse files Browse the repository at this point in the history
  • Loading branch information
emanuelmutschlechner committed May 31, 2018
1 parent 7a4c44b commit adecc2b
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 23 deletions.
33 changes: 33 additions & 0 deletions docs/api/README.md
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
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
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
Expand Up @@ -36,6 +36,7 @@ declare type RouterOptions = {
fallback?: boolean;
base?: string;
linkActiveClass?: string;
linkExactPathActiveClass?: string;
parseQuery?: (query: string) => Object;
stringifyQuery?: (query: Object) => string;
scrollBehavior?: (
Expand Down
14 changes: 13 additions & 1 deletion src/components/link.js
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
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
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
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
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

0 comments on commit adecc2b

Please sign in to comment.