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 12, 2018
1 parent 83443ed commit 0ffa4c8
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 31 deletions.
1 change: 1 addition & 0 deletions docs/en/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- [base](api/options.md#base)
- [linkActiveClass](api/options.md#linkactiveclass)
- [linkExactActiveClass](api/options.md#linkexactactiveclass)
- [linkExactPathActiveClass](api/options.md#linkexactpathactiveclass)
- [scrollBehavior](api/options.md#scrollbehavior)
- [parseQuery / stringifyQuery](api/options.md#parsequery--stringifyquery)
- [fallback](api/options.md#fallback)
Expand Down
10 changes: 10 additions & 0 deletions docs/en/api/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@

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

### linkExactPathActiveClass

> 3.X.Y+

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

### scrollBehavior

- type: `Function`
Expand Down
40 changes: 30 additions & 10 deletions docs/en/api/router-link.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,26 @@

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.

- **exact-active-class**

> 2.5.0+

- type: `string`

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

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-active-class**

> 3.X.Y+

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

- **exact**

- type: `boolean`
Expand All @@ -105,25 +125,25 @@

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

- **event**
- **exact-path**

> 2.1.0+
> 3.X.Y+

- type: `string | Array<string>`
- type: `boolean`

- default: `'click'`
- default: `false`

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

- **exact-active-class**
- **event**

> 2.5.0+
> 2.1.0+

- type: `string`
- type: `string | Array<string>`

- default: `"router-link-exact-active"`
- default: `'click'`

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.
Specify the event(s) that can trigger the link navigation.

### Applying Active Class to Outer Element

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
2 changes: 2 additions & 0 deletions flow/declarations.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ declare type RouterOptions = {
fallback?: boolean;
base?: string;
linkActiveClass?: string;
linkExactActiveClass?: string;
linkExactPathActiveClass?: string;
parseQuery?: (query: string) => Object;
stringifyQuery?: (query: Object) => string;
scrollBehavior?: (
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
6 changes: 3 additions & 3 deletions src/util/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function getFullPath (
return (path || '/') + stringify(query) + hash
}

export function isSameRoute (a: Route, b: ?Route): boolean {
export function isSameRoute (a: Route, b: ?Route, ignoreQuery: ?boolean): boolean {
if (b === START) {
return a === b
} else if (!b) {
Expand All @@ -79,13 +79,13 @@ export function isSameRoute (a: Route, b: ?Route): boolean {
return (
a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
a.hash === b.hash &&
isObjectEqual(a.query, b.query)
(ignoreQuery || 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) &&
(ignoreQuery || 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], null, [7])
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('ignore query', () => {
const a = { path: '/abc' }
const b = { path: '/abc', query: { foo: 'bar' }}
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

0 comments on commit 0ffa4c8

Please sign in to comment.